xq-lsp 1.8.2 → 1.8.3-next.428

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/README.md CHANGED
@@ -12,7 +12,7 @@ Language Server Protocol implementation for XQuery, providing autocompletion, ho
12
12
  - **Signature help** — active parameter hints inside function calls
13
13
  - **Go to definition** — jump to function or variable declarations, including across imported files
14
14
  - **Document symbols** — file outline of all declared functions and variables
15
- - **Diagnostics** — syntax errors, undeclared namespace prefixes, type mismatches, and unused symbols
15
+ - **Diagnostics** — syntax errors, undeclared namespace prefixes, type mismatches, and unused symbols — pushed on change, and also available on request via LSP 3.17 pull diagnostics (`textDocument/diagnostic`), including for files that are not open
16
16
 
17
17
  ## Configuration
18
18
 
@@ -148,14 +148,6 @@ map {
148
148
  - **Context-depending completion** — which prevent syntactical errors
149
149
  - **Treesitter parser** — should improve performance and scalability for huge files
150
150
  - **XQuery 4** — the parser already supports this. Just make it work
151
- - **Arity checks** — report when one calls `fn:document('too', 'many', 'arguments')`
152
- - **Register or discover known prefix/namespace combinations** — if you have the `tei` namespace
153
- declared in another file, you might mean that when you type `tei:TEI` in another file
154
- - **Context items** — doing `declare function xx () { bla };` is an error: there is no context
155
- item. Check if there is a context item available when you use the context item expression (`.`) or
156
- a step expression.
157
- - **Unused functions and variables** — no one likes those. Except when prefixed with `_`, then they
158
- make sense
159
151
  - More of the (static) errors in the spec at [The spec at section F Error
160
152
  Conditions](https://www.w3.org/TR/xquery-31/#id-errors), whichever are easy to implement
161
153
 
@@ -193,6 +185,12 @@ Open any `.xq` file and eglot starts the server automatically. Run `M-x eglot` t
193
185
  | Document symbols | `M-x imenu` |
194
186
  | Diagnostics | `M-x flymake-show-buffer-diagnostics` |
195
187
 
188
+ **Inlay hints** (inferred types after untyped `let`/`for`/`declare variable` names, and
189
+ parameter names before call arguments) are shown by `eglot-inlay-hints-mode`, which eglot
190
+ turns on automatically since Emacs 29.1. If they don't appear, check that `:inlayHintProvider`
191
+ isn't listed in `eglot-ignored-server-capabilities`, or toggle them with
192
+ `M-x eglot-inlay-hints-mode`.
193
+
196
194
  ### With lsp-mode
197
195
 
198
196
  ```elisp
@@ -209,6 +207,15 @@ Open any `.xq` file and eglot starts the server automatically. Run `M-x eglot` t
209
207
  (add-hook 'xquery-mode-hook #'lsp)
210
208
  ```
211
209
 
210
+ lsp-mode does not show inlay hints unless asked to:
211
+
212
+ ```elisp
213
+ (setq lsp-inlay-hint-enable t)
214
+ ```
215
+
216
+ The reference counts above each function and module variable are code lenses; lsp-mode shows
217
+ them by default (`lsp-lens-enable`). Eglot does not support code lenses.
218
+
212
219
  ## VS Code
213
220
 
214
221
  A minimal VS Code extension lives in [`editors/vscode/`](./editors/vscode/).
@@ -193,7 +193,7 @@ declare function fn:format-number($value as xs:numeric?, $picture as xs:string)
193
193
  declare function fn:format-number(
194
194
  $value as xs:numeric?,
195
195
  $picture as xs:string,
196
- $decimal-format-name as xs:string
196
+ $decimal-format-name as xs:string?
197
197
  ) as xs:string external;
198
198
 
199
199
  (: ── String functions ───────────────────────────────────────────────────────── :)
@@ -7,6 +7,7 @@ export declare const XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance";
7
7
  export declare const XMLNS_MATH = "http://www.w3.org/2005/xpath-functions/math";
8
8
  export declare const XMLNS_MAP = "http://www.w3.org/2005/xpath-functions/map";
9
9
  export declare const XMLNS_ARRAY = "http://www.w3.org/2005/xpath-functions/array";
10
+ export declare const BUILTIN_PREFIXES: Record<string, string>;
10
11
  /**
11
12
  * Resolve a namespace prefix to its URI given the namespace context of a file.
12
13
  * Unprefixed calls (prefix="") resolve to the file's default function namespace.
package/dist/analyzer.js CHANGED
@@ -12,7 +12,7 @@ export const XMLNS_ARRAY = "http://www.w3.org/2005/xpath-functions/array";
12
12
  // context, independent of any runtime/library configuration. math/map/array
13
13
  // aren't spec-mandated but are universally available in practice (they back
14
14
  // the 3.1 map/array/math core function libraries), so they're predeclared here too.
15
- const BUILTIN_PREFIXES = {
15
+ export const BUILTIN_PREFIXES = {
16
16
  fn: XMLNS_FN,
17
17
  local: XMLNS_LOCAL,
18
18
  xs: XMLNS_XS,
@@ -160,7 +160,10 @@ function findPrecedingDoc(comments, text, offset) {
160
160
  }
161
161
  // ── Namespace helpers ────────────────────────────────────────────────────────
162
162
  function extractModuleNamespace(text) {
163
- const m = text.match(/module\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/);
163
+ // The negative lookbehind excludes `import module namespace ...` — a module
164
+ // import, not this file's own `module namespace` declaration — which would
165
+ // otherwise match the same pattern and be mistaken for it.
166
+ const m = text.match(/(?<!import\s+)\bmodule\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/);
164
167
  return m ? { prefix: m[1], uri: m[2] } : undefined;
165
168
  }
166
169
  function extractDefaultFunctionNamespace(text) {
@@ -180,6 +183,23 @@ function buildPrefixMap(moduleNs, imports, namespaceDecls) {
180
183
  function resolveNamespaceUri(prefix, prefixMap) {
181
184
  return prefixMap.get(prefix) ?? `urn:xq-lsp:undeclared:${prefix}`;
182
185
  }
186
+ const PREDEFINED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
187
+ /**
188
+ * A BracedURILiteral's content may contain PredefinedEntityRef (&amp; &lt; &gt;
189
+ * &quot; &apos;) and CharRef (&#DDDD; / &#xHHHH;) escapes, same as string content —
190
+ * e.g. `Q{http:&#x2F;&#x2F;example.com}` names the URI "http://example.com". The
191
+ * parser's terminal value is the raw, un-decoded source slice, so this decoding
192
+ * has to happen here rather than being assumed already done.
193
+ */
194
+ function decodeCharRefs(text) {
195
+ return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (match, ref) => {
196
+ if (ref[0] === "#") {
197
+ const codePoint = ref[1] === "x" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
198
+ return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint);
199
+ }
200
+ return PREDEFINED_ENTITIES[ref] ?? match;
201
+ });
202
+ }
183
203
  /**
184
204
  * Per the BracedURILiteral grammar, whitespace inside `Q{...}` is insignificant:
185
205
  * leading/trailing whitespace is trimmed and internal runs are collapsed to a
@@ -187,7 +207,7 @@ function resolveNamespaceUri(prefix, prefixMap) {
187
207
  * This does not decode percent-escapes — `Q{foo%20bar}` stays distinct from `Q{foo bar}`.
188
208
  */
