xq-lsp 1.8.2 → 1.8.3-next.430
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 +38 -0
- package/dist/ast-nodes.js +71 -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 +1215 -122
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/analyzer.ts +45 -3
- package/src/ast-nodes.ts +85 -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 +1265 -123
- package/src/types.ts +2 -0
package/dist/types.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface FunctionSymbol {
|
|
|
26
26
|
doc?: DocComment;
|
|
27
27
|
sourceUri: string;
|
|
28
28
|
sourceOffset?: number;
|
|
29
|
+
visibility?: "public" | "private";
|
|
29
30
|
}
|
|
30
31
|
export interface VariableSymbol {
|
|
31
32
|
qname: QName;
|
|
@@ -33,6 +34,7 @@ export interface VariableSymbol {
|
|
|
33
34
|
isModuleLevel: boolean;
|
|
34
35
|
sourceUri: string;
|
|
35
36
|
doc?: string;
|
|
37
|
+
type?: string;
|
|
36
38
|
}
|
|
37
39
|
export interface ImportInfo {
|
|
38
40
|
prefix: string;
|
package/package.json
CHANGED
package/src/analyzer.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const XMLNS_ARRAY = "http://www.w3.org/2005/xpath-functions/array";
|
|
|
26
26
|
// context, independent of any runtime/library configuration. math/map/array
|
|
27
27
|
// aren't spec-mandated but are universally available in practice (they back
|
|
28
28
|
// the 3.1 map/array/math core function libraries), so they're predeclared here too.
|
|
29
|
-
const BUILTIN_PREFIXES: Record<string, string> = {
|
|
29
|
+
export const BUILTIN_PREFIXES: Record<string, string> = {
|
|
30
30
|
fn: XMLNS_FN,
|
|
31
31
|
local: XMLNS_LOCAL,
|
|
32
32
|
xs: XMLNS_XS,
|
|
@@ -176,7 +176,10 @@ function findPrecedingDoc(comments: Terminal[], text: string, offset: number): D
|
|
|
176
176
|
// ── Namespace helpers ────────────────────────────────────────────────────────
|
|
177
177
|
|
|
178
178
|
function extractModuleNamespace(text: string): { prefix: string; uri: string } | undefined {
|
|
179
|
-
|
|
179
|
+
// The negative lookbehind excludes `import module namespace ...` — a module
|
|
180
|
+
// import, not this file's own `module namespace` declaration — which would
|
|
181
|
+
// otherwise match the same pattern and be mistaken for it.
|
|
182
|
+
const m = text.match(/(?<!import\s+)\bmodule\s+namespace\s+([\w\-]+)\s*=\s*["']([^"']*)["']/);
|
|
180
183
|
return m ? { prefix: m[1], uri: m[2] } : undefined;
|
|
181
184
|
}
|
|
182
185
|
|
|
@@ -201,6 +204,25 @@ function resolveNamespaceUri(prefix: string, prefixMap: Map<string, string>): st
|
|
|
201
204
|
return prefixMap.get(prefix) ?? `urn:xq-lsp:undeclared:${prefix}`;
|
|
202
205
|
}
|
|
203
206
|
|
|
207
|
+
const PREDEFINED_ENTITIES: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A BracedURILiteral's content may contain PredefinedEntityRef (& < >
|
|
211
|
+
* " ') and CharRef (&#DDDD; / &#xHHHH;) escapes, same as string content —
|
|
212
|
+
* e.g. `Q{http://example.com}` names the URI "http://example.com". The
|
|
213
|
+
* parser's terminal value is the raw, un-decoded source slice, so this decoding
|
|
214
|
+
* has to happen here rather than being assumed already done.
|
|
215
|
+
*/
|
|
216
|
+
function decodeCharRefs(text: string): string {
|
|
217
|
+
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (match, ref: string) => {
|
|
218
|
+
if (ref[0] === "#") {
|
|
219
|
+
const codePoint = ref[1] === "x" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
|
|
220
|
+
return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint);
|
|
221
|
+
}
|
|
222
|
+
return PREDEFINED_ENTITIES[ref] ?? match;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
204
226
|
/**
|
|
205
227
|
* Per the BracedURILiteral grammar, whitespace inside `Q{...}` is insignificant:
|
|
206
228
|
* leading/trailing whitespace is trimmed and internal runs are collapsed to a
|
|
@@ -208,7 +230,7 @@ function resolveNamespaceUri(prefix: string, prefixMap: Map<string, string>): st
|
|
|
208
230
|
* This does not decode percent-escapes — `Q{foo%20bar}` stays distinct from `Q{foo bar}`.
|
|
209
231
|
*/
|
|
210
232
|
function normalizeBracedUri(uri: string): string {
|
|
211
|
-
return uri.trim().replace(/\s+/g, " ");
|
|
233
|
+
return decodeCharRefs(uri).trim().replace(/\s+/g, " ");
|
|
212
234
|
}
|
|
213
235
|
|
|
214
236
|
/**
|
|
@@ -244,6 +266,19 @@ export function sequenceTypeText(text: string, node: Node): string | undefined {
|
|
|
244
266
|
return text.slice(node.start, node.end ?? undefined).trim() || undefined;
|
|
245
267
|
}
|
|
246
268
|
|
|
269
|
+
// A %public/%private annotation on an AnnotatedDecl controls whether the
|
|
270
|
+
// declaration is visible to modules that import it (XQuery 3.0+). Returns
|
|
271
|
+
// undefined when neither annotation is present; callers treat that as public,
|
|
272
|
+
// same as FunctionSymbol.visibility's own default.
|
|
273
|
+
function extractVisibility(annotated: Node): "public" | "private" | undefined {
|
|
274
|
+
for (const ann of directChildrenOf(annotated, "Annotation")) {
|
|
275
|
+
const eqname = directChildOf(ann, "EQName");
|
|
276
|
+
const raw = eqname ? firstTerminalValue(eqname) : null;
|
|
277
|
+
if (raw === "private" || raw === "public") return raw;
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
247
282
|
function extractFunctions(
|
|
248
283
|
ast: Node,
|
|
249
284
|
sourceUri: string,
|
|
@@ -296,6 +331,7 @@ function extractFunctions(
|
|
|
296
331
|
doc,
|
|
297
332
|
sourceUri,
|
|
298
333
|
sourceOffset: annotated.start,
|
|
334
|
+
visibility: extractVisibility(annotated),
|
|
299
335
|
});
|
|
300
336
|
}
|
|
301
337
|
return results;
|
|
@@ -318,12 +354,15 @@ function extractModuleVariables(
|
|
|
318
354
|
const name = varName ? firstTerminalValue(varName) : null;
|
|
319
355
|
if (!name) continue;
|
|
320
356
|
const doc = findPrecedingDoc(comments, text, annotated.start);
|
|
357
|
+
const typeDecl = directChildOf(varDecl, "TypeDeclaration");
|
|
358
|
+
const seqType = typeDecl ? directChildOf(typeDecl, "SequenceType") : undefined;
|
|
321
359
|
results.push({
|
|
322
360
|
qname: makeVarQName(name, prefixMap),
|
|
323
361
|
offset: varDecl.start,
|
|
324
362
|
isModuleLevel: true,
|
|
325
363
|
sourceUri,
|
|
326
364
|
doc: doc?.description || undefined,
|
|
365
|
+
type: seqType ? sequenceTypeText(text, seqType) : undefined,
|
|
327
366
|
});
|
|
328
367
|
}
|
|
329
368
|
return results;
|
|
@@ -466,12 +505,15 @@ function analyzeRegex(text: string, sourceUri: string): FileAnalysis {
|
|
|
466
505
|
RE_VAR_DECL.lastIndex = 0;
|
|
467
506
|
while ((m = RE_VAR_DECL.exec(text)) !== null) {
|
|
468
507
|
const doc = findPrecedingDocInText(text, m.index);
|
|
508
|
+
// `declare variable $x as T := …` / `… as T external` — T runs up to `:=` or `external`.
|
|
509
|
+
const typeMatch = /^\s+as\s+([^;]{1,200}?)\s*(?::=|\bexternal\b)/.exec(text.slice(m.index + m[0].length));
|
|
469
510
|
moduleVariables.push({
|
|
470
511
|
qname: makeVarQName(m[1], prefixMap),
|
|
471
512
|
offset: m.index,
|
|
472
513
|
isModuleLevel: true,
|
|
473
514
|
sourceUri,
|
|
474
515
|
doc: doc?.description || undefined,
|
|
516
|
+
type: typeMatch?.[1],
|
|
475
517
|
});
|
|
476
518
|
}
|
|
477
519
|
|
package/src/ast-nodes.ts
CHANGED
|
@@ -145,6 +145,63 @@ export function asFunctionDecl(node: Node, analysis: FileAnalysis): FunctionDecl
|
|
|
145
145
|
return { nameNode, qname: { prefix, localName, namespaceUri }, params, body: directChildOf(node, "FunctionBody") ?? null };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// ── Prolog declarations ───────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** The version string of `xquery version "x"` (quotes stripped) and its literal node; null when only an encoding is given. */
|
|
151
|
+
export function asVersionDecl(node: Node): { version: string; versionNode: Node } | null {
|
|
152
|
+
if (node.type !== "VersionDecl" || !directChildOf(node, "'version'")) return null;
|
|
153
|
+
const versionNode = directChildOf(node, "StringLiteral");
|
|
154
|
+
const raw = versionNode ? firstTerminalValue(versionNode) : null;
|
|
155
|
+
if (!versionNode || !raw) return null;
|
|
156
|
+
// String literals may contain character references, e.g. '1.0'
|
|
157
|
+
const version = raw
|
|
158
|
+
.slice(1, -1)
|
|
159
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
160
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)));
|
|
161
|
+
return { version, versionNode };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolved QName and name node of a `declare option` OptionDecl. */
|
|
165
|
+
export function asOptionDecl(node: Node, analysis: FileAnalysis): { qname: QName; nameNode: Node } | null {
|
|
166
|
+
if (node.type !== "OptionDecl") return null;
|
|
167
|
+
const nameNode = directChildOf(node, "EQName");
|
|
168
|
+
const qname = nameNode ? qnameFromRawTerminal(nameNode, analysis) : null;
|
|
169
|
+
return nameNode && qname ? { qname, nameNode } : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Prefix bound by a `declare namespace p = "uri"` NamespaceDecl. */
|
|
173
|
+
export function asNamespaceDecl(node: Node): { prefix: string } | null {
|
|
174
|
+
if (node.type !== "NamespaceDecl") return null;
|
|
175
|
+
const prefixNode = directChildOf(node, "NCName");
|
|
176
|
+
const prefix = prefixNode ? firstTerminalValue(prefixNode) : null;
|
|
177
|
+
return prefix === null ? null : { prefix };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Which default namespace a `declare default element|function namespace` DefaultNamespaceDecl sets. */
|
|
181
|
+
export function asDefaultNamespaceDecl(node: Node): { kind: "element" | "function" } | null {
|
|
182
|
+
if (node.type !== "DefaultNamespaceDecl") return null;
|
|
183
|
+
if (directChildOf(node, "'element'")) return { kind: "element" };
|
|
184
|
+
if (directChildOf(node, "'function'")) return { kind: "function" };
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface DecimalFormatDeclShape {
|
|
189
|
+
/** Expanded name of the format; "" for the default decimal format. */
|
|
190
|
+
key: string;
|
|
191
|
+
properties: Array<{ nameNode: Node; name: string }>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function asDecimalFormatDecl(node: Node, analysis: FileAnalysis): DecimalFormatDeclShape | null {
|
|
195
|
+
if (node.type !== "DecimalFormatDecl") return null;
|
|
196
|
+
const eqname = directChildOf(node, "EQName");
|
|
197
|
+
const qname = eqname ? qnameFromRawTerminal(eqname, analysis) : null;
|
|
198
|
+
const properties = directChildrenOf(node, "DFPropertyName").flatMap((nameNode) => {
|
|
199
|
+
const name = firstTerminalValue(nameNode);
|
|
200
|
+
return name ? [{ nameNode, name }] : [];
|
|
201
|
+
});
|
|
202
|
+
return { key: qname ? `${qname.namespaceUri}#${qname.localName}` : "", properties };
|
|
203
|
+
}
|
|
204
|
+
|
|
148
205
|
// ── InlineFunctionExpr ────────────────────────────────────────────────────────
|
|
149
206
|
|
|
150
207
|
export interface InlineFunctionExprShape {
|
|
@@ -426,3 +483,31 @@ export function argExpr(node: Node): Node | null {
|
|
|
426
483
|
if (node.type !== "Argument") return null;
|
|
427
484
|
return directChildOf(node, "ExprSingle") ?? null;
|
|
428
485
|
}
|
|
486
|
+
|
|
487
|
+
// ── Arithmetic expressions ────────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
export interface ArithmeticExprShape {
|
|
490
|
+
/** Operand expressions in source order (always at least two). */
|
|
491
|
+
operands: Node[];
|
|
492
|
+
/**
|
|
493
|
+
* Operators in source order; `operators[i]` sits between `operands[i]` and `operands[i + 1]`.
|
|
494
|
+
* Invariant: `operators.length === operands.length - 1`, so `operands[i + 1]` is always
|
|
495
|
+
* defined for every index into `operators`.
|
|
496
|
+
*/
|
|
497
|
+
operators: string[];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// The grammar produces a flat, left-associative chain `operand (op operand)*`. Every expression
|
|
501
|
+
// passes through these node types, so a chain with a single operand is not arithmetic → null.
|
|
502
|
+
export function asArithmeticExpr(node: Node): ArithmeticExprShape | null {
|
|
503
|
+
if (node.type !== "AdditiveExpr" && node.type !== "MultiplicativeExpr") return null;
|
|
504
|
+
if (isTerminal(node)) return null;
|
|
505
|
+
const operands: Node[] = [];
|
|
506
|
+
const operators: string[] = [];
|
|
507
|
+
for (const c of (node as import("xq-parser").NonTerminal).children) {
|
|
508
|
+
if (isTerminal(c)) operators.push(c.value);
|
|
509
|
+
else operands.push(c);
|
|
510
|
+
}
|
|
511
|
+
// Enforces the invariant documented on ArithmeticExprShape; a malformed chain is not arithmetic.
|
|
512
|
+
return operands.length > 1 && operators.length === operands.length - 1 ? { operands, operators } : null;
|
|
513
|
+
}
|
package/src/diagnostics.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.ts";
|
|
|
12
12
|
import { checkContextItemUsage } from "./context-item-diagnostics.ts";
|
|
13
13
|
import { checkUndeclaredVariables } from "./variable-diagnostics.ts";
|
|
14
14
|
import { checkDuplicateFunctions } from "./duplicate-function-diagnostics.ts";
|
|
15
|
+
import { checkDuplicatePrologDecls } from "./prolog-diagnostics.ts";
|
|
15
16
|
import { checkModuleImportTargets } from "./module-import-diagnostics.ts";
|
|
16
17
|
import { checkUnused } from "./unused-diagnostics.ts";
|
|
17
18
|
import { checkBracedUriWhitespace } from "./braced-uri-diagnostics.ts";
|
|
@@ -35,6 +36,7 @@ export function runDiagnostics(
|
|
|
35
36
|
...checkContextItemUsage(ast),
|
|
36
37
|
...checkUndeclaredVariables(ast, analysis, imports),
|
|
37
38
|
...checkDuplicateFunctions(ast, analysis, imports),
|
|
39
|
+
...checkDuplicatePrologDecls(ast, analysis),
|
|
38
40
|
...checkModuleImportTargets(analysis, imports),
|
|
39
41
|
];
|
|
40
42
|
}
|
|
@@ -1,13 +1,85 @@
|
|
|
1
1
|
import type { Node, NonTerminal } from "xq-parser";
|
|
2
2
|
import type { TypeDiagnostic, FileAnalysis, FunctionSymbol, QName } from "./types.ts";
|
|
3
3
|
import { formatQName } from "./types.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
findAll,
|
|
6
|
+
isTerminal,
|
|
7
|
+
directChildrenOf,
|
|
8
|
+
firstTerminalValue,
|
|
9
|
+
parseEQName,
|
|
10
|
+
resolvePrefix,
|
|
11
|
+
XMLNS_LOCAL,
|
|
12
|
+
XMLNS_FN,
|
|
13
|
+
XMLNS_XS,
|
|
14
|
+
XMLNS_MATH,
|
|
15
|
+
XMLNS_MAP,
|
|
16
|
+
XMLNS_ARRAY,
|
|
17
|
+
} from "./analyzer.ts";
|
|
5
18
|
import { asFunctionCall, asNamedFunctionRef } from "./ast-nodes.ts";
|
|
6
19
|
|
|
7
|
-
|
|
20
|
+
// Namespaces that are always in scope without an explicit import: the
|
|
21
|
+
// spec-mandated predeclared ones, plus whatever this file declares of its own
|
|
22
|
+
// (local: functions, its own module namespace, and its default function
|
|
23
|
+
// namespace) — we have complete, closed-world knowledge of all of these.
|
|
24
|
+
function alwaysAccessibleNamespaceUris(analysis: FileAnalysis): Set<string> {
|
|
25
|
+
const uris = new Set([XMLNS_LOCAL, XMLNS_FN, XMLNS_XS, XMLNS_MATH, XMLNS_MAP, XMLNS_ARRAY, analysis.defaultFunctionNamespace]);
|
|
26
|
+
if (analysis.moduleNamespaceUri) uris.add(analysis.moduleNamespaceUri);
|
|
27
|
+
return uris;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gather every function symbol this file can legally call, plus the set of
|
|
32
|
+
* imported namespace URIs we couldn't resolve to any analysis (unresolvable
|
|
33
|
+
* "at" path, or an unresolvable bare import — genuinely unknown, so calls
|
|
34
|
+
* into them are left unchecked to avoid false positives).
|
|
35
|
+
*
|
|
36
|
+
* A namespace not covered by any of the above (not declared, not imported,
|
|
37
|
+
* not predeclared) has zero accessible functions by construction: nothing in
|
|
38
|
+
* this module's static scope could have brought a function from it into
|
|
39
|
+
* view, regardless of what happens to be sitting in `importedAnalyses` (the
|
|
40
|
+
* QT4 harness, for instance, loads catalog modules that were never actually
|
|
41
|
+
* imported by the query under test, specifically to verify this isn't enough
|
|
42
|
+
* to make their functions callable).
|
|
43
|
+
*/
|
|
44
|
+
function accessibleFunctions(
|
|
45
|
+
analysis: FileAnalysis,
|
|
46
|
+
importedAnalyses: Map<string, FileAnalysis>,
|
|
47
|
+
): { fns: FunctionSymbol[]; unresolvedImportUris: Set<string> } {
|
|
8
48
|
const fns = [...analysis.functions];
|
|
9
|
-
|
|
10
|
-
|
|
49
|
+
const alwaysOpen = alwaysAccessibleNamespaceUris(analysis);
|
|
50
|
+
const importedNamespaceUris = new Set(analysis.imports.map((i) => i.namespaceUri));
|
|
51
|
+
// Runtime-predeclared namespaces (e.g. eXist-db's util:, xmldb:) are merged into
|
|
52
|
+
// namespaceDecls with offset -1 — see withPredeclaredNs — marking them as
|
|
53
|
+
// implicitly available without a written import. The QT4 harness uses the same
|
|
54
|
+
// offset -1 convention for <environment><namespace> bindings, which name a
|
|
55
|
+
// namespace without providing (or requiring) any declaration of what's in it.
|
|
56
|
+
const predeclaredUris = new Set(analysis.namespaceDecls.filter((nd) => nd.offset === -1).map((nd) => nd.namespaceUri));
|
|
57
|
+
// Namespaces we might have real knowledge of — but only once we find a matching
|
|
58
|
+
// entry in importedAnalyses below. Until then, stay silent about them rather
|
|
59
|
+
// than treating "no analysis available" as "no such function". Namespaces
|
|
60
|
+
// already in alwaysOpen are excluded: they're already guaranteed accessible
|
|
61
|
+
// (e.g. builtins arrive under a "builtin:" map key, not one matching their own
|
|
62
|
+
// namespace URI), so redundantly re-declaring one — as the QT4 harness's
|
|
63
|
+
// <environment><namespace prefix="map" uri=".../map"/> does — must not make it
|
|
64
|
+
// look unresolved.
|
|
65
|
+
const candidateUris = new Set([...importedNamespaceUris, ...predeclaredUris].filter((u) => !alwaysOpen.has(u)));
|
|
66
|
+
const resolvedUris = new Set<string>();
|
|
67
|
+
|
|
68
|
+
for (const [key, a] of importedAnalyses) {
|
|
69
|
+
if (key.startsWith("builtin:") || alwaysOpen.has(key)) {
|
|
70
|
+
fns.push(...a.functions);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (candidateUris.has(key)) {
|
|
74
|
+
// A %private function/variable is only visible within its own declaring
|
|
75
|
+
// module — filter it out for anything reached through an import.
|
|
76
|
+
fns.push(...a.functions.filter((f) => f.visibility !== "private"));
|
|
77
|
+
resolvedUris.add(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const unresolvedImportUris = new Set([...candidateUris].filter((u) => !resolvedUris.has(u)));
|
|
82
|
+
return { fns, unresolvedImportUris };
|
|
11
83
|
}
|
|
12
84
|
|
|
13
85
|
function arityDescription(overloads: FunctionSymbol[]): string {
|
|
@@ -20,15 +92,20 @@ function checkArity(
|
|
|
20
92
|
arity: number,
|
|
21
93
|
nodeStart: number,
|
|
22
94
|
allFns: FunctionSymbol[],
|
|
23
|
-
|
|
95
|
+
unresolvedImportUris: Set<string>,
|
|
24
96
|
errors: TypeDiagnostic[],
|
|
25
97
|
): void {
|
|
26
98
|
const { namespaceUri, localName } = qname;
|
|
99
|
+
// A prefix that resolves to nothing at all is a separate diagnostic's
|
|
100
|
+
// concern (findUndeclaredPrefixUsages) — don't pile on here.
|
|
101
|
+
if (namespaceUri.startsWith("urn:xq-lsp:undeclared:")) return;
|
|
102
|
+
// An import we couldn't resolve to any analysis: genuinely unknown, stay silent.
|
|
103
|
+
if (unresolvedImportUris.has(namespaceUri)) return;
|
|
104
|
+
|
|
27
105
|
const overloads = allFns.filter((f) => f.qname.namespaceUri === namespaceUri && f.qname.localName === localName);
|
|
28
106
|
const name = formatQName(qname);
|
|
29
107
|
|
|
30
108
|
if (overloads.length === 0) {
|
|
31
|
-
if (!knownNamespaceUris.has(namespaceUri)) return;
|
|
32
109
|
errors.push({ message: `${name} is not declared`, code: "XPST0017", offset: nodeStart, length: name.length });
|
|
33
110
|
return;
|
|
34
111
|
}
|
|
@@ -48,10 +125,13 @@ function checkArity(
|
|
|
48
125
|
* Walk all FunctionCall, NamedFunctionRef, and ArrowExpr nodes in the AST
|
|
49
126
|
* and report XPST0017 for any call where the argument count does not match
|
|
50
127
|
* any known overload, or where the function name is not declared in a
|
|
51
|
-
* namespace
|
|
128
|
+
* namespace this file can call into.
|
|
52
129
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
130
|
+
* A namespace is only skipped (left unchecked) when it's the target of an
|
|
131
|
+
* import we couldn't resolve to any analysis, or when the prefix itself is
|
|
132
|
+
* undeclared (that's findUndeclaredPrefixUsages's concern) — see
|
|
133
|
+
* accessibleFunctions and checkArity. Every other namespace is closed-world:
|
|
134
|
+
* not finding a matching declaration there means one doesn't exist.
|
|
55
135
|
*/
|
|
56
136
|
export function checkFunctionCalls(
|
|
57
137
|
ast: Node,
|
|
@@ -59,16 +139,14 @@ export function checkFunctionCalls(
|
|
|
59
139
|
importedAnalyses: Map<string, FileAnalysis>,
|
|
60
140
|
): TypeDiagnostic[] {
|
|
61
141
|
const errors: TypeDiagnostic[] = [];
|
|
62
|
-
const allFns =
|
|
63
|
-
// Namespaces we have at least one function declaration for.
|
|
64
|
-
const knownNamespaceUris = new Set(allFns.map((f) => f.qname.namespaceUri));
|
|
142
|
+
const { fns: allFns, unresolvedImportUris } = accessibleFunctions(analysis, importedAnalyses);
|
|
65
143
|
|
|
66
144
|
// ── FunctionCall ─────────────────────────────────────────────────────────────
|
|
67
145
|
|
|
68
146
|
for (const callNode of findAll(ast, "FunctionCall")) {
|
|
69
147
|
const call = asFunctionCall(callNode, analysis);
|
|
70
148
|
if (!call) continue;
|
|
71
|
-
checkArity(call.qname, call.args.length, callNode.start, allFns,
|
|
149
|
+
checkArity(call.qname, call.args.length, callNode.start, allFns, unresolvedImportUris, errors);
|
|
72
150
|
}
|
|
73
151
|
|
|
74
152
|
// ── NamedFunctionRef ─────────────────────────────────────────────────────────
|
|
@@ -76,7 +154,7 @@ export function checkFunctionCalls(
|
|
|
76
154
|
for (const refNode of findAll(ast, "NamedFunctionRef")) {
|
|
77
155
|
const ref = asNamedFunctionRef(refNode, analysis);
|
|
78
156
|
if (!ref) continue;
|
|
79
|
-
checkArity(ref.qname, ref.arity, refNode.start, allFns,
|
|
157
|
+
checkArity(ref.qname, ref.arity, refNode.start, allFns, unresolvedImportUris, errors);
|
|
80
158
|
}
|
|
81
159
|
|
|
82
160
|
// ── ArrowExpr ────────────────────────────────────────────────────────────────
|
|
@@ -102,7 +180,7 @@ export function checkFunctionCalls(
|
|
|
102
180
|
const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
|
|
103
181
|
const explicitArgs = directChildrenOf(argList, "Argument").length;
|
|
104
182
|
|
|
105
|
-
checkArity({ prefix, localName, namespaceUri }, 1 + explicitArgs, arrowNode.start, allFns,
|
|
183
|
+
checkArity({ prefix, localName, namespaceUri }, 1 + explicitArgs, arrowNode.start, allFns, unresolvedImportUris, errors);
|
|
106
184
|
}
|
|
107
185
|
}
|
|
108
186
|
|
package/src/inlay-hints.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { InlayHintKind } from "vscode-languageserver/node.js";
|
|
2
2
|
import type { InlayHint, Range } from "vscode-languageserver/node.js";
|
|
3
3
|
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
|
-
import type { Node
|
|
4
|
+
import type { Node } from "xq-parser";
|
|
5
5
|
import type { FileAnalysis, FunctionSymbol, XQueryType } from "./types.ts";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
6
|
+
import { findAll } from "./analyzer.ts";
|
|
7
|
+
import { asFunctionCall } from "./ast-nodes.ts";
|
|
8
|
+
import {
|
|
9
|
+
externalVariableTypes,
|
|
10
|
+
formatType,
|
|
11
|
+
resolveFunction,
|
|
12
|
+
walkModuleScopes,
|
|
13
|
+
withInferredReturnTypes,
|
|
14
|
+
} from "./typechecker.ts";
|
|
10
15
|
|
|
11
16
|
function allFunctionsFlat(analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): FunctionSymbol[] {
|
|
12
17
|
const fns = [...analysis.functions];
|
|
@@ -14,28 +19,10 @@ function allFunctionsFlat(analysis: FileAnalysis, importedAnalyses: Map<string,
|
|
|
14
19
|
return fns;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
function resolveCallee(
|
|
18
|
-
call: { qname: { namespaceUri: string; localName: string } },
|
|
19
|
-
argCount: number,
|
|
20
|
-
allFns: FunctionSymbol[],
|
|
21
|
-
): FunctionSymbol | undefined {
|
|
22
|
-
const overloads = allFns.filter(
|
|
23
|
-
(f) => f.qname.namespaceUri === call.qname.namespaceUri && f.qname.localName === call.qname.localName,
|
|
24
|
-
);
|
|
25
|
-
return overloads.find((f) => (f.variadic ? argCount >= f.arity : f.arity === argCount));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
function inRange(offset: number, range: { start: number; end: number }): boolean {
|
|
29
23
|
return offset >= range.start && offset <= range.end;
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
// Narrows a sequence's type to its item type (occurrence ""), for `for` bindings
|
|
33
|
-
// where the bound variable holds one item per iteration, not the whole sequence.
|
|
34
|
-
function itemTypeOf(t: XQueryType): XQueryType {
|
|
35
|
-
if (t.kind === "unknown" || t.kind === "empty") return t;
|
|
36
|
-
return t.occurrence === "" ? t : { ...t, occurrence: "" };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
26
|
// ── Parameter name hints ─────────────────────────────────────────────────────
|
|
40
27
|
|
|
41
28
|
function collectParameterHints(
|
|
@@ -49,7 +36,7 @@ function collectParameterHints(
|
|
|
49
36
|
for (const callNode of findAll(ast, "FunctionCall")) {
|
|
50
37
|
const call = asFunctionCall(callNode, analysis);
|
|
51
38
|
if (!call || call.args.length === 0) continue;
|
|
52
|
-
const fn =
|
|
39
|
+
const fn = resolveFunction(call.qname, call.args.length, allFns);
|
|
53
40
|
if (!fn) continue;
|
|
54
41
|
|
|
55
42
|
for (let i = 0; i < call.args.length && i < fn.params.length; i++) {
|
|
@@ -65,78 +52,40 @@ function collectParameterHints(
|
|
|
65
52
|
}
|
|
66
53
|
}
|
|
67
54
|
|
|
68
|
-
// ── Inferred type hints on untyped
|
|
55
|
+
// ── Inferred type hints on untyped bindings ──────────────────────────────────
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function buildModuleVarTypes(ast: Node, text: string, analysis: FileAnalysis): Map<string, XQueryType> {
|
|
79
|
-
const types = new Map<string, XQueryType>();
|
|
80
|
-
for (const node of findAll(ast, "VarDecl")) {
|
|
81
|
-
const binding = asTypedBinding(node, text, analysis);
|
|
82
|
-
if (binding?.typeStr) types.set(qnameKey(binding.qname), parseType(binding.typeStr));
|
|
83
|
-
}
|
|
84
|
-
return types;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Walks the tree tracking in-scope variable types, mirroring typechecker.ts's scope walk,
|
|
88
|
-
// but also records the inferred type of untyped let/for bindings so later bindings see it.
|
|
89
|
-
function walkForTypeHints(
|
|
90
|
-
node: Node,
|
|
91
|
-
scopeTypes: Map<string, XQueryType>,
|
|
92
|
-
moduleTypes: Map<string, XQueryType>,
|
|
93
|
-
text: string,
|
|
57
|
+
// Shows the inferred type after the name of every binding without a declared type:
|
|
58
|
+
// let/for/group-by variables and module-level `declare variable`s. The scope walk is the
|
|
59
|
+
// same one the type checker uses, so hints reflect exactly what diagnostics see.
|
|
60
|
+
function collectTypeHints(
|
|
61
|
+
ast: Node,
|
|
94
62
|
analysis: FileAnalysis,
|
|
95
63
|
allFns: FunctionSymbol[],
|
|
96
64
|
range: { start: number; end: number },
|
|
97
65
|
hints: InlayHint[],
|
|
98
66
|
doc: TextDocument,
|
|
67
|
+
outerScope: Map<string, XQueryType>,
|
|
99
68
|
): void {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// is the *item* type (occurrence narrowed to singular) — not the type of
|
|
121
|
-
// the whole iterated sequence, which is what inferExprType returns.
|
|
122
|
-
const inferred = node.type === "ForBinding" ? itemTypeOf(inferredSeq) : inferredSeq;
|
|
123
|
-
scopeTypes.set(qnameKey(typed.qname), inferred);
|
|
124
|
-
const nameEnd = binding.nameNode.end ?? binding.nameNode.start;
|
|
125
|
-
if (inferred.kind !== "unknown" && inRange(nameEnd, range)) {
|
|
126
|
-
hints.push({
|
|
127
|
-
position: doc.positionAt(nameEnd),
|
|
128
|
-
label: `: ${formatType(inferred)}`,
|
|
129
|
-
kind: InlayHintKind.Type,
|
|
130
|
-
paddingLeft: true,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
for (const child of nt.children) {
|
|
138
|
-
walkForTypeHints(child, scopeTypes, moduleTypes, text, analysis, allFns, range, hints, doc);
|
|
139
|
-
}
|
|
69
|
+
const seen = new Set<number>();
|
|
70
|
+
walkModuleScopes(
|
|
71
|
+
ast,
|
|
72
|
+
analysis,
|
|
73
|
+
allFns,
|
|
74
|
+
{
|
|
75
|
+
onBinding: (nameNode, type) => {
|
|
76
|
+
const nameEnd = nameNode.end ?? nameNode.start;
|
|
77
|
+
if (type.kind === "unknown" || !inRange(nameEnd, range) || seen.has(nameEnd)) return;
|
|
78
|
+
seen.add(nameEnd);
|
|
79
|
+
hints.push({
|
|
80
|
+
position: doc.positionAt(nameEnd),
|
|
81
|
+
label: `: ${formatType(type)}`,
|
|
82
|
+
kind: InlayHintKind.Type,
|
|
83
|
+
paddingLeft: true,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
outerScope,
|
|
88
|
+
);
|
|
140
89
|
}
|
|
141
90
|
|
|
142
91
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
@@ -148,15 +97,15 @@ export function getInlayHints(
|
|
|
148
97
|
imported: Map<string, FileAnalysis>,
|
|
149
98
|
range: Range,
|
|
150
99
|
): InlayHint[] {
|
|
151
|
-
const text = doc.getText();
|
|
152
100
|
const offsetRange = { start: doc.offsetAt(range.start), end: doc.offsetAt(range.end) };
|
|
153
|
-
const
|
|
101
|
+
const declaredFns = allFunctionsFlat(analysis, imported);
|
|
102
|
+
const outerScope = externalVariableTypes(analysis, imported.values(), declaredFns);
|
|
103
|
+
const allFns = withInferredReturnTypes(ast, analysis, declaredFns, outerScope);
|
|
154
104
|
const hints: InlayHint[] = [];
|
|
155
105
|
|
|
156
106
|
collectParameterHints(ast, analysis, allFns, offsetRange, hints, doc);
|
|
157
107
|
|
|
158
|
-
|
|
159
|
-
walkForTypeHints(ast, new Map(moduleTypes), moduleTypes, text, analysis, allFns, offsetRange, hints, doc);
|
|
108
|
+
collectTypeHints(ast, analysis, allFns, offsetRange, hints, doc, outerScope);
|
|
160
109
|
|
|
161
110
|
return hints;
|
|
162
111
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FileAnalysis } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The declarations that *other* files contribute to a module namespace. XQuery lets one module be
|
|
5
|
+
* implemented by several module files sharing a target namespace, and some runtimes (e.g. Fonto)
|
|
6
|
+
* merge them, so a file can call a function declared in a sibling without importing it.
|
|
7
|
+
*
|
|
8
|
+
* `merged` is the glob index entry for the namespace, which may include the current file's own
|
|
9
|
+
* on-disk declarations. Those are dropped so they aren't reported as duplicates of the (possibly
|
|
10
|
+
* edited) in-memory versions, nor resolved to stale positions. Returns null when nothing is left.
|
|
11
|
+
*/
|
|
12
|
+
export function siblingModuleAnalysis(merged: FileAnalysis, currentUri: string): FileAnalysis | null {
|
|
13
|
+
const functions = merged.functions.filter((f) => f.sourceUri !== currentUri);
|
|
14
|
+
const moduleVariables = merged.moduleVariables.filter((v) => v.sourceUri !== currentUri);
|
|
15
|
+
if (functions.length === 0 && moduleVariables.length === 0) return null;
|
|
16
|
+
return { ...merged, functions, moduleVariables };
|
|
17
|
+
}
|