xq-lsp 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +77 -9
- package/demo/favicon.svg +6 -0
- package/demo/index.html +2 -0
- package/demo/main.ts +3 -12
- package/dist/analyzer.js +8 -6
- package/dist/completion-core.d.ts +1 -1
- package/dist/completion-core.js +72 -7
- package/dist/diagnostics.d.ts +20 -0
- package/dist/diagnostics.js +28 -0
- package/dist/qt4-worker.js +5 -11
- package/dist/runtimes/existdb/cache.xq +83 -0
- package/dist/runtimes/existdb/compression.xq +198 -0
- package/dist/runtimes/existdb/console.xq +24 -0
- package/dist/runtimes/existdb/contentextraction.xq +31 -0
- package/dist/runtimes/existdb/counter.xq +30 -0
- package/dist/runtimes/existdb/crypto.xq +97 -0
- package/dist/runtimes/existdb/datetime.xq +91 -0
- package/dist/runtimes/existdb/exist.xq +29 -0
- package/dist/runtimes/existdb/file.xq +129 -0
- package/dist/runtimes/existdb/ft.xq +325 -0
- package/dist/runtimes/existdb/httpclient.xq +118 -0
- package/dist/runtimes/existdb/image.xq +65 -0
- package/dist/runtimes/existdb/inspect.xq +49 -0
- package/dist/runtimes/existdb/kwic.xq +24 -0
- package/dist/runtimes/existdb/mail.xq +124 -0
- package/dist/runtimes/existdb/ngram.xq +57 -0
- package/dist/runtimes/existdb/process.xq +8 -0
- package/dist/runtimes/existdb/range.xq +171 -0
- package/dist/runtimes/existdb/repo.xq +125 -0
- package/dist/runtimes/existdb/request.xq +242 -0
- package/dist/runtimes/existdb/response.xq +50 -0
- package/dist/runtimes/existdb/scheduler.xq +35 -0
- package/dist/runtimes/existdb/session.xq +111 -0
- package/dist/runtimes/existdb/sm.xq +497 -0
- package/dist/runtimes/existdb/sort.xq +59 -0
- package/dist/runtimes/existdb/system.xq +267 -0
- package/dist/runtimes/existdb/transform.xq +88 -0
- package/dist/runtimes/existdb/util.xq +892 -0
- package/dist/runtimes/existdb/validation.xq +83 -0
- package/dist/runtimes/existdb/xmldb.xq +482 -0
- package/dist/runtimes/existdb/xmldiff.xq +22 -0
- package/dist/runtimes/fonto/fonto.xq +164 -0
- package/dist/runtimes.d.ts +14 -1
- package/dist/runtimes.js +73 -0
- package/dist/server.js +51 -70
- package/dist/types.d.ts +2 -0
- package/dist/unused-diagnostics.d.ts +1 -1
- package/dist/unused-diagnostics.js +57 -1
- package/dist/variable-diagnostics.d.ts +19 -0
- package/dist/variable-diagnostics.js +164 -0
- package/logo.svg +6 -0
- package/package.json +4 -4
- package/src/analyzer.ts +8 -6
- package/src/completion-core.ts +71 -6
- package/src/diagnostics.ts +43 -0
- package/src/qt4-worker.ts +5 -9
- package/src/runtimes/existdb/exist.xq +29 -0
- package/src/runtimes/existdb/util.xq +32 -0
- package/src/runtimes.ts +76 -1
- package/src/server.ts +53 -73
- package/src/types.ts +2 -0
- package/src/unused-diagnostics.ts +59 -2
- package/src/variable-diagnostics.ts +190 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.5.0...lsp-v1.6.0) (2026-07-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add diagnostics for unused imports and namespace declarations ([0ded7ba](https://github.com/DrRataplan/xq-lsp/commit/0ded7ba26b98bc308ca59cf7061f8ade197a4d13))
|
|
9
|
+
* **claude-code:** add xq-lsp plugin for Claude Code ([aa96a4d](https://github.com/DrRataplan/xq-lsp/commit/aa96a4d5fdd9192e33be862608674d71e020017f))
|
|
10
|
+
* **vscode:** add XQuery 4.0 syntax highlighting grammar ([f2ee489](https://github.com/DrRataplan/xq-lsp/commit/f2ee489ff1ec3ab67f9142d6c039c559d28d5e67))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* add the $exist:* variables to the exist runtime ([4798582](https://github.com/DrRataplan/xq-lsp/commit/4798582c28f036c959432dbcca56edc81f7cca93))
|
|
16
|
+
* copy runtime files to dist and inline existdb predeclared namespaces ([e98b6d6](https://github.com/DrRataplan/xq-lsp/commit/e98b6d672ba504bba5dda30980b7a58cbdfcf847))
|
|
17
|
+
* **vscode:** recognize .xquery file extension ([7afc049](https://github.com/DrRataplan/xq-lsp/commit/7afc04913c1b02419c1965c489c32d09df1834b6))
|
|
18
|
+
|
|
19
|
+
## [1.5.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.4.1...lsp-v1.5.0) (2026-06-18)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **completion:** add function param completions with proper scope isolation ([dab41a3](https://github.com/DrRataplan/xq-lsp/commit/dab41a349ae1d387f1b32b530e476b1d30da8f07))
|
|
25
|
+
* **diagnostics:** report undeclared variable references as XPST0008 ([6ff1fa9](https://github.com/DrRataplan/xq-lsp/commit/6ff1fa9d5a9ecfb4d440119f3efa9e3f96204e05))
|
|
26
|
+
* eXist-db pre-declared namespaces suppress false XQST0081 diagnostics ([cd94650](https://github.com/DrRataplan/xq-lsp/commit/cd94650db968d727717de62e6a60411481a7fb16))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* inject W3C predeclared namespaces in qt4-worker ([4c93ad6](https://github.com/DrRataplan/xq-lsp/commit/4c93ad677db0eb266e04f080ee1a1ad986dfc03a))
|
|
32
|
+
* **variable-diagnostics:** add explicit type annotation to fix TS build ([5867d7e](https://github.com/DrRataplan/xq-lsp/commit/5867d7ea549026cda98559b8d980f3bdf561f32f))
|
|
33
|
+
* **vscode:** make extension work by shimming import.meta.url ([fca848a](https://github.com/DrRataplan/xq-lsp/commit/fca848af51ee17a0dba8e7b95f4767c4d1ce974f))
|
|
34
|
+
|
|
3
35
|
## [1.4.1](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.4.0...lsp-v1.4.1) (2026-06-10)
|
|
4
36
|
|
|
5
37
|
|
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
<img src="./logo.svg" alt="xq-lsp" width="48" height="48">
|
|
2
|
+
|
|
1
3
|
# xq-lsp
|
|
2
4
|
|
|
3
5
|
Language Server Protocol implementation for XQuery, providing autocompletion, hover documentation, signature help, go-to-definition, and diagnostics.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
9
|
+
- **Syntax highlighting** — keywords, types, variables, operators, comments, and XQuery 4.0 tokens (VS Code extension)
|
|
7
10
|
- **Completion** — functions declared in the current file, imported modules, and variables in scope
|
|
8
11
|
- **Hover** — function signatures on hover
|
|
9
12
|
- **Signature help** — active parameter hints inside function calls
|
|
@@ -59,24 +62,70 @@ For files with syntax errors (common while editing), the server falls back to re
|
|
|
59
62
|
|
|
60
63
|
Use the `lib` key to load built-in definitions for a specific runtime:
|
|
61
64
|
|
|
62
|
-
| Value
|
|
63
|
-
|
|
|
64
|
-
| `"fonto"`
|
|
65
|
+
| Value | Runtime |
|
|
66
|
+
| ----------- | ------------------------------------------------------------------------- |
|
|
67
|
+
| `"fonto"` | [Fonto XML editor](https://www.fontoxml.com/) — `fonto:*` functions |
|
|
68
|
+
| `"existdb"` | [eXist-db](https://exist-db.org/) — all extension module functions |
|
|
65
69
|
|
|
66
70
|
```xquery
|
|
67
|
-
map {"glob": "src/**/*.xq", "lib": "
|
|
71
|
+
map {"glob": "src/**/*.xq", "lib": "existdb"}
|
|
68
72
|
```
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
Multiple libs use an XPath sequence:
|
|
71
75
|
|
|
72
76
|
```xquery
|
|
73
|
-
|
|
77
|
+
map {"glob": "src/**/*.xq", "lib": ("fonto", "other")}
|
|
74
78
|
```
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
#### eXist-db runtime
|
|
81
|
+
|
|
82
|
+
The `"existdb"` runtime bundles stub definitions for every eXist-db extension
|
|
83
|
+
module (`util`, `xmldb`, `request`, `sm`, `ft`, `compression`, …).
|
|
84
|
+
|
|
85
|
+
eXist-db automatically pre-declares a large set of namespace prefixes — you can
|
|
86
|
+
call `util:log(…)`, `xmldb:store(…)`, `process:execute(…)`, etc. without any
|
|
87
|
+
`import module namespace` statement. When the `existdb` runtime is active the
|
|
88
|
+
server knows about these pre-declared prefixes and:
|
|
89
|
+
|
|
90
|
+
- **suppresses** false `XQST0081` ("namespace prefix not declared") diagnostics
|
|
91
|
+
for all pre-declared namespaces
|
|
92
|
+
- **provides completions, hover, and go-to-definition** for those modules
|
|
93
|
+
without requiring an explicit `import module namespace` in your source
|
|
94
|
+
|
|
95
|
+
If you do choose to write explicit imports (e.g. for editor portability), the
|
|
96
|
+
server handles those correctly too:
|
|
77
97
|
|
|
78
98
|
```xquery
|
|
79
|
-
|
|
99
|
+
(: explicit import — also fine :)
|
|
100
|
+
import module namespace util = "http://exist-db.org/xquery/util";
|
|
101
|
+
util:uuid()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Modules that require explicit import even in eXist-db (e.g. `datetime`,
|
|
105
|
+
`httpclient`, `console`, `crypto`, `kwic`) still need a `import module
|
|
106
|
+
namespace` declaration; the server offers quick-fix code actions to insert it.
|
|
107
|
+
|
|
108
|
+
```xquery
|
|
109
|
+
(: lsp-config.xq in your project root :)
|
|
110
|
+
map {
|
|
111
|
+
"glob": "src/**/*.xq",
|
|
112
|
+
"lib": "existdb",
|
|
113
|
+
"import": map {
|
|
114
|
+
"generateLocationHints": false()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The `generateLocationHints: false()` setting prevents the server from appending
|
|
120
|
+
`at "…"` paths to generated imports, which eXist-db does not need (it resolves
|
|
121
|
+
modules by namespace URI).
|
|
122
|
+
|
|
123
|
+
#### Fonto runtime
|
|
124
|
+
|
|
125
|
+
Then import the namespace in your XQuery files as usual — the server resolves completions, hover, and go-to-definition against the bundled definitions:
|
|
126
|
+
|
|
127
|
+
```xquery
|
|
128
|
+
import module namespace fonto="http://www.fontoxml.com/functions";
|
|
80
129
|
```
|
|
81
130
|
|
|
82
131
|
### Location hints
|
|
@@ -188,4 +237,23 @@ npx vsce package
|
|
|
188
237
|
code --install-extension xquery-lsp-vscode-*.vsix
|
|
189
238
|
```
|
|
190
239
|
|
|
191
|
-
The extension registers `.xq`, `.xql`, `.xqm`, and `.
|
|
240
|
+
The extension registers `.xq`, `.xql`, `.xqm`, `.xqy`, and `.xquery` files as XQuery, provides syntax highlighting via a bundled TextMate grammar, and starts `xq-lsp` automatically when you open one.
|
|
241
|
+
|
|
242
|
+
## Claude Code
|
|
243
|
+
|
|
244
|
+
A plugin lives in [`editors/claude-code/`](./editors/claude-code/) that registers `xq-lsp` as a native LSP server for Claude Code, giving diagnostics, go-to-definition, find-references, and hover for XQuery files.
|
|
245
|
+
|
|
246
|
+
Make sure the `xq-lsp` binary is on `PATH`:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
npm install -g xq-lsp
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Then, from within Claude Code:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
/plugin marketplace add DrRataplan/xq-lsp
|
|
256
|
+
/plugin install xquery-lsp@xq-lsp
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
See [`editors/claude-code/README.md`](./editors/claude-code/README.md) for local-development setup and details on what the plugin configures.
|
package/demo/favicon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#1e1b4b"/>
|
|
3
|
+
<path d="M5 9 L11 16 L5 23" stroke="#a78bfa" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M27 9 L21 16 L27 23" stroke="#a78bfa" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<circle cx="16" cy="16" r="2.5" fill="#a78bfa"/>
|
|
6
|
+
</svg>
|
package/demo/index.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>xq-lsp playground</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
8
|
<style>
|
|
8
9
|
* {
|
|
9
10
|
box-sizing: border-box;
|
|
@@ -132,6 +133,7 @@
|
|
|
132
133
|
</head>
|
|
133
134
|
<body>
|
|
134
135
|
<header>
|
|
136
|
+
<img src="./favicon.svg" alt="" width="20" height="20" />
|
|
135
137
|
<h1>xq-lsp playground</h1>
|
|
136
138
|
<button id="share-btn">Copy link</button>
|
|
137
139
|
</header>
|
package/demo/main.ts
CHANGED
|
@@ -11,10 +11,7 @@ import builtinsMath from "../builtins/builtins-math.xq?raw";
|
|
|
11
11
|
import builtinsMap from "../builtins/builtins-map.xq?raw";
|
|
12
12
|
import builtinsArray from "../builtins/builtins-array.xq?raw";
|
|
13
13
|
import { findUndeclaredPrefixUsages } from "../src/namespace-diagnostics.ts";
|
|
14
|
-
import {
|
|
15
|
-
import { checkFunctionCalls } from "../src/functioncall-diagnostics.ts";
|
|
16
|
-
import { checkUnused } from "../src/unused-diagnostics.ts";
|
|
17
|
-
import { checkContextItemUsage } from "../src/context-item-diagnostics.ts";
|
|
14
|
+
import { runDiagnostics, runHints } from "../src/diagnostics.ts";
|
|
18
15
|
import { type FileAnalysis } from "../src/types.ts";
|
|
19
16
|
import { resolveFunctionAtOffset, resolveSignatureAtOffset, functionSignature } from "../src/hover-core.ts";
|
|
20
17
|
import { getCompletions } from "../src/completion-core.ts";
|
|
@@ -238,18 +235,12 @@ const xqueryLinter = linter((view): Diagnostic[] => {
|
|
|
238
235
|
|
|
239
236
|
if (ast) {
|
|
240
237
|
const imported = getImported(analysis);
|
|
241
|
-
for (const d of
|
|
242
|
-
diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "warning", message: d.message });
|
|
243
|
-
}
|
|
244
|
-
for (const d of checkFunctionCalls(ast, analysis, imported)) {
|
|
238
|
+
for (const d of runDiagnostics(ast, code, analysis, imported)) {
|
|
245
239
|
diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "error", message: d.message });
|
|
246
240
|
}
|
|
247
|
-
for (const d of
|
|
241
|
+
for (const d of runHints(ast, analysis)) {
|
|
248
242
|
diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "hint", message: d.message });
|
|
249
243
|
}
|
|
250
|
-
for (const d of checkContextItemUsage(ast)) {
|
|
251
|
-
diagnostics.push({ from: d.offset, to: d.offset + d.length, severity: "error", message: d.message });
|
|
252
|
-
}
|
|
253
244
|
}
|
|
254
245
|
|
|
255
246
|
return diagnostics;
|
package/dist/analyzer.js
CHANGED
|
@@ -291,7 +291,7 @@ function extractNamespaceDecls(ast) {
|
|
|
291
291
|
const namespaceUri = uriNode ? stripQuotes(firstTerminalValue(uriNode) ?? "") : null;
|
|
292
292
|
if (!namespaceUri)
|
|
293
293
|
continue;
|
|
294
|
-
results.push({ prefix, namespaceUri });
|
|
294
|
+
results.push({ prefix, namespaceUri, offset: ncname.start ?? nd.start ?? 0 });
|
|
295
295
|
}
|
|
296
296
|
return results;
|
|
297
297
|
}
|
|
@@ -307,7 +307,7 @@ function extractImports(ast) {
|
|
|
307
307
|
continue;
|
|
308
308
|
const namespaceUri = stripQuotes(firstTerminalValue(uris[0]) ?? "");
|
|
309
309
|
const atPath = uris.length >= 2 ? stripQuotes(firstTerminalValue(uris[1]) ?? "") : undefined;
|
|
310
|
-
results.push({ prefix, namespaceUri, atPath });
|
|
310
|
+
results.push({ prefix, namespaceUri, atPath, offset: ncname.start ?? mi.start ?? 0 });
|
|
311
311
|
}
|
|
312
312
|
return results;
|
|
313
313
|
}
|
|
@@ -338,8 +338,8 @@ const RE_FUNC = /declare\s+(?:%[\w:\-]+(?:\([^)]*\))?\s+)*function\s+([\w:\-]+)\
|
|
|
338
338
|
const RE_VAR_DECL = /declare\s+(?:%[\w:\-]+(?:\([^)]*\))?\s+)*variable\s+\$([\w:\-]+)/g;
|
|
339
339
|
const RE_LET = /\blet\s+\$([\w:\-]+)\s*:=/g;
|
|
340
340
|
const RE_FOR = /\bfor\s+\$([\w:\-]+)\s+in\b/g;
|
|
341
|
-
const RE_IMPORT = /import\s+module\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["'](?:\s+at\s+["']([^"']*)["'])?/
|
|
342
|
-
const RE_NS_DECL = /declare\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/
|
|
341
|
+
const RE_IMPORT = /import\s+module\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["'](?:\s+at\s+["']([^"']*)["'])?/gd;
|
|
342
|
+
const RE_NS_DECL = /declare\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/gd;
|
|
343
343
|
function analyzeRegex(text, sourceUri) {
|
|
344
344
|
const moduleVariables = [];
|
|
345
345
|
const localBindings = [];
|
|
@@ -350,7 +350,8 @@ function analyzeRegex(text, sourceUri) {
|
|
|
350
350
|
const imports = [];
|
|
351
351
|
RE_IMPORT.lastIndex = 0;
|
|
352
352
|
while ((m = RE_IMPORT.exec(text)) !== null) {
|
|
353
|
-
const
|
|
353
|
+
const indices = m.indices;
|
|
354
|
+
const imp = { prefix: m[1], namespaceUri: m[2], offset: indices[1][0] };
|
|
354
355
|
if (m[3])
|
|
355
356
|
imp.atPath = m[3];
|
|
356
357
|
imports.push(imp);
|
|
@@ -358,7 +359,8 @@ function analyzeRegex(text, sourceUri) {
|
|
|
358
359
|
const namespaceDecls = [];
|
|
359
360
|
RE_NS_DECL.lastIndex = 0;
|
|
360
361
|
while ((m = RE_NS_DECL.exec(text)) !== null) {
|
|
361
|
-
|
|
362
|
+
const indices = m.indices;
|
|
363
|
+
namespaceDecls.push({ prefix: m[1], namespaceUri: m[2], offset: indices[1][0] });
|
|
362
364
|
}
|
|
363
365
|
const prefixMap = buildPrefixMap(moduleNs, imports, namespaceDecls);
|
|
364
366
|
const functions = [];
|
|
@@ -14,4 +14,4 @@ export interface CompletionEntry {
|
|
|
14
14
|
isSnippet: boolean;
|
|
15
15
|
}
|
|
16
16
|
export declare function functionDoc(fn: FunctionSymbol): string;
|
|
17
|
-
export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean): CompletionEntry[];
|
|
17
|
+
export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis): CompletionEntry[];
|
package/dist/completion-core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatQName } from "./types.js";
|
|
2
|
-
import { resolvePrefix } from "./analyzer.js";
|
|
2
|
+
import { resolvePrefix, nodeStackAtOffset, findAll } from "./analyzer.js";
|
|
3
3
|
function parseToken(textBefore) {
|
|
4
4
|
const match = textBefore.match(/\$([\w:\-]*)$|(?:([\w\-]+):)([\w\-]*)$|([\w\-]+)$/);
|
|
5
5
|
if (!match)
|
|
@@ -41,6 +41,31 @@ function buildVariableEntry(v, filter) {
|
|
|
41
41
|
return null;
|
|
42
42
|
return { label, kind: "variable", insertText: label, isSnippet: false };
|
|
43
43
|
}
|
|
44
|
+
function buildParamEntry(p, filter) {
|
|
45
|
+
if (filter && !p.name.toLowerCase().startsWith(filter.toLowerCase()))
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
label: `$${p.name}`,
|
|
49
|
+
kind: "variable",
|
|
50
|
+
detail: p.type ? `as ${p.type}` : undefined,
|
|
51
|
+
insertText: `$${p.name}`,
|
|
52
|
+
isSnippet: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Find the function symbol enclosing the cursor using the AST. */
|
|
56
|
+
function findEnclosingFunction(ast, cursorOffset, functions) {
|
|
57
|
+
const stack = nodeStackAtOffset(ast, cursorOffset);
|
|
58
|
+
const bodyNode = stack.slice().reverse().find((n) => n.type === "FunctionBody");
|
|
59
|
+
if (!bodyNode || bodyNode.start === undefined || bodyNode.end == null)
|
|
60
|
+
return null;
|
|
61
|
+
const annotatedNode = stack.find((n) => n.type === "AnnotatedDecl");
|
|
62
|
+
if (!annotatedNode || annotatedNode.start === undefined)
|
|
63
|
+
return null;
|
|
64
|
+
const fn = functions.find((f) => f.sourceOffset === annotatedNode.start);
|
|
65
|
+
if (!fn)
|
|
66
|
+
return null;
|
|
67
|
+
return { fn, bodyStart: bodyNode.start, bodyEnd: bodyNode.end };
|
|
68
|
+
}
|
|
44
69
|
function buildFunctionEntry(fn, label, snippets) {
|
|
45
70
|
return {
|
|
46
71
|
label,
|
|
@@ -51,23 +76,63 @@ function buildFunctionEntry(fn, label, snippets) {
|
|
|
51
76
|
isSnippet: snippets && fn.params.length > 0,
|
|
52
77
|
};
|
|
53
78
|
}
|
|
54
|
-
export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets = false) {
|
|
79
|
+
export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets = false, lastValidAnalysis) {
|
|
55
80
|
const token = parseToken(ctx.textBeforeCursor);
|
|
56
81
|
const items = [];
|
|
57
82
|
if (token.kind === "variable") {
|
|
58
83
|
const filter = token.prefix;
|
|
84
|
+
// Module-level variables are always visible
|
|
59
85
|
for (const v of currentAnalysis.moduleVariables) {
|
|
60
86
|
const e = buildVariableEntry(v, filter);
|
|
61
87
|
if (e)
|
|
62
88
|
items.push(e);
|
|
63
89
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
// Prefer the current AST; fall back to the last successfully-parsed one for scope analysis
|
|
91
|
+
const scopeAst = currentAnalysis.ast ?? lastValidAnalysis?.ast;
|
|
92
|
+
if (scopeAst) {
|
|
93
|
+
const enclosing = findEnclosingFunction(scopeAst, ctx.cursorOffset, currentAnalysis.functions);
|
|
94
|
+
if (enclosing) {
|
|
95
|
+
// Inside a function body: params + local bindings within this body before cursor
|
|
96
|
+
for (const p of enclosing.fn.params) {
|
|
97
|
+
const e = buildParamEntry(p, filter);
|
|
98
|
+
if (e)
|
|
99
|
+
items.push(e);
|
|
100
|
+
}
|
|
101
|
+
for (const v of currentAnalysis.localBindings) {
|
|
102
|
+
if (v.offset > enclosing.bodyStart && v.offset < ctx.cursorOffset && v.offset < enclosing.bodyEnd) {
|
|
103
|
+
const e = buildVariableEntry(v, filter);
|
|
104
|
+
if (e)
|
|
105
|
+
items.push(e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// In query body: exclude local bindings that live inside function bodies
|
|
111
|
+
const functionBodyRanges = findAll(scopeAst, "FunctionBody")
|
|
112
|
+
.filter((b) => b.start !== undefined && b.end != null)
|
|
113
|
+
.map((b) => ({ start: b.start, end: b.end }));
|
|
114
|
+
for (const v of currentAnalysis.localBindings) {
|
|
115
|
+
if (v.offset >= ctx.cursorOffset)
|
|
116
|
+
continue;
|
|
117
|
+
if (functionBodyRanges.some((r) => v.offset >= r.start && v.offset <= r.end))
|
|
118
|
+
continue;
|
|
119
|
+
const e = buildVariableEntry(v, filter);
|
|
120
|
+
if (e)
|
|
121
|
+
items.push(e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// No AST ever available: simple offset-based filter, no function-param completions
|
|
127
|
+
for (const v of currentAnalysis.localBindings) {
|
|
128
|
+
if (v.offset < ctx.cursorOffset) {
|
|
129
|
+
const e = buildVariableEntry(v, filter);
|
|
130
|
+
if (e)
|
|
131
|
+
items.push(e);
|
|
132
|
+
}
|
|
69
133
|
}
|
|
70
134
|
}
|
|
135
|
+
// Imported module-level variables are always visible
|
|
71
136
|
for (const analysis of importedAnalyses.values()) {
|
|
72
137
|
for (const v of analysis.moduleVariables) {
|
|
73
138
|
const e = buildVariableEntry(v, filter);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central registry of all diagnostic checkers.
|
|
3
|
+
*
|
|
4
|
+
* Add every new checker here so qt4-worker, server, and the playground
|
|
5
|
+
* all stay in sync automatically.
|
|
6
|
+
*/
|
|
7
|
+
import type { Node } from "xq-parser";
|
|
8
|
+
import type { TypeDiagnostic, FileAnalysis } from "./types.ts";
|
|
9
|
+
/**
|
|
10
|
+
* Run all error-level diagnostics (XQuery static/dynamic error codes).
|
|
11
|
+
* Safe to use in QT4 test coverage checks.
|
|
12
|
+
*
|
|
13
|
+
* @param imports Merged map of imported module analyses and built-in function libraries.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runDiagnostics(ast: Node, text: string, analysis: FileAnalysis, imports: Map<string, FileAnalysis>): TypeDiagnostic[];
|
|
16
|
+
/**
|
|
17
|
+
* Run hint-level diagnostics (unused functions/variables).
|
|
18
|
+
* Produces xq-lsp:* codes — kept separate to avoid false-positives in QT4 tests.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runHints(ast: Node, analysis: FileAnalysis): TypeDiagnostic[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { checkTypes } from "./typechecker.js";
|
|
2
|
+
import { checkFunctionCalls } from "./functioncall-diagnostics.js";
|
|
3
|
+
import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.js";
|
|
4
|
+
import { checkContextItemUsage } from "./context-item-diagnostics.js";
|
|
5
|
+
import { checkUndeclaredVariables } from "./variable-diagnostics.js";
|
|
6
|
+
import { checkUnused } from "./unused-diagnostics.js";
|
|
7
|
+
/**
|
|
8
|
+
* Run all error-level diagnostics (XQuery static/dynamic error codes).
|
|
9
|
+
* Safe to use in QT4 test coverage checks.
|
|
10
|
+
*
|
|
11
|
+
* @param imports Merged map of imported module analyses and built-in function libraries.
|
|
12
|
+
*/
|
|
13
|
+
export function runDiagnostics(ast, text, analysis, imports) {
|
|
14
|
+
return [
|
|
15
|
+
...checkTypes(ast, text, analysis, imports),
|
|
16
|
+
...checkFunctionCalls(ast, analysis, imports),
|
|
17
|
+
...findUndeclaredPrefixUsages(ast, analysis),
|
|
18
|
+
...checkContextItemUsage(ast),
|
|
19
|
+
...checkUndeclaredVariables(ast, analysis, imports),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run hint-level diagnostics (unused functions/variables).
|
|
24
|
+
* Produces xq-lsp:* codes — kept separate to avoid false-positives in QT4 tests.
|
|
25
|
+
*/
|
|
26
|
+
export function runHints(ast, analysis) {
|
|
27
|
+
return checkUnused(ast, analysis);
|
|
28
|
+
}
|
package/dist/qt4-worker.js
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import { workerData, parentPort } from "node:worker_threads";
|
|
2
2
|
import { analyzeWithAst } from "./analyzer.js";
|
|
3
|
-
import {
|
|
4
|
-
import { checkFunctionCalls } from "./functioncall-diagnostics.js";
|
|
5
|
-
import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.js";
|
|
3
|
+
import { runDiagnostics } from "./diagnostics.js";
|
|
6
4
|
import { getBuiltins } from "./builtins.js";
|
|
7
5
|
const BUILTINS = new Map([["builtin:fn", getBuiltins()]]);
|
|
8
|
-
function
|
|
6
|
+
function collectCodes(query, envNamespaces) {
|
|
9
7
|
try {
|
|
10
8
|
const { analysis, ast, parseError } = analyzeWithAst(query, "file:///test.xq");
|
|
11
9
|
for (const ns of envNamespaces) {
|
|
12
10
|
if (ns.prefix && !analysis.namespaceDecls.some((d) => d.prefix === ns.prefix)) {
|
|
13
|
-
analysis.namespaceDecls.push({ prefix: ns.prefix, namespaceUri: ns.uri });
|
|
11
|
+
analysis.namespaceDecls.push({ prefix: ns.prefix, namespaceUri: ns.uri, offset: -1 });
|
|
14
12
|
}
|
|
15
13
|
}
|
|
16
14
|
const codes = [];
|
|
17
15
|
if (parseError)
|
|
18
16
|
codes.push("XPST0003");
|
|
19
17
|
if (ast) {
|
|
20
|
-
for (const d of
|
|
21
|
-
codes.push(d.code);
|
|
22
|
-
for (const d of checkFunctionCalls(ast, analysis, BUILTINS))
|
|
23
|
-
codes.push(d.code);
|
|
24
|
-
for (const d of findUndeclaredPrefixUsages(ast, analysis))
|
|
18
|
+
for (const d of runDiagnostics(ast, query, analysis, BUILTINS))
|
|
25
19
|
codes.push(d.code);
|
|
26
20
|
}
|
|
27
21
|
return [...new Set(codes)];
|
|
@@ -32,7 +26,7 @@ function runDiagnostics(query, envNamespaces) {
|
|
|
32
26
|
}
|
|
33
27
|
const batch = workerData;
|
|
34
28
|
const results = batch.map((tc) => {
|
|
35
|
-
const got =
|
|
29
|
+
const got = collectCodes(tc.query, tc.envNamespaces);
|
|
36
30
|
const hasError = got.length > 0;
|
|
37
31
|
const outcome = tc.expected === "static-error"
|
|
38
32
|
? hasError
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module namespace cache = "http://exist-db.org/xquery/cache";
|
|
2
|
+
|
|
3
|
+
(:~
|
|
4
|
+
: Eviction policy work of the cache is performed asynchronously. Performs any
|
|
5
|
+
: pending maintenance operations needed by the cache, on the current thread.
|
|
6
|
+
: Typically not needed by users, and only used for testing scenarios. Requires
|
|
7
|
+
: 'clear' permissions.
|
|
8
|
+
: @param $cache-name The name of the cache
|
|
9
|
+
:)
|
|
10
|
+
declare function cache:cleanup($cache-name as xs:string) as empty-sequence() external;
|
|
11
|
+
|
|
12
|
+
(:~
|
|
13
|
+
: Clears all key/values from either all caches or the named cache
|
|
14
|
+
:)
|
|
15
|
+
declare function cache:clear() as empty-sequence() external;
|
|
16
|
+
|
|
17
|
+
(:~
|
|
18
|
+
: Clears all key/values from either all caches or the named cache
|
|
19
|
+
: @param $cache-name The name of the cache
|
|
20
|
+
:)
|
|
21
|
+
declare function cache:clear($cache-name as xs:string) as empty-sequence() external;
|
|
22
|
+
|
|
23
|
+
(:~
|
|
24
|
+
: Explicitly create a cache with a specific configuration
|
|
25
|
+
: @param $cache-name The name of the cache
|
|
26
|
+
: @param $config A map with configuration for the cache. At present cache LRU and permission groups may be specified, for operations on the cache. `maximumSize` is optional and specifies the maximum number of entries. `expireAfterAccess` is optional and specifies the expiry period for infrequently accessed entries (in milliseconds). `expireAfterWrite` is optional and specifies the expiry period after the entry's creation, or the most recent replacement of its value (in milliseconds). If a permission group is not specified for an operation, then permissions are not checked for that operation. Should have the format: map { "maximumSize": 1000, "expireAfterAccess": 120000, "expireAfterWrite": 240000, "permissions": map { "put-group": "group1", "get-group": "group2", "remove-group": "group3", "clear-group": "group4"} }
|
|
27
|
+
: @return true if the cache was created, false if the cache already exists
|
|
28
|
+
:)
|
|
29
|
+
declare function cache:create($cache-name as xs:string, $config as map(*)) as xs:boolean external;
|
|
30
|
+
|
|
31
|
+
(:~
|
|
32
|
+
: Destroys a cache entirely
|
|
33
|
+
: @param $cache-name The name of the cache
|
|
34
|
+
:)
|
|
35
|
+
declare function cache:destroy($cache-name as xs:string) as empty-sequence() external;
|
|
36
|
+
|
|
37
|
+
(:~
|
|
38
|
+
: Get data from identified global cache by key
|
|
39
|
+
: @param $cache-name The name of the cache
|
|
40
|
+
: @param $key The key
|
|
41
|
+
: @return The value associated with the key
|
|
42
|
+
:)
|
|
43
|
+
declare function cache:get($cache-name as xs:string, $key as xs:anyType+) as item()* external;
|
|
44
|
+
|
|
45
|
+
(:~
|
|
46
|
+
: List all keys stored in a cache. Note this operation is expensive.
|
|
47
|
+
: @param $cache-name The name of the cache
|
|
48
|
+
: @return The keys in the cache. Note these will be returned in serialized string form, as that is used internally.
|
|
49
|
+
:)
|
|
50
|
+
declare function cache:keys($cache-name as xs:string) as xs:string* external;
|
|
51
|
+
|
|
52
|
+
(:~
|
|
53
|
+
: List all values (for the associated keys) stored in a cache.
|
|
54
|
+
: @param $cache-name The name of the cache
|
|
55
|
+
: @param $keys The keys, if none are specified, all values are returned
|
|
56
|
+
: @return The values associated with the keys
|
|
57
|
+
:)
|
|
58
|
+
declare function cache:list($cache-name as xs:string, $keys as xs:anyType*) as item()* external;
|
|
59
|
+
|
|
60
|
+
(:~
|
|
61
|
+
: Get the names of all current caches
|
|
62
|
+
: @return The names of all caches currently in use.
|
|
63
|
+
:)
|
|
64
|
+
declare function cache:names() as xs:string* external;
|
|
65
|
+
|
|
66
|
+
(:~
|
|
67
|
+
: Put data with a key into the identified cache. Returns the previous value
|
|
68
|
+
: associated with the key
|
|
69
|
+
: @param $cache-name The name of the cache
|
|
70
|
+
: @param $key The key
|
|
71
|
+
: @param $value The value
|
|
72
|
+
: @return The previous value associated with the key
|
|
73
|
+
:)
|
|
74
|
+
declare function cache:put($cache-name as xs:string, $key as xs:anyType+, $value as item()*) as item()* external;
|
|
75
|
+
|
|
76
|
+
(:~
|
|
77
|
+
: Remove data from the identified cache by the key. Returns the value that was
|
|
78
|
+
: previously associated with key
|
|
79
|
+
: @param $cache-name The name of the cache
|
|
80
|
+
: @param $key The key
|
|
81
|
+
: @return The value that was previously associated with the key
|
|
82
|
+
:)
|
|
83
|
+
declare function cache:remove($cache-name as xs:string, $key as xs:anyType+) as item()* external;
|