189
209
  function normalizeBracedUri(uri) {
190
- return uri.trim().replace(/\s+/g, " ");
210
+ return decodeCharRefs(uri).trim().replace(/\s+/g, " ");
191
211
  }
192
212
  /**
193
213
  * Split a raw EQName terminal into its components, handling both the
@@ -218,6 +238,19 @@ export function sequenceTypeText(text, node) {
218
238
  return undefined;
219
239
  return text.slice(node.start, node.end ?? undefined).trim() || undefined;
220
240
  }
241
+ // A %public/%private annotation on an AnnotatedDecl controls whether the
242
+ // declaration is visible to modules that import it (XQuery 3.0+). Returns
243
+ // undefined when neither annotation is present; callers treat that as public,
244
+ // same as FunctionSymbol.visibility's own default.
245
+ function extractVisibility(annotated) {
246
+ for (const ann of directChildrenOf(annotated, "Annotation")) {
247
+ const eqname = directChildOf(ann, "EQName");
248
+ const raw = eqname ? firstTerminalValue(eqname) : null;
249
+ if (raw === "private" || raw === "public")
250
+ return raw;
251
+ }
252
+ return undefined;
253
+ }
221
254
  function extractFunctions(ast, sourceUri, comments, text, prefixMap, defaultFnNs) {
222
255
  const results = [];
223
256
  // Use AnnotatedDecl (starts at 'declare') so the comment lookup clears the 'declare' keyword
@@ -261,6 +294,7 @@ function extractFunctions(ast, sourceUri, comments, text, prefixMap, defaultFnNs
261
294
  doc,
262
295
  sourceUri,
263
296
  sourceOffset: annotated.start,
297
+ visibility: extractVisibility(annotated),
264
298
  });
265
299
  }
266
300
  return results;
@@ -278,12 +312,15 @@ function extractModuleVariables(ast, sourceUri, prefixMap, comments, text) {
278
312
  if (!name)
279
313
  continue;
280
314
  const doc = findPrecedingDoc(comments, text, annotated.start);
315
+ const typeDecl = directChildOf(varDecl, "TypeDeclaration");
316
+ const seqType = typeDecl ? directChildOf(typeDecl, "SequenceType") : undefined;
281
317
  results.push({
282
318
  qname: makeVarQName(name, prefixMap),
283
319
  offset: varDecl.start,
284
320
  isModuleLevel: true,
285
321
  sourceUri,
286
322
  doc: doc?.description || undefined,
323
+ type: seqType ? sequenceTypeText(text, seqType) : undefined,
287
324
  });
288
325
  }
289
326
  return results;
@@ -417,12 +454,15 @@ function analyzeRegex(text, sourceUri) {
417
454
  RE_VAR_DECL.lastIndex = 0;
418
455
  while ((m = RE_VAR_DECL.exec(text)) !== null) {
419
456
  const doc = findPrecedingDocInText(text, m.index);
457
+ // `declare variable $x as T := …` / `… as T external` — T runs up to `:=` or `external`.
458
+ const typeMatch = /^\s+as\s+([^;]{1,200}?)\s*(?::=|\bexternal\b)/.exec(text.slice(m.index + m[0].length));
420
459
  moduleVariables.push({
421
460
  qname: makeVarQName(m[1], prefixMap),
422
461
  offset: m.index,
423
462
  isModuleLevel: true,
424
463
  sourceUri,
425
464
  doc: doc?.description || undefined,
465
+ type: typeMatch?.[1],
426
466
  });
427
467
  }
428
468
  RE_LET.lastIndex = 0;
@@ -28,6 +28,33 @@ export interface FunctionDeclShape {
28
28
  }
29
29
  /** Inner shape of a FunctionDecl node: name, resolved params, and function body. */
