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 +16 -9
- package/builtins/builtins-fn.xq +1 -1
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +43 -3
- package/dist/ast-nodes.d.ts +27 -0
- package/dist/ast-nodes.js +53 -0
- package/dist/diagnostics.js +2 -0
- package/dist/functioncall-diagnostics.d.ts +6 -3
- package/dist/functioncall-diagnostics.js +76 -17
- package/dist/inlay-hints.js +28 -82
- package/dist/module-siblings.d.ts +11 -0
- package/dist/module-siblings.js +16 -0
- package/dist/predeclared-namespaces.js +1 -0
- package/dist/prolog-diagnostics.d.ts +10 -0
- package/dist/prolog-diagnostics.js +113 -0
- package/dist/qt4-worker.js +5 -3
- package/dist/runtimes/fonto/fonto.xq +95 -0
- package/dist/semantic-tokens.d.ts +29 -0
- package/dist/semantic-tokens.js +254 -0
- package/dist/server.js +40 -7
- package/dist/typechecker.d.ts +48 -1
- package/dist/typechecker.js +1187 -108
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/analyzer.ts +45 -3
- package/src/ast-nodes.ts +57 -0
- package/src/diagnostics.ts +2 -0
- package/src/functioncall-diagnostics.ts +93 -15
- package/src/inlay-hints.ts +42 -93
- package/src/module-siblings.ts +17 -0
- package/src/predeclared-namespaces.ts +1 -0
- package/src/prolog-diagnostics.ts +137 -0
- package/src/qt4-worker.ts +6 -4
- package/src/runtimes/fonto/fonto.xq +95 -0
- package/src/semantic-tokens.ts +289 -0
- package/src/server.ts +40 -6
- package/src/typechecker.ts +1240 -115
- package/src/types.ts +2 -0
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/).
|
package/builtins/builtins-fn.xq
CHANGED
|
@@ -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 ───────────────────────────────────────────────────────── :)
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 (& < >
|
|
189
|
+
* " ') and CharRef (&#DDDD; / &#xHHHH;) escapes, same as string content —
|
|
190
|
+
* e.g. `Q{http://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;
|
package/dist/ast-nodes.d.ts
CHANGED
|
@@ -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.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;
|
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
|
7
|
+
* namespace this file can call into.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
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
|
|
96
|
+
* namespace this file can call into.
|
|
39
97
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
142
|
+
checkArity({ prefix, localName, namespaceUri }, 1 + explicitArgs, arrowNode.start, allFns, unresolvedImportUris, errors);
|
|
84
143
|
}
|
|
85
144
|
}
|
|
86
145
|
return errors;
|
package/dist/inlay-hints.js
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { InlayHintKind } from "vscode-languageserver/node.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 =
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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[];
|