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.
Files changed (64) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +77 -9
  3. package/demo/favicon.svg +6 -0
  4. package/demo/index.html +2 -0
  5. package/demo/main.ts +3 -12
  6. package/dist/analyzer.js +8 -6
  7. package/dist/completion-core.d.ts +1 -1
  8. package/dist/completion-core.js +72 -7
  9. package/dist/diagnostics.d.ts +20 -0
  10. package/dist/diagnostics.js +28 -0
  11. package/dist/qt4-worker.js +5 -11
  12. package/dist/runtimes/existdb/cache.xq +83 -0
  13. package/dist/runtimes/existdb/compression.xq +198 -0
  14. package/dist/runtimes/existdb/console.xq +24 -0
  15. package/dist/runtimes/existdb/contentextraction.xq +31 -0
  16. package/dist/runtimes/existdb/counter.xq +30 -0
  17. package/dist/runtimes/existdb/crypto.xq +97 -0
  18. package/dist/runtimes/existdb/datetime.xq +91 -0
  19. package/dist/runtimes/existdb/exist.xq +29 -0
  20. package/dist/runtimes/existdb/file.xq +129 -0
  21. package/dist/runtimes/existdb/ft.xq +325 -0
  22. package/dist/runtimes/existdb/httpclient.xq +118 -0
  23. package/dist/runtimes/existdb/image.xq +65 -0
  24. package/dist/runtimes/existdb/inspect.xq +49 -0
  25. package/dist/runtimes/existdb/kwic.xq +24 -0
  26. package/dist/runtimes/existdb/mail.xq +124 -0
  27. package/dist/runtimes/existdb/ngram.xq +57 -0
  28. package/dist/runtimes/existdb/process.xq +8 -0
  29. package/dist/runtimes/existdb/range.xq +171 -0
  30. package/dist/runtimes/existdb/repo.xq +125 -0
  31. package/dist/runtimes/existdb/request.xq +242 -0
  32. package/dist/runtimes/existdb/response.xq +50 -0
  33. package/dist/runtimes/existdb/scheduler.xq +35 -0
  34. package/dist/runtimes/existdb/session.xq +111 -0
  35. package/dist/runtimes/existdb/sm.xq +497 -0
  36. package/dist/runtimes/existdb/sort.xq +59 -0
  37. package/dist/runtimes/existdb/system.xq +267 -0
  38. package/dist/runtimes/existdb/transform.xq +88 -0
  39. package/dist/runtimes/existdb/util.xq +892 -0
  40. package/dist/runtimes/existdb/validation.xq +83 -0
  41. package/dist/runtimes/existdb/xmldb.xq +482 -0
  42. package/dist/runtimes/existdb/xmldiff.xq +22 -0
  43. package/dist/runtimes/fonto/fonto.xq +164 -0
  44. package/dist/runtimes.d.ts +14 -1
  45. package/dist/runtimes.js +73 -0
  46. package/dist/server.js +51 -70
  47. package/dist/types.d.ts +2 -0
  48. package/dist/unused-diagnostics.d.ts +1 -1
  49. package/dist/unused-diagnostics.js +57 -1
  50. package/dist/variable-diagnostics.d.ts +19 -0
  51. package/dist/variable-diagnostics.js +164 -0
  52. package/logo.svg +6 -0
  53. package/package.json +4 -4
  54. package/src/analyzer.ts +8 -6
  55. package/src/completion-core.ts +71 -6
  56. package/src/diagnostics.ts +43 -0
  57. package/src/qt4-worker.ts +5 -9
  58. package/src/runtimes/existdb/exist.xq +29 -0
  59. package/src/runtimes/existdb/util.xq +32 -0
  60. package/src/runtimes.ts +76 -1
  61. package/src/server.ts +53 -73
  62. package/src/types.ts +2 -0
  63. package/src/unused-diagnostics.ts +59 -2
  64. 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 | Runtime |
63
- | --------- | ------------------------------------------------------------------- |
64
- | `"fonto"` | [Fonto XML editor](https://www.fontoxml.com/) — `fonto:*` functions |
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": "fonto"}
71
+ map {"glob": "src/**/*.xq", "lib": "existdb"}
68
72
  ```
69
73
 
70
- Then import the namespace in your XQuery files as usual — the server resolves completions, hover, and go-to-definition against the bundled definitions:
74
+ Multiple libs use an XPath sequence:
71
75
 
72
76
  ```xquery
73
- import module namespace fonto="http://www.fontoxml.com/functions";
77
+ map {"glob": "src/**/*.xq", "lib": ("fonto", "other")}
74
78
  ```
75
79
 
76
- Multiple libs use an XPath sequence:
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
- map {"glob": "src/**/*.xq", "lib": ("fonto", "other")}
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 `.xqy` files as XQuery and starts `xq-lsp` automatically when you open one.
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.
@@ -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 { checkTypes } from "../src/typechecker.ts";
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 checkTypes(ast, code, analysis, new Map())) {
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 checkUnused(ast, analysis)) {
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+["']([^"']*)["'])?/g;
342
- const RE_NS_DECL = /declare\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/g;
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 imp = { prefix: m[1], namespaceUri: m[2] };
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
- namespaceDecls.push({ prefix: m[1], namespaceUri: m[2] });
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[];
@@ -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
- for (const v of currentAnalysis.localBindings) {
65
- if (v.offset < ctx.cursorOffset) {
66
- const e = buildVariableEntry(v, filter);
67
- if (e)
68
- items.push(e);
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
+ }
@@ -1,27 +1,21 @@
1
1
  import { workerData, parentPort } from "node:worker_threads";
2
2
  import { analyzeWithAst } from "./analyzer.js";
3
- import { checkTypes } from "./typechecker.js";
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 runDiagnostics(query, envNamespaces) {
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 checkTypes(ast, query, analysis, BUILTINS))
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 = runDiagnostics(tc.query, tc.envNamespaces);
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;