30
30
  export declare function asFunctionDecl(node: Node, analysis: FileAnalysis): FunctionDeclShape | null;
31
+ /** The version string of `xquery version "x"` (quotes stripped) and its literal node; null when only an encoding is given. */
32
+ export declare function asVersionDecl(node: Node): {
33
+ version: string;
34
+ versionNode: Node;
35
+ } | null;
36
+ /** Resolved QName and name node of a `declare option` OptionDecl. */
37
+ export declare function asOptionDecl(node: Node, analysis: FileAnalysis): {
38
+ qname: QName;
39
+ nameNode: Node;
40
+ } | null;
41
+ /** Prefix bound by a `declare namespace p = "uri"` NamespaceDecl. */
42
+ export declare function asNamespaceDecl(node: Node): {
43
+ prefix: string;
44
+ } | null;
45
+ /** Which default namespace a `declare default element|function namespace` DefaultNamespaceDecl sets. */
46
+ export declare function asDefaultNamespaceDecl(node: Node): {
47
+ kind: "element" | "function";
48
+ } | null;
49
+ export interface DecimalFormatDeclShape {
50
+ /** Expanded name of the format; "" for the default decimal format. */
51
+ key: string;
52
+ properties: Array<{
53
+ nameNode: Node;
54
+ name: string;
55
+ }>;
56
+ }
57
+ export declare function asDecimalFormatDecl(node: Node, analysis: FileAnalysis): DecimalFormatDeclShape | null;
31
58
  export interface InlineFunctionExprShape {
32
59
  params: Array<{
33
60
  nameNode: Node;
package/dist/ast-nodes.js CHANGED
@@ -113,6 +113,59 @@ export function asFunctionDecl(node, analysis) {
113
113
  const params = extractParamsFromParamList(directChildOf(node, "ParamList"), analysis);
114
114
  return { nameNode, qname: { prefix, localName, namespaceUri }, params, body: directChildOf(node, "FunctionBody") ?? null };
115
115
  }
116
+ // ── Prolog declarations ───────────────────────────────────────────────────────
117
+ /** The version string of `xquery version "x"` (quotes stripped) and its literal node; null when only an encoding is given. */
118
+ export function asVersionDecl(node) {
119
+ if (node.type !== "VersionDecl" || !directChildOf(node, "'version'"))
120
+ return null;
121
+ const versionNode = directChildOf(node, "StringLiteral");
122
+ const raw = versionNode ? firstTerminalValue(versionNode) : null;
123
+ if (!versionNode || !raw)
124
+ return null;
125
+ // String literals may contain character references, e.g. '1&#x2e;0'
126
+ const version = raw
127
+ .slice(1, -1)
128
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
129
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)));
130
+ return { version, versionNode };
131
+ }
132
+ /** Resolved QName and name node of a `declare option` OptionDecl. */
133
+ export function asOptionDecl(node, analysis) {
134
+ if (node.type !== "OptionDecl")
135
+ return null;
136
+ const nameNode = directChildOf(node, "EQName");
137
+ const qname = nameNode ? qnameFromRawTerminal(nameNode, analysis) : null;
138
+ return nameNode && qname ? { qname, nameNode } : null;
139
+ }
140
+ /** Prefix bound by a `declare namespace p = "uri"` NamespaceDecl. */
141
+ export function asNamespaceDecl(node) {
142
+ if (node.type !== "NamespaceDecl")
143
+ return null;
144
+ const prefixNode = directChildOf(node, "NCName");
145
+ const prefix = prefixNode ? firstTerminalValue(prefixNode) : null;
146
+ return prefix === null ? null : { prefix };
147
+ }
148
+ /** Which default namespace a `declare default element|function namespace` DefaultNamespaceDecl sets. */
149
+ export function asDefaultNamespaceDecl(node) {
150
+ if (node.type !== "DefaultNamespaceDecl")
151
+ return null;
152
+ if (directChildOf(node, "'element'"))
153
+ return { kind: "element" };
154
+ if (directChildOf(node, "'function'"))
155
+ return { kind: "function" };
156
+ return null;
157
+ }
158
+ export function asDecimalFormatDecl(node, analysis) {
159
+ if (node.type !== "DecimalFormatDecl")
160
+ return null;
161
+ const eqname = directChildOf(node, "EQName");
162
+ const qname = eqname ? qnameFromRawTerminal(eqname, analysis) : null;
163
+ const properties = directChildrenOf(node, "DFPropertyName").flatMap((nameNode) => {
164
+ const name = firstTerminalValue(nameNode);
165
+ return name ? [{ nameNode, name }] : [];
166
+ });
167
+ return { key: qname ? `${qname.namespaceUri}#${qname.localName}` : "", properties };
168
+ }
116
169
  export function asInlineFunctionExpr(node, analysis) {
117
170
  if (node.type !== "InlineFunctionExpr")
118
171
  return null;
@@ -4,6 +4,7 @@ import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.js";
4
4
  import { checkContextItemUsage } from "./context-item-diagnostics.js";
5
5
  import { checkUndeclaredVariables } from "./variable-diagnostics.js";
6
6
  import { checkDuplicateFunctions } from "./duplicate-function-diagnostics.js";
7
+ import { checkDuplicatePrologDecls } from "./prolog-diagnostics.js";
7
8
  import { checkModuleImportTargets } from "./module-import-diagnostics.js";
8
9
  import { checkUnused } from "./unused-diagnostics.js";
9
10
  import { checkBracedUriWhitespace } from "./braced-uri-diagnostics.js";
@@ -21,6 +22,7 @@ export function runDiagnostics(ast, text, analysis, imports) {
21
22
  ...checkContextItemUsage(ast),
22
23
  ...checkUndeclaredVariables(ast, analysis, imports),
23
24
  ...checkDuplicateFunctions(ast, analysis, imports),
25
+ ...checkDuplicatePrologDecls(ast, analysis),
24
26
  ...checkModuleImportTargets(analysis, imports),
25
27
  ];
26
28
  }
@@ -4,9 +4,12 @@ import type { TypeDiagnostic, FileAnalysis } from "./types.ts";
4
4
  * Walk all FunctionCall, NamedFunctionRef, and ArrowExpr nodes in the AST
5
5
  * and report XPST0017 for any call where the argument count does not match
6
6
  * any known overload, or where the function name is not declared in a
7
- * namespace we have analysis for.
7
+ * namespace this file can call into.
8
8
  *
9
- * If the function's namespace is entirely unknown (no analysis loaded for it),
10
- * the call is silently skipped — it could be a runtime-only library.
9
+ * A namespace is only skipped (left unchecked) when it's the target of an
10
+ * import we couldn't resolve to any analysis, or when the prefix itself is
11
+ * undeclared (that's findUndeclaredPrefixUsages's concern) — see
12
+ * accessibleFunctions and checkArity. Every other namespace is closed-world:
13
+ * not finding a matching declaration there means one doesn't exist.
11
14
  */
12
15
  export declare function checkFunctionCalls(ast: Node, analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): TypeDiagnostic[];
@@ -1,23 +1,81 @@
1
1
  import { formatQName } from "./types.js";
2
- import { findAll, isTerminal, directChildrenOf, firstTerminalValue, parseEQName, resolvePrefix } from "./analyzer.js";
2
+ import { findAll, isTerminal, directChildrenOf, firstTerminalValue, parseEQName, resolvePrefix, XMLNS_LOCAL, XMLNS_FN, XMLNS_XS, XMLNS_MATH, XMLNS_MAP, XMLNS_ARRAY, } from "./analyzer.js";
3
3
  import { asFunctionCall, asNamedFunctionRef } from "./ast-nodes.js";
4
- function allFunctionsFlat(analysis, importedAnalyses) {
4
+ // Namespaces that are always in scope without an explicit import: the
5
+ // spec-mandated predeclared ones, plus whatever this file declares of its own
6
+ // (local: functions, its own module namespace, and its default function
7
+ // namespace) — we have complete, closed-world knowledge of all of these.
8
+ function alwaysAccessibleNamespaceUris(analysis) {
9
+ const uris = new Set([XMLNS_LOCAL, XMLNS_FN, XMLNS_XS, XMLNS_MATH, XMLNS_MAP, XMLNS_ARRAY, analysis.defaultFunctionNamespace]);
10
+ if (analysis.moduleNamespaceUri)
11
+ uris.add(analysis.moduleNamespaceUri);
12
+ return uris;
13
+ }
14
+ /**
15
+ * Gather every function symbol this file can legally call, plus the set of
16
+ * imported namespace URIs we couldn't resolve to any analysis (unresolvable
17
+ * "at" path, or an unresolvable bare import — genuinely unknown, so calls
18
+ * into them are left unchecked to avoid false positives).
19
+ *
20
+ * A namespace not covered by any of the above (not declared, not imported,
21
+ * not predeclared) has zero accessible functions by construction: nothing in
22
+ * this module's static scope could have brought a function from it into
23
+ * view, regardless of what happens to be sitting in `importedAnalyses` (the
24
+ * QT4 harness, for instance, loads catalog modules that were never actually
25
+ * imported by the query under test, specifically to verify this isn't enough
26
+ * to make their functions callable).
27
+ */
28
+ function accessibleFunctions(analysis, importedAnalyses) {
5
29
  const fns = [...analysis.functions];
6
- for (const a of importedAnalyses.values())
7
- fns.push(...a.functions);
8
- return fns;
30
+ const alwaysOpen = alwaysAccessibleNamespaceUris(analysis);
31
+ const importedNamespaceUris = new Set(analysis.imports.map((i) => i.namespaceUri));
32
+ // Runtime-predeclared namespaces (e.g. eXist-db's util:, xmldb:) are merged into
33
+ // namespaceDecls with offset -1 — see withPredeclaredNs — marking them as
34
+ // implicitly available without a written import. The QT4 harness uses the same
35
+ // offset -1 convention for <environment><namespace> bindings, which name a
36
+ // namespace without providing (or requiring) any declaration of what's in it.
37
+ const predeclaredUris = new Set(analysis.namespaceDecls.filter((nd) => nd.offset === -1).map((nd) => nd.namespaceUri));
38
+ // Namespaces we might have real knowledge of — but only once we find a matching
39
+ // entry in importedAnalyses below. Until then, stay silent about them rather
40
+ // than treating "no analysis available" as "no such function". Namespaces
41
+ // already in alwaysOpen are excluded: they're already guaranteed accessible
42
+ // (e.g. builtins arrive under a "builtin:" map key, not one matching their own
43
+ // namespace URI), so redundantly re-declaring one — as the QT4 harness's
44
+ // <environment><namespace prefix="map" uri=".../map"/> does — must not make it
45
+ // look unresolved.
46
+ const candidateUris = new Set([...importedNamespaceUris, ...predeclaredUris].filter((u) => !alwaysOpen.has(u)));
47
+ const resolvedUris = new Set();
48
+ for (const [key, a] of importedAnalyses) {
49
+ if (key.startsWith("builtin:") || alwaysOpen.has(key)) {
50
+ fns.push(...a.functions);
51
+ continue;
52
+ }
53
+ if (candidateUris.has(key)) {
54
+ // A %private function/variable is only visible within its own declaring
55
+ // module — filter it out for anything reached through an import.
56
+ fns.push(...a.functions.filter((f) => f.visibility !== "private"));
57
+ resolvedUris.add(key);
58
+ }
59
+ }
60
+ const unresolvedImportUris = new Set([...candidateUris].filter((u) => !resolvedUris.has(u)));
61
+ return { fns, unresolvedImportUris };
9
62
  }
10
63
  function arityDescription(overloads) {
11
64
  const arities = [...new Set(overloads.map((f) => f.arity))].sort((a, b) => a - b);
12
65
  return overloads.some((f) => f.variadic) ? `${arities[0]} or more` : arities.length === 1 ? `${arities[0]}` : arities.join(" or ");
13
66
  }
14
- function checkArity(qname, arity, nodeStart, allFns, knownNamespaceUris, errors) {
67
+ function checkArity(qname, arity, nodeStart, allFns, unresolvedImportUris, errors) {
15
68
  const { namespaceUri, localName } = qname;
69
+ // A prefix that resolves to nothing at all is a separate diagnostic's
70
+ // concern (findUndeclaredPrefixUsages) — don't pile on here.
71
+ if (namespaceUri.startsWith("urn:xq-lsp:undeclared:"))
72
+ return;
73
+ // An import we couldn't resolve to any analysis: genuinely unknown, stay silent.
74
+ if (unresolvedImportUris.has(namespaceUri))
75
+ return;
16
76
  const overloads = allFns.filter((f) => f.qname.namespaceUri === namespaceUri && f.qname.localName === localName);
17
77
  const name = formatQName(qname);
18
78
  if (overloads.length === 0) {
19
- if (!knownNamespaceUris.has(namespaceUri))
20
- return;
21
79
  errors.push({ message: `${name} is not declared`, code: "XPST0017", offset: nodeStart, length: name.length });
22
80
  return;
23
81
  }
@@ -35,29 +93,30 @@ function checkArity(qname, arity, nodeStart, allFns, knownNamespaceUris, errors)
35
93
  * Walk all FunctionCall, NamedFunctionRef, and ArrowExpr nodes in the AST
36
94
  * and report XPST0017 for any call where the argument count does not match
37
95
  * any known overload, or where the function name is not declared in a
38
- * namespace we have analysis for.
96
+ * namespace this file can call into.
39
97
  *
40
- * If the function's namespace is entirely unknown (no analysis loaded for it),
41
- * the call is silently skipped — it could be a runtime-only library.
98
+ * A namespace is only skipped (left unchecked) when it's the target of an
99
+ * import we couldn't resolve to any analysis, or when the prefix itself is
100
+ * undeclared (that's findUndeclaredPrefixUsages's concern) — see
101
+ * accessibleFunctions and checkArity. Every other namespace is closed-world:
102
+ * not finding a matching declaration there means one doesn't exist.
42
103
  */
43
104
  export function checkFunctionCalls(ast, analysis, importedAnalyses) {
44
105
  const errors = [];
45
- const allFns = allFunctionsFlat(analysis, importedAnalyses);
46
- // Namespaces we have at least one function declaration for.
47
- const knownNamespaceUris = new Set(allFns.map((f) => f.qname.namespaceUri));
106
+ const { fns: allFns, unresolvedImportUris } = accessibleFunctions(analysis, importedAnalyses);
48
107
  // ── FunctionCall ─────────────────────────────────────────────────────────────
49
108
  for (const callNode of findAll(ast, "FunctionCall")) {
50
109
  const call = asFunctionCall(callNode, analysis);
51
110
  if (!call)
52
111
  continue;
53
- checkArity(call.qname, call.args.length, callNode.start, allFns, knownNamespaceUris, errors);
112
+ checkArity(call.qname, call.args.length, callNode.start, allFns, unresolvedImportUris, errors);
54
113
  }
55
114
  // ── NamedFunctionRef ─────────────────────────────────────────────────────────
56
115
  for (const refNode of findAll(ast, "NamedFunctionRef")) {
57
116
  const ref = asNamedFunctionRef(refNode, analysis);
58
117
  if (!ref)
59
118
  continue;
60
- checkArity(ref.qname, ref.arity, refNode.start, allFns, knownNamespaceUris, errors);
119
+ checkArity(ref.qname, ref.arity, refNode.start, allFns, unresolvedImportUris, errors);
61
120
  }
62
121
  // ── ArrowExpr ────────────────────────────────────────────────────────────────
63
122
  // "$x => f($y)" is syntax sugar for "f($x, $y)"; the arrow provides one extra
@@ -80,7 +139,7 @@ export function checkFunctionCalls(ast, analysis, importedAnalyses) {
80
139
  const { prefix, localName, uri } = parseEQName(rawName);
81
140
  const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
82
141
  const explicitArgs = directChildrenOf(argList, "Argument").length;
83
- checkArity({ prefix, localName, namespaceUri }, 1 + explicitArgs, arrowNode.start, allFns, knownNamespaceUris, errors);
142
+ checkArity({ prefix, localName, namespaceUri }, 1 + explicitArgs, arrowNode.start, allFns, unresolvedImportUris, errors);
84
143
  }
85
144
  }
86
145
  return errors;
@@ -1,35 +1,23 @@
1
1
  import { InlayHintKind } from "vscode-languageserver/node.js";
2
- import { qnameKey } from "./types.js";
3
- import { findAll, isTerminal, directChildOf } from "./analyzer.js";
4
- import { asFunctionCall, asBinding, asTypedBinding } from "./ast-nodes.js";
5
- import { parseType, inferExprType, formatType } from "./typechecker.js";
2
+ import { findAll } from "./analyzer.js";
3
+ import { asFunctionCall } from "./ast-nodes.js";
4
+ import { externalVariableTypes, formatType, resolveFunction, walkModuleScopes, withInferredReturnTypes, } from "./typechecker.js";
6
5
  function allFunctionsFlat(analysis, importedAnalyses) {
7
6
  const fns = [...analysis.functions];
8
7
  for (const a of importedAnalyses.values())
9
8
  fns.push(...a.functions);
10
9
  return fns;
11
10
  }
12
- function resolveCallee(call, argCount, allFns) {
13
- const overloads = allFns.filter((f) => f.qname.namespaceUri === call.qname.namespaceUri && f.qname.localName === call.qname.localName);
14
- return overloads.find((f) => (f.variadic ? argCount >= f.arity : f.arity === argCount));
15
- }
16
11
  function inRange(offset, range) {
17
12
  return offset >= range.start && offset <= range.end;
18
13
  }
19
- // Narrows a sequence's type to its item type (occurrence ""), for `for` bindings
20
- // where the bound variable holds one item per iteration, not the whole sequence.
21
- function itemTypeOf(t) {
22
- if (t.kind === "unknown" || t.kind === "empty")
23
- return t;
24
- return t.occurrence === "" ? t : { ...t, occurrence: "" };
25
- }
26
14
  // ── Parameter name hints ─────────────────────────────────────────────────────
27
15
  function collectParameterHints(ast, analysis, allFns, range, hints, doc) {
28
16
  for (const callNode of findAll(ast, "FunctionCall")) {
29
17
  const call = asFunctionCall(callNode, analysis);
30
18
  if (!call || call.args.length === 0)
31
19
  continue;
32
- const fn = resolveCallee(call, call.args.length, allFns);
20
+ const fn = resolveFunction(call.qname, call.args.length, allFns);
33
21
  if (!fn)
34
22
  continue;
35
23
  for (let i = 0; i < call.args.length && i < fn.params.length; i++) {
@@ -45,77 +33,35 @@ function collectParameterHints(ast, analysis, allFns, range, hints, doc) {
45
33
  }
46
34
  }
47
35
  }
48
- // ── Inferred type hints on untyped let/for bindings ──────────────────────────
49
- function collectParams(paramList, text, analysis, scope) {
50
- if (!paramList)
51
- return;
52
- for (const param of findAll(paramList, "Param")) {
53
- const binding = asTypedBinding(param, text, analysis);
54
- if (binding?.typeStr)
55
- scope.set(qnameKey(binding.qname), parseType(binding.typeStr));
56
- }
57
- }
58
- function buildModuleVarTypes(ast, text, analysis) {
59
- const types = new Map();
60
- for (const node of findAll(ast, "VarDecl")) {
61
- const binding = asTypedBinding(node, text, analysis);
62
- if (binding?.typeStr)
63
- types.set(qnameKey(binding.qname), parseType(binding.typeStr));
64
- }
65
- return types;
66
- }
67
- // Walks the tree tracking in-scope variable types, mirroring typechecker.ts's scope walk,
68
- // but also records the inferred type of untyped let/for bindings so later bindings see it.
69
- function walkForTypeHints(node, scopeTypes, moduleTypes, text, analysis, allFns, range, hints, doc) {
70
- if (isTerminal(node))
71
- return;
72
- const nt = node;
73
- if (node.type === "InlineFunctionExpr") {
74
- const innerScope = new Map(moduleTypes);
75
- collectParams(directChildOf(node, "ParamList"), text, analysis, innerScope);
76
- const body = directChildOf(node, "FunctionBody");
77
- if (body)
78
- walkForTypeHints(body, innerScope, moduleTypes, text, analysis, allFns, range, hints, doc);
79
- return;
80
- }
81
- if (node.type === "LetBinding" || node.type === "ForBinding") {
82
- const typed = asTypedBinding(node, text, analysis);
83
- const binding = asBinding(node, analysis);
84
- if (typed && binding) {
85
- if (typed.typeStr) {
86
- scopeTypes.set(qnameKey(typed.qname), parseType(typed.typeStr));
87
- }
88
- else if (binding.initExpr) {
89
- const inferredSeq = inferExprType(binding.initExpr, scopeTypes, analysis, allFns);
90
- // A `for` binding iterates the sequence one item at a time, so $x's type
91
- // is the *item* type (occurrence narrowed to singular) — not the type of
92
- // the whole iterated sequence, which is what inferExprType returns.
93
- const inferred = node.type === "ForBinding" ? itemTypeOf(inferredSeq) : inferredSeq;
94
- scopeTypes.set(qnameKey(typed.qname), inferred);
95
- const nameEnd = binding.nameNode.end ?? binding.nameNode.start;
96
- if (inferred.kind !== "unknown" && inRange(nameEnd, range)) {
97
- hints.push({
98
- position: doc.positionAt(nameEnd),
99
- label: `: ${formatType(inferred)}`,
100
- kind: InlayHintKind.Type,
101
- paddingLeft: true,
102
- });
103
- }
104
- }
105
- }
106
- }
107
- for (const child of nt.children) {
108
- walkForTypeHints(child, scopeTypes, moduleTypes, text, analysis, allFns, range, hints, doc);
109
- }
36
+ // ── Inferred type hints on untyped bindings ──────────────────────────────────
37
+ // Shows the inferred type after the name of every binding without a declared type:
38
+ // let/for/group-by variables and module-level `declare variable`s. The scope walk is the
39
+ // same one the type checker uses, so hints reflect exactly what diagnostics see.
40
+ function collectTypeHints(ast, analysis, allFns, range, hints, doc, outerScope) {
41
+ const seen = new Set();
42
+ walkModuleScopes(ast, analysis, allFns, {
43
+ onBinding: (nameNode, type) => {
44
+ const nameEnd = nameNode.end ?? nameNode.start;
45
+ if (type.kind === "unknown" || !inRange(nameEnd, range) || seen.has(nameEnd))
46
+ return;
47
+ seen.add(nameEnd);
48
+ hints.push({
49
+ position: doc.positionAt(nameEnd),
50
+ label: `: ${formatType(type)}`,
51
+ kind: InlayHintKind.Type,
52
+ paddingLeft: true,
53
+ });
54
+ },
55
+ }, outerScope);
110
56
  }
111
57
  // ── Entry point ───────────────────────────────────────────────────────────────
112
58
  export function getInlayHints(doc, ast, analysis, imported, range) {
113
- const text = doc.getText();
114
59
  const offsetRange = { start: doc.offsetAt(range.start), end: doc.offsetAt(range.end) };
115
- const allFns = allFunctionsFlat(analysis, imported);
60
+ const declaredFns = allFunctionsFlat(analysis, imported);
61
+ const outerScope = externalVariableTypes(analysis, imported.values(), declaredFns);
62
+ const allFns = withInferredReturnTypes(ast, analysis, declaredFns, outerScope);
116
63
  const hints = [];
117
64
  collectParameterHints(ast, analysis, allFns, offsetRange, hints, doc);
118
- const moduleTypes = buildModuleVarTypes(ast, text, analysis);
119
- walkForTypeHints(ast, new Map(moduleTypes), moduleTypes, text, analysis, allFns, offsetRange, hints, doc);
65
+ collectTypeHints(ast, analysis, allFns, offsetRange, hints, doc, outerScope);
120
66
  return hints;
121
67
  }
@@ -0,0 +1,11 @@
1
+ import type { FileAnalysis } from "./types.ts";
2
+ /**
3
+ * The declarations that *other* files contribute to a module namespace. XQuery lets one module be
4
+ * implemented by several module files sharing a target namespace, and some runtimes (e.g. Fonto)
5
+ * merge them, so a file can call a function declared in a sibling without importing it.
6
+ *
7
+ * `merged` is the glob index entry for the namespace, which may include the current file's own
8
+ * on-disk declarations. Those are dropped so they aren't reported as duplicates of the (possibly
9
+ * edited) in-memory versions, nor resolved to stale positions. Returns null when nothing is left.
10
+ */
11
+ export declare function siblingModuleAnalysis(merged: FileAnalysis, currentUri: string): FileAnalysis | null;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The declarations that *other* files contribute to a module namespace. XQuery lets one module be
3
+ * implemented by several module files sharing a target namespace, and some runtimes (e.g. Fonto)
4
+ * merge them, so a file can call a function declared in a sibling without importing it.
5
+ *
6
+ * `merged` is the glob index entry for the namespace, which may include the current file's own
7
+ * on-disk declarations. Those are dropped so they aren't reported as duplicates of the (possibly
8
+ * edited) in-memory versions, nor resolved to stale positions. Returns null when nothing is left.
9
+ */
10
+ export function siblingModuleAnalysis(merged, currentUri) {
11
+ const functions = merged.functions.filter((f) => f.sourceUri !== currentUri);
12
+ const moduleVariables = merged.moduleVariables.filter((v) => v.sourceUri !== currentUri);
13
+ if (functions.length === 0 && moduleVariables.length === 0)
14
+ return null;
15
+ return { ...merged, functions, moduleVariables };
16
+ }
@@ -125,6 +125,7 @@ export function getRuntimePredeclaredVariables(runtimes, sourceUri) {
125
125
  isModuleLevel: true,
126
126
  sourceUri,
127
127
  doc,
128
+ type: "xs:string", // all three are file system / collection paths
128
129
  }));
129
130
  }
130
131
  /**
@@ -0,0 +1,10 @@
1
+ import type { Node } from "xq-parser";
2
+ import type { TypeDiagnostic, FileAnalysis } from "./types.ts";
3
+ /**
4
+ * Report an unsupported `xquery version` (XQST0031), serialization option
5
+ * misuse (XQST0108/0109/0110) and prolog components that the spec allows only once: setters such as
6
+ * `declare base-uri`, namespace prefixes, default element/function namespaces,
7
+ * decimal formats, and duplicate function parameter names.
8
+ * Only the second and later occurrences are flagged.
9
+ */
10
+ export declare function checkDuplicatePrologDecls(ast: Node, analysis: FileAnalysis): TypeDiagnostic[];