xq-lsp 1.8.3-next.430 → 1.8.3-next.439
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/dist/module-import-diagnostics.d.ts +10 -0
- package/dist/module-import-diagnostics.js +21 -0
- package/dist/namespace-diagnostics.d.ts +16 -1
- package/dist/namespace-diagnostics.js +105 -12
- package/dist/server.js +5 -2
- package/dist/unused-diagnostics.js +8 -2
- package/dist/variable-diagnostics.d.ts +2 -0
- package/dist/variable-diagnostics.js +32 -24
- package/package.json +1 -1
- package/src/module-import-diagnostics.ts +28 -0
- package/src/namespace-diagnostics.ts +116 -12
- package/src/server.ts +7 -2
- package/src/unused-diagnostics.ts +9 -2
- package/src/variable-diagnostics.ts +34 -22
|
@@ -17,3 +17,13 @@ export interface ModuleImportDiagnostic extends TypeDiagnostic {
|
|
|
17
17
|
* checked here.
|
|
18
18
|
*/
|
|
19
19
|
export declare function checkModuleImportTargets(analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): ModuleImportDiagnostic[];
|
|
20
|
+
export interface UnresolvedImportDiagnostic extends TypeDiagnostic {
|
|
21
|
+
code: "xq-lsp:unresolved-import";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Report imports whose module could not be loaded. Variable and function
|
|
25
|
+
* checks stay silent for such namespaces, so this is where the user learns
|
|
26
|
+
* why. Reported as a warning (not XQST0059) because the module may
|
|
27
|
+
* legitimately live outside the workspace or be generated at install time.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkUnresolvedImports(analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): UnresolvedImportDiagnostic[];
|
|
@@ -33,3 +33,24 @@ export function checkModuleImportTargets(analysis, importedAnalyses) {
|
|
|
33
33
|
}
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Report imports whose module could not be loaded. Variable and function
|
|
38
|
+
* checks stay silent for such namespaces, so this is where the user learns
|
|
39
|
+
* why. Reported as a warning (not XQST0059) because the module may
|
|
40
|
+
* legitimately live outside the workspace or be generated at install time.
|
|
41
|
+
*/
|
|
42
|
+
export function checkUnresolvedImports(analysis, importedAnalyses) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const imp of analysis.imports) {
|
|
45
|
+
if (importedAnalyses.has(imp.atPath ?? imp.namespaceUri) || importedAnalyses.has(imp.namespaceUri))
|
|
46
|
+
continue;
|
|
47
|
+
const location = imp.atPath ? `at '${imp.atPath}'` : `for namespace '${imp.namespaceUri}'`;
|
|
48
|
+
out.push({
|
|
49
|
+
message: `Module ${location} could not be resolved; its functions and variables are not checked`,
|
|
50
|
+
code: "xq-lsp:unresolved-import",
|
|
51
|
+
offset: imp.offset,
|
|
52
|
+
length: imp.prefix.length,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
@@ -3,12 +3,27 @@ import type { FileAnalysis } from "./types.ts";
|
|
|
3
3
|
export type NamespaceUsageKind = "function" | "variable" | "element";
|
|
4
4
|
export interface NamespaceDiagnostic {
|
|
5
5
|
message: string;
|
|
6
|
-
code: "XQST0081";
|
|
6
|
+
code: "XQST0081" | "FONS0004";
|
|
7
7
|
offset: number;
|
|
8
8
|
length: number;
|
|
9
9
|
prefix: string;
|
|
10
10
|
usageKind: NamespaceUsageKind;
|
|
11
11
|
}
|
|
12
|
+
export interface DynamicPrefixUse {
|
|
13
|
+
/** The prefix text. */
|
|
14
|
+
prefix: string;
|
|
15
|
+
/** Source offset of the prefix (inside the string literal). */
|
|
16
|
+
offset: number;
|
|
17
|
+
/** True when an unbound prefix is an error (xs:QName / xs:NOTATION); false for namespace-uri-for-prefix, which just returns (). */
|
|
18
|
+
required: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Prefixes taken from a string literal and resolved against the static
|
|
22
|
+
* in-scope namespaces at runtime: `xs:QName("p:l")`, `"p:l" cast as xs:QName`
|
|
23
|
+
* (also `castable as`, `xs:NOTATION`) and `fn:namespace-uri-for-prefix("p", …)`.
|
|
24
|
+
* Only literal operands are detected.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findDynamicPrefixUses(ast: Node, analysis: FileAnalysis): DynamicPrefixUse[];
|
|
12
27
|
/**
|
|
13
28
|
* Walk the AST and report every prefixed name reference whose prefix is not
|
|
14
29
|
* declared in `analysis` (via import module namespace, declare namespace,
|
|
@@ -1,4 +1,70 @@
|
|
|
1
|
-
import { isTerminal, directChildOf, firstTerminalValue, resolvePrefix } from "./analyzer.js";
|
|
1
|
+
import { XMLNS_FN, XMLNS_XS, isTerminal, directChildOf, findAll, firstTerminalValue, parseEQName, resolvePrefix, } from "./analyzer.js";
|
|
2
|
+
import { asFunctionCall } from "./ast-nodes.js";
|
|
3
|
+
/** Unwraps single-child grammar wrappers down to a StringLiteral terminal; null for anything more complex. */
|
|
4
|
+
function asStringLiteral(node) {
|
|
5
|
+
let cur = node;
|
|
6
|
+
while (cur && !isTerminal(cur)) {
|
|
7
|
+
const children = cur.children;
|
|
8
|
+
if (children.length !== 1)
|
|
9
|
+
return null;
|
|
10
|
+
cur = children[0];
|
|
11
|
+
}
|
|
12
|
+
return cur && cur.type === "StringLiteral" ? cur : null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Prefixes taken from a string literal and resolved against the static
|
|
16
|
+
* in-scope namespaces at runtime: `xs:QName("p:l")`, `"p:l" cast as xs:QName`
|
|
17
|
+
* (also `castable as`, `xs:NOTATION`) and `fn:namespace-uri-for-prefix("p", …)`.
|
|
18
|
+
* Only literal operands are detected.
|
|
19
|
+
*/
|
|
20
|
+
export function findDynamicPrefixUses(ast, analysis) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const fromQNameLiteral = (lit, required) => {
|
|
23
|
+
if (!lit)
|
|
24
|
+
return;
|
|
25
|
+
const text = lit.value.slice(1, -1);
|
|
26
|
+
const trimmedStart = text.length - text.trimStart().length;
|
|
27
|
+
const t = text.trim();
|
|
28
|
+
const c = t.indexOf(":");
|
|
29
|
+
if (c <= 0)
|
|
30
|
+
return;
|
|
31
|
+
out.push({ prefix: t.slice(0, c), offset: lit.start + 1 + trimmedStart, required });
|
|
32
|
+
};
|
|
33
|
+
for (const call of findAll(ast, "FunctionCall")) {
|
|
34
|
+
const fc = asFunctionCall(call, analysis);
|
|
35
|
+
if (!fc)
|
|
36
|
+
continue;
|
|
37
|
+
const { namespaceUri, localName } = fc.qname;
|
|
38
|
+
const lit = asStringLiteral(fc.args[0]);
|
|
39
|
+
if (!lit)
|
|
40
|
+
continue;
|
|
41
|
+
if (namespaceUri === XMLNS_XS && (localName === "QName" || localName === "NOTATION")) {
|
|
42
|
+
fromQNameLiteral(lit, true);
|
|
43
|
+
}
|
|
44
|
+
else if (namespaceUri === XMLNS_FN && localName === "namespace-uri-for-prefix") {
|
|
45
|
+
const prefix = lit.value.slice(1, -1);
|
|
46
|
+
if (prefix.length > 0)
|
|
47
|
+
out.push({ prefix, offset: lit.start + 1, required: false });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// castable as never raises an error for an unbound prefix; it just yields false.
|
|
51
|
+
for (const type of ["CastExpr", "CastableExpr"]) {
|
|
52
|
+
for (const node of findAll(ast, type)) {
|
|
53
|
+
const single = directChildOf(node, "SingleType");
|
|
54
|
+
const typeNode = single && findAll(single, "QName")[0];
|
|
55
|
+
const typeName = typeNode && firstTerminalValue(typeNode);
|
|
56
|
+
if (!typeName)
|
|
57
|
+
continue;
|
|
58
|
+
const { prefix, localName, uri } = parseEQName(typeName);
|
|
59
|
+
if ((uri ?? resolvePrefix(prefix, analysis)) !== XMLNS_XS)
|
|
60
|
+
continue;
|
|
61
|
+
if (localName !== "QName" && localName !== "NOTATION")
|
|
62
|
+
continue;
|
|
63
|
+
fromQNameLiteral(asStringLiteral(node.children[0]), type === "CastExpr");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
2
68
|
function checkQName(node, kind, analysis, inlineStack, out) {
|
|
3
69
|
if (!node)
|
|
4
70
|
return;
|
|
@@ -23,6 +89,20 @@ function checkQName(node, kind, analysis, inlineStack, out) {
|
|
|
23
89
|
});
|
|
24
90
|
}
|
|
25
91
|
}
|
|
92
|
+
function inlineXmlnsPrefixes(elem) {
|
|
93
|
+
const local = new Set();
|
|
94
|
+
const attrList = directChildOf(elem, "DirAttributeList");
|
|
95
|
+
if (attrList && !isTerminal(attrList)) {
|
|
96
|
+
for (const c of attrList.children) {
|
|
97
|
+
if (c.type !== "QName")
|
|
98
|
+
continue;
|
|
99
|
+
const attrName = firstTerminalValue(c);
|
|
100
|
+
if (attrName?.startsWith("xmlns:"))
|
|
101
|
+
local.add(attrName.slice(6));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return local;
|
|
105
|
+
}
|
|
26
106
|
function walk(node, analysis, inlineStack, out) {
|
|
27
107
|
if (isTerminal(node))
|
|
28
108
|
return;
|
|
@@ -30,17 +110,7 @@ function walk(node, analysis, inlineStack, out) {
|
|
|
30
110
|
if (node.type === "DirElemConstructor") {
|
|
31
111
|
// Collect xmlns:prefix declarations from this element's attribute list,
|
|
32
112
|
// then push them onto the scope stack for the duration of this subtree.
|
|
33
|
-
const local =
|
|
34
|
-
const attrList = directChildOf(node, "DirAttributeList");
|
|
35
|
-
if (attrList && !isTerminal(attrList)) {
|
|
36
|
-
for (const c of attrList.children) {
|
|
37
|
-
if (c.type !== "QName")
|
|
38
|
-
continue;
|
|
39
|
-
const attrName = firstTerminalValue(c);
|
|
40
|
-
if (attrName?.startsWith("xmlns:"))
|
|
41
|
-
local.add(attrName.slice(6));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
113
|
+
const local = inlineXmlnsPrefixes(node);
|
|
44
114
|
inlineStack.push(local);
|
|
45
115
|
checkQName(directChildOf(node, "QName"), "element", analysis, inlineStack, out);
|
|
46
116
|
for (const c of children)
|
|
@@ -77,6 +147,29 @@ export function findUndeclaredPrefixUsages(ast, analysis) {
|
|
|
77
147
|
return [];
|
|
78
148
|
const out = [];
|
|
79
149
|
walk(ast, analysis, [], out);
|
|
150
|
+
// Prefixes inside string literals resolve against the in-scope namespaces, which
|
|
151
|
+
// include xmlns:prefix declarations of enclosing direct element constructors.
|
|
152
|
+
const dirElems = findAll(ast, "DirElemConstructor").map((el) => ({
|
|
153
|
+
start: el.start ?? 0,
|
|
154
|
+
end: el.end ?? 0,
|
|
155
|
+
prefixes: inlineXmlnsPrefixes(el),
|
|
156
|
+
}));
|
|
157
|
+
for (const use of findDynamicPrefixUses(ast, analysis)) {
|
|
158
|
+
if (!use.required)
|
|
159
|
+
continue;
|
|
160
|
+
if (dirElems.some((el) => el.start <= use.offset && use.offset < el.end && el.prefixes.has(use.prefix)))
|
|
161
|
+
continue;
|
|
162
|
+
if (!resolvePrefix(use.prefix, analysis).startsWith("urn:xq-lsp:undeclared:"))
|
|
163
|
+
continue;
|
|
164
|
+
out.push({
|
|
165
|
+
message: `Namespace prefix '${use.prefix}' is not declared`,
|
|
166
|
+
code: "FONS0004",
|
|
167
|
+
offset: use.offset,
|
|
168
|
+
length: use.prefix.length,
|
|
169
|
+
prefix: use.prefix,
|
|
170
|
+
usageKind: "element",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
80
173
|
return out;
|
|
81
174
|
}
|
|
82
175
|
// ── Insertion-position helpers (used by the code action handler) ─────────────
|
package/dist/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { findConfig, expandGlobs } from "./config.js";
|
|
|
18
18
|
import { findImportInsertPosition, findDeclareNsInsertPosition, computeRelativePath } from "./namespace-diagnostics.js";
|
|
19
19
|
import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.js";
|
|
20
20
|
import { runDiagnostics, runHints } from "./diagnostics.js";
|
|
21
|
+
import { checkUnresolvedImports } from "./module-import-diagnostics.js";
|
|
21
22
|
import { getRuntimeAnalyses, getRuntimePredeclaredNamespaces, withPredeclaredNs, getRuntimePredeclaredVariables, withPredeclaredVariables, } from "./runtimes.js";
|
|
22
23
|
import { buildOrganizeImportsEdit, buildExtractVariableEdit, buildExtractFunctionEdit } from "./refactor-actions.js";
|
|
23
24
|
import { getWorkspaceSymbols } from "./workspace-symbol.js";
|
|
@@ -279,6 +280,7 @@ function computeDiagnostics(doc) {
|
|
|
279
280
|
const nsDiagRaw = findUndeclaredPrefixUsages(ast, analysis);
|
|
280
281
|
const errorDiagRaw = hasAst && ast !== null ? runDiagnostics(ast, doc.getText(), analysis, resolvedImported) : [];
|
|
281
282
|
const hintDiagRaw = hasAst && ast !== null ? runHints(ast, analysis) : [];
|
|
283
|
+
const unresolvedImportRaw = hasAst ? checkUnresolvedImports(analysis, resolvedImported) : [];
|
|
282
284
|
function toLsp(d, severity) {
|
|
283
285
|
return {
|
|
284
286
|
severity,
|
|
@@ -295,7 +297,8 @@ function computeDiagnostics(doc) {
|
|
|
295
297
|
// nsDiags already covers XQST0081 (with the code-action data field), so exclude those here.
|
|
296
298
|
const errorDiags = errorDiagRaw.filter((d) => d.code !== "XQST0081").map((d) => toLsp(d, DiagnosticSeverity.Error));
|
|
297
299
|
const hintDiags = hintDiagRaw.map((d) => toLsp(d, DiagnosticSeverity.Hint));
|
|
298
|
-
|
|
300
|
+
const unresolvedImportDiags = unresolvedImportRaw.map((d) => toLsp(d, DiagnosticSeverity.Warning));
|
|
301
|
+
return [...parseDiags, ...nsDiags, ...errorDiags, ...unresolvedImportDiags, ...hintDiags];
|
|
299
302
|
}
|
|
300
303
|
documents.onDidChangeContent((change) => {
|
|
301
304
|
connection.sendDiagnostics({ uri: change.document.uri, diagnostics: computeDiagnostics(change.document) });
|
|
@@ -524,7 +527,7 @@ connection.onCodeAction((params) => {
|
|
|
524
527
|
const actions = [];
|
|
525
528
|
const only = params.context.only;
|
|
526
529
|
for (const diag of params.context.diagnostics) {
|
|
527
|
-
if (diag.code !== "XQST0081")
|
|
530
|
+
if (diag.code !== "XQST0081" && diag.code !== "FONS0004")
|
|
528
531
|
continue;
|
|
529
532
|
const data = diag.data;
|
|
530
533
|
if (!data?.prefix)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { qnameKey, formatQName } from "./types.js";
|
|
2
|
+
import { findDynamicPrefixUses } from "./namespace-diagnostics.js";
|
|
2
3
|
import { isTerminal, directChildOf, directChildrenOf, findAll, firstTerminalValue, parseEQName, resolvePrefix } from "./analyzer.js";
|
|
3
4
|
import { asFunctionCall, asNamedFunctionRef, asVarRef, asVarName, asFunctionDecl, asFunctionDeclaration, asVariableDeclaration, asBinding, asCatchClause, } from "./ast-nodes.js";
|
|
4
5
|
// ── Scope stack ───────────────────────────────────────────────────────────────
|
|
@@ -170,7 +171,7 @@ function walk(node, scope, analysis, usedFns, usedVars) {
|
|
|
170
171
|
* two terminal types catches every usage site without enumerating each
|
|
171
172
|
* grammar production that can carry a prefix.
|
|
172
173
|
*/
|
|
173
|
-
function collectUsedPrefixes(ast) {
|
|
174
|
+
function collectUsedPrefixes(ast, analysis) {
|
|
174
175
|
const used = new Set();
|
|
175
176
|
for (const node of findAll(ast, "QName")) {
|
|
176
177
|
const value = firstTerminalValue(node);
|
|
@@ -191,8 +192,13 @@ function collectUsedPrefixes(ast) {
|
|
|
191
192
|
if (prefix !== "*")
|
|
192
193
|
used.add(prefix);
|
|
193
194
|
}
|
|
195
|
+
collectDynamicPrefixUses(ast, analysis, used);
|
|
194
196
|
return used;
|
|
195
197
|
}
|
|
198
|
+
function collectDynamicPrefixUses(ast, analysis, used) {
|
|
199
|
+
for (const use of findDynamicPrefixUses(ast, analysis))
|
|
200
|
+
used.add(use.prefix);
|
|
201
|
+
}
|
|
196
202
|
// ── Public entry point ────────────────────────────────────────────────────────
|
|
197
203
|
/**
|
|
198
204
|
* Walk the AST and report declared functions and module-level variables that
|
|
@@ -248,7 +254,7 @@ export function checkUnused(ast, analysis) {
|
|
|
248
254
|
});
|
|
249
255
|
}
|
|
250
256
|
// ── Check imports and namespace declarations ─────────────────────────────
|
|
251
|
-
const usedPrefixes = collectUsedPrefixes(ast);
|
|
257
|
+
const usedPrefixes = collectUsedPrefixes(ast, analysis);
|
|
252
258
|
for (const imp of analysis.imports) {
|
|
253
259
|
if (usedPrefixes.has(imp.prefix))
|
|
254
260
|
continue;
|
|
@@ -11,6 +11,8 @@ export interface UndeclaredVariableDiagnostic {
|
|
|
11
11
|
* to a declared variable: function parameter, let/for binding, module-level
|
|
12
12
|
* `declare variable`, or an imported module variable.
|
|
13
13
|
*
|
|
14
|
+
* Variables whose prefix is bound by an import that did not resolve to an
|
|
15
|
+
* analysed module are skipped too (XQST0059 reports the import itself).
|
|
14
16
|
* Variables whose namespace prefix is undeclared are skipped — XQST0081
|
|
15
17
|
* already fires for those, and adding XPST0008 on top would be noisy.
|
|
16
18
|
* Variables inside catch clauses are also skipped since the implicit
|
|
@@ -10,10 +10,14 @@ class ScopeStack {
|
|
|
10
10
|
has(key) { return this.frames.some((f) => f.has(key)); }
|
|
11
11
|
}
|
|
12
12
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
13
|
-
function isKnown(qname, scope, moduleVarKeys) {
|
|
13
|
+
function isKnown(qname, scope, moduleVarKeys, unresolvedImportUris) {
|
|
14
14
|
// Variables with an undeclared-prefix URI are already caught by XQST0081; skip here.
|
|
15
15
|
if (qname.namespaceUri.startsWith("urn:xq-lsp:undeclared:"))
|
|
16
16
|
return true;
|
|
17
|
+
// The import exists but its module was not analysed (outside the workspace,
|
|
18
|
+
// generated at install time, …): we cannot know what it declares, so stay silent.
|
|
19
|
+
if (unresolvedImportUris.has(qname.namespaceUri))
|
|
20
|
+
return true;
|
|
17
21
|
return scope.has(qnameKey(qname)) || moduleVarKeys.has(qnameKey(qname));
|
|
18
22
|
}
|
|
19
23
|
function reportVarRef(node, qname, out) {
|
|
@@ -25,18 +29,18 @@ function reportVarRef(node, qname, out) {
|
|
|
25
29
|
length: name.length + 1, // +1 for "$"
|
|
26
30
|
});
|
|
27
31
|
}
|
|
28
|
-
function addBinding(bindingNode, scope, analysis, moduleVarKeys, out) {
|
|
32
|
+
function addBinding(bindingNode, scope, analysis, moduleVarKeys, unresolvedImportUris, out) {
|
|
29
33
|
const b = asBinding(bindingNode, analysis);
|
|
30
34
|
if (!b)
|
|
31
35
|
return;
|
|
32
36
|
if (b.initExpr)
|
|
33
|
-
walk(b.initExpr, scope, analysis, moduleVarKeys, out);
|
|
37
|
+
walk(b.initExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
34
38
|
scope.add(qnameKey(b.qname));
|
|
35
39
|
if (b.positionalVar)
|
|
36
40
|
scope.add(qnameKey(b.positionalVar.qname));
|
|
37
41
|
}
|
|
38
42
|
// ── Scope-aware AST walker ────────────────────────────────────────────────────
|
|
39
|
-
function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
43
|
+
function walk(node, scope, analysis, moduleVarKeys, unresolvedImportUris, out) {
|
|
40
44
|
if (isTerminal(node))
|
|
41
45
|
return;
|
|
42
46
|
const { children } = node;
|
|
@@ -49,7 +53,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
49
53
|
for (const p of fn.params)
|
|
50
54
|
scope.add(qnameKey(p.qname));
|
|
51
55
|
if (fn.body)
|
|
52
|
-
walk(fn.body, scope, analysis, moduleVarKeys, out);
|
|
56
|
+
walk(fn.body, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
53
57
|
scope.pop();
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
@@ -57,7 +61,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
57
61
|
const varDecl = directChildOf(node, "VarDecl");
|
|
58
62
|
if (varDecl && !isTerminal(varDecl)) {
|
|
59
63
|
for (const child of varDecl.children)
|
|
60
|
-
walk(child, scope, analysis, moduleVarKeys, out);
|
|
64
|
+
walk(child, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
61
65
|
}
|
|
62
66
|
return;
|
|
63
67
|
}
|
|
@@ -77,19 +81,19 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
77
81
|
switch (clause.type) {
|
|
78
82
|
case "ForClause":
|
|
79
83
|
for (const b of directChildrenOf(clause, "ForBinding"))
|
|
80
|
-
addBinding(b, scope, analysis, moduleVarKeys, out);
|
|
84
|
+
addBinding(b, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
81
85
|
break;
|
|
82
86
|
case "LetClause":
|
|
83
87
|
for (const b of directChildrenOf(clause, "LetBinding"))
|
|
84
|
-
addBinding(b, scope, analysis, moduleVarKeys, out);
|
|
88
|
+
addBinding(b, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
85
89
|
break;
|
|
86
90
|
case "CountClause":
|
|
87
|
-
addBinding(clause, scope, analysis, moduleVarKeys, out);
|
|
91
|
+
addBinding(clause, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
88
92
|
break;
|
|
89
93
|
case "GroupByClause": {
|
|
90
94
|
const specList = directChildOf(clause, "GroupingSpecList");
|
|
91
95
|
for (const spec of specList ? directChildrenOf(specList, "GroupingSpec") : [])
|
|
92
|
-
addBinding(spec, scope, analysis, moduleVarKeys, out);
|
|
96
|
+
addBinding(spec, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
93
97
|
break;
|
|
94
98
|
}
|
|
95
99
|
case "WindowClause": {
|
|
@@ -99,7 +103,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
99
103
|
if (b) {
|
|
100
104
|
// The "in" source expr is evaluated in the outer scope, before $w exists.
|
|
101
105
|
if (b.initExpr)
|
|
102
|
-
walk(b.initExpr, scope, analysis, moduleVarKeys, out);
|
|
106
|
+
walk(b.initExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
103
107
|
// Start/end condition vars ($current/$positional/$previous/$next) are visible
|
|
104
108
|
// across both conditions and the return clause — but $w itself is not yet bound
|
|
105
109
|
// while its own boundary conditions are being evaluated.
|
|
@@ -121,14 +125,14 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
121
125
|
}
|
|
122
126
|
const whenExpr = directChildOf(cond, "ExprSingle");
|
|
123
127
|
if (whenExpr)
|
|
124
|
-
walk(whenExpr, scope, analysis, moduleVarKeys, out);
|
|
128
|
+
walk(whenExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
125
129
|
}
|
|
126
130
|
scope.add(qnameKey(b.qname));
|
|
127
131
|
}
|
|
128
132
|
break;
|
|
129
133
|
}
|
|
130
134
|
default:
|
|
131
|
-
walk(clause, scope, analysis, moduleVarKeys, out);
|
|
135
|
+
walk(clause, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
scope.pop();
|
|
@@ -145,7 +149,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
145
149
|
scope.add(qnameKey(qname));
|
|
146
150
|
}
|
|
147
151
|
else {
|
|
148
|
-
walk(child, scope, analysis, moduleVarKeys, out);
|
|
152
|
+
walk(child, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
scope.pop();
|
|
@@ -159,7 +163,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
159
163
|
for (const p of fn.params)
|
|
160
164
|
scope.add(qnameKey(p.qname));
|
|
161
165
|
if (fn.body)
|
|
162
|
-
walk(fn.body, scope, analysis, moduleVarKeys, out);
|
|
166
|
+
walk(fn.body, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
163
167
|
scope.pop();
|
|
164
168
|
return;
|
|
165
169
|
}
|
|
@@ -169,7 +173,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
169
173
|
case "TypeswitchExpr": {
|
|
170
174
|
const operand = directChildOf(node, "Expr");
|
|
171
175
|
if (operand)
|
|
172
|
-
walk(operand, scope, analysis, moduleVarKeys, out);
|
|
176
|
+
walk(operand, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
173
177
|
for (const caseClause of directChildrenOf(node, "CaseClause")) {
|
|
174
178
|
scope.push();
|
|
175
179
|
const varNameNode = directChildOf(caseClause, "VarName");
|
|
@@ -178,7 +182,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
178
182
|
scope.add(qnameKey(qname));
|
|
179
183
|
const returnExpr = directChildOf(caseClause, "ExprSingle");
|
|
180
184
|
if (returnExpr)
|
|
181
|
-
walk(returnExpr, scope, analysis, moduleVarKeys, out);
|
|
185
|
+
walk(returnExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
182
186
|
scope.pop();
|
|
183
187
|
}
|
|
184
188
|
// `default $v return ...` — $v (optional) and the return expr are direct
|
|
@@ -190,7 +194,7 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
190
194
|
scope.add(qnameKey(defaultQname));
|
|
191
195
|
const defaultExpr = directChildOf(node, "ExprSingle");
|
|
192
196
|
if (defaultExpr)
|
|
193
|
-
walk(defaultExpr, scope, analysis, moduleVarKeys, out);
|
|
197
|
+
walk(defaultExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
194
198
|
scope.pop();
|
|
195
199
|
return;
|
|
196
200
|
}
|
|
@@ -200,25 +204,25 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
200
204
|
break;
|
|
201
205
|
scope.push();
|
|
202
206
|
for (const b of tx.copyBindings) {
|
|
203
|
-
walk(b.initExpr, scope, analysis, moduleVarKeys, out);
|
|
207
|
+
walk(b.initExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
204
208
|
scope.add(qnameKey(b.qname));
|
|
205
209
|
}
|
|
206
210
|
if (tx.modifyExpr)
|
|
207
|
-
walk(tx.modifyExpr, scope, analysis, moduleVarKeys, out);
|
|
211
|
+
walk(tx.modifyExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
208
212
|
if (tx.returnExpr)
|
|
209
|
-
walk(tx.returnExpr, scope, analysis, moduleVarKeys, out);
|
|
213
|
+
walk(tx.returnExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
210
214
|
scope.pop();
|
|
211
215
|
return;
|
|
212
216
|
}
|
|
213
217
|
case "VarRef": {
|
|
214
218
|
const qname = asVarRef(node, analysis);
|
|
215
|
-
if (qname && !isKnown(qname, scope, moduleVarKeys))
|
|
219
|
+
if (qname && !isKnown(qname, scope, moduleVarKeys, unresolvedImportUris))
|
|
216
220
|
reportVarRef(node, qname, out);
|
|
217
221
|
return;
|
|
218
222
|
}
|
|
219
223
|
default:
|
|
220
224
|
for (const child of children)
|
|
221
|
-
walk(child, scope, analysis, moduleVarKeys, out);
|
|
225
|
+
walk(child, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
// ── Public entry point ────────────────────────────────────────────────────────
|
|
@@ -227,6 +231,8 @@ function walk(node, scope, analysis, moduleVarKeys, out) {
|
|
|
227
231
|
* to a declared variable: function parameter, let/for binding, module-level
|
|
228
232
|
* `declare variable`, or an imported module variable.
|
|
229
233
|
*
|
|
234
|
+
* Variables whose prefix is bound by an import that did not resolve to an
|
|
235
|
+
* analysed module are skipped too (XQST0059 reports the import itself).
|
|
230
236
|
* Variables whose namespace prefix is undeclared are skipped — XQST0081
|
|
231
237
|
* already fires for those, and adding XPST0008 on top would be noisy.
|
|
232
238
|
* Variables inside catch clauses are also skipped since the implicit
|
|
@@ -239,7 +245,9 @@ export function checkUndeclaredVariables(ast, analysis, importedAnalyses) {
|
|
|
239
245
|
for (const imported of importedAnalyses.values())
|
|
240
246
|
for (const v of imported.moduleVariables)
|
|
241
247
|
moduleVarKeys.add(qnameKey(v.qname));
|
|
248
|
+
// Imports whose module was not analysed: their variables are unknowable.
|
|
249
|
+
const unresolvedImportUris = new Set(analysis.imports.map((i) => i.namespaceUri).filter((u) => !importedAnalyses.has(u)));
|
|
242
250
|
const out = [];
|
|
243
|
-
walk(ast, new ScopeStack(), analysis, moduleVarKeys, out);
|
|
251
|
+
walk(ast, new ScopeStack(), analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
244
252
|
return out;
|
|
245
253
|
}
|
package/package.json
CHANGED
|
@@ -41,3 +41,31 @@ export function checkModuleImportTargets(
|
|
|
41
41
|
}
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
export interface UnresolvedImportDiagnostic extends TypeDiagnostic {
|
|
46
|
+
code: "xq-lsp:unresolved-import";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Report imports whose module could not be loaded. Variable and function
|
|
51
|
+
* checks stay silent for such namespaces, so this is where the user learns
|
|
52
|
+
* why. Reported as a warning (not XQST0059) because the module may
|
|
53
|
+
* legitimately live outside the workspace or be generated at install time.
|
|
54
|
+
*/
|
|
55
|
+
export function checkUnresolvedImports(
|
|
56
|
+
analysis: FileAnalysis,
|
|
57
|
+
importedAnalyses: Map<string, FileAnalysis>,
|
|
58
|
+
): UnresolvedImportDiagnostic[] {
|
|
59
|
+
const out: UnresolvedImportDiagnostic[] = [];
|
|
60
|
+
for (const imp of analysis.imports) {
|
|
61
|
+
if (importedAnalyses.has(imp.atPath ?? imp.namespaceUri) || importedAnalyses.has(imp.namespaceUri)) continue;
|
|
62
|
+
const location = imp.atPath ? `at '${imp.atPath}'` : `for namespace '${imp.namespaceUri}'`;
|
|
63
|
+
out.push({
|
|
64
|
+
message: `Module ${location} could not be resolved; its functions and variables are not checked`,
|
|
65
|
+
code: "xq-lsp:unresolved-import",
|
|
66
|
+
offset: imp.offset,
|
|
67
|
+
length: imp.prefix.length,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
@@ -1,18 +1,96 @@
|
|
|
1
|
-
import type { Node } from "xq-parser";
|
|
1
|
+
import type { Node, NonTerminal, Terminal } from "xq-parser";
|
|
2
2
|
import type { FileAnalysis } from "./types.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
XMLNS_FN,
|
|
5
|
+
XMLNS_XS,
|
|
6
|
+
isTerminal,
|
|
7
|
+
directChildOf,
|
|
8
|
+
findAll,
|
|
9
|
+
firstTerminalValue,
|
|
10
|
+
parseEQName,
|
|
11
|
+
resolvePrefix,
|
|
12
|
+
} from "./analyzer.ts";
|
|
13
|
+
import { asFunctionCall } from "./ast-nodes.ts";
|
|
4
14
|
|
|
5
15
|
export type NamespaceUsageKind = "function" | "variable" | "element";
|
|
6
16
|
|
|
7
17
|
export interface NamespaceDiagnostic {
|
|
8
18
|
message: string;
|
|
9
|
-
code: "XQST0081";
|
|
19
|
+
code: "XQST0081" | "FONS0004";
|
|
10
20
|
offset: number; // offset of the prefix start in source
|
|
11
21
|
length: number; // length of the prefix (not including the colon)
|
|
12
22
|
prefix: string;
|
|
13
23
|
usageKind: NamespaceUsageKind;
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
/** Unwraps single-child grammar wrappers down to a StringLiteral terminal; null for anything more complex. */
|
|
27
|
+
function asStringLiteral(node: Node | undefined): Terminal | null {
|
|
28
|
+
let cur: Node | undefined = node;
|
|
29
|
+
while (cur && !isTerminal(cur)) {
|
|
30
|
+
const children = (cur as NonTerminal).children;
|
|
31
|
+
if (children.length !== 1) return null;
|
|
32
|
+
cur = children[0];
|
|
33
|
+
}
|
|
34
|
+
return cur && cur.type === "StringLiteral" ? (cur as Terminal) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DynamicPrefixUse {
|
|
38
|
+
/** The prefix text. */
|
|
39
|
+
prefix: string;
|
|
40
|
+
/** Source offset of the prefix (inside the string literal). */
|
|
41
|
+
offset: number;
|
|
42
|
+
/** True when an unbound prefix is an error (xs:QName / xs:NOTATION); false for namespace-uri-for-prefix, which just returns (). */
|
|
43
|
+
required: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prefixes taken from a string literal and resolved against the static
|
|
48
|
+
* in-scope namespaces at runtime: `xs:QName("p:l")`, `"p:l" cast as xs:QName`
|
|
49
|
+
* (also `castable as`, `xs:NOTATION`) and `fn:namespace-uri-for-prefix("p", …)`.
|
|
50
|
+
* Only literal operands are detected.
|
|
51
|
+
*/
|
|
52
|
+
export function findDynamicPrefixUses(ast: Node, analysis: FileAnalysis): DynamicPrefixUse[] {
|
|
53
|
+
const out: DynamicPrefixUse[] = [];
|
|
54
|
+
const fromQNameLiteral = (lit: Terminal | null, required: boolean): void => {
|
|
55
|
+
if (!lit) return;
|
|
56
|
+
const text = lit.value.slice(1, -1);
|
|
57
|
+
const trimmedStart = text.length - text.trimStart().length;
|
|
58
|
+
const t = text.trim();
|
|
59
|
+
const c = t.indexOf(":");
|
|
60
|
+
if (c <= 0) return;
|
|
61
|
+
out.push({ prefix: t.slice(0, c), offset: lit.start + 1 + trimmedStart, required });
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const call of findAll(ast, "FunctionCall")) {
|
|
65
|
+
const fc = asFunctionCall(call, analysis);
|
|
66
|
+
if (!fc) continue;
|
|
67
|
+
const { namespaceUri, localName } = fc.qname;
|
|
68
|
+
const lit = asStringLiteral(fc.args[0]);
|
|
69
|
+
if (!lit) continue;
|
|
70
|
+
if (namespaceUri === XMLNS_XS && (localName === "QName" || localName === "NOTATION")) {
|
|
71
|
+
fromQNameLiteral(lit, true);
|
|
72
|
+
} else if (namespaceUri === XMLNS_FN && localName === "namespace-uri-for-prefix") {
|
|
73
|
+
const prefix = lit.value.slice(1, -1);
|
|
74
|
+
if (prefix.length > 0) out.push({ prefix, offset: lit.start + 1, required: false });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// castable as never raises an error for an unbound prefix; it just yields false.
|
|
79
|
+
for (const type of ["CastExpr", "CastableExpr"]) {
|
|
80
|
+
for (const node of findAll(ast, type)) {
|
|
81
|
+
const single = directChildOf(node, "SingleType");
|
|
82
|
+
const typeNode = single && findAll(single, "QName")[0];
|
|
83
|
+
const typeName = typeNode && firstTerminalValue(typeNode);
|
|
84
|
+
if (!typeName) continue;
|
|
85
|
+
const { prefix, localName, uri } = parseEQName(typeName);
|
|
86
|
+
if ((uri ?? resolvePrefix(prefix, analysis)) !== XMLNS_XS) continue;
|
|
87
|
+
if (localName !== "QName" && localName !== "NOTATION") continue;
|
|
88
|
+
fromQNameLiteral(asStringLiteral((node as NonTerminal).children[0]), type === "CastExpr");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
16
94
|
function checkQName(
|
|
17
95
|
node: Node | undefined,
|
|
18
96
|
kind: NamespaceUsageKind,
|
|
@@ -40,6 +118,19 @@ function checkQName(
|
|
|
40
118
|
}
|
|
41
119
|
}
|
|
42
120
|
|
|
121
|
+
function inlineXmlnsPrefixes(elem: Node): Set<string> {
|
|
122
|
+
const local = new Set<string>();
|
|
123
|
+
const attrList = directChildOf(elem, "DirAttributeList");
|
|
124
|
+
if (attrList && !isTerminal(attrList)) {
|
|
125
|
+
for (const c of (attrList as { children: Node[] }).children) {
|
|
126
|
+
if (c.type !== "QName") continue;
|
|
127
|
+
const attrName = firstTerminalValue(c);
|
|
128
|
+
if (attrName?.startsWith("xmlns:")) local.add(attrName.slice(6));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return local;
|
|
132
|
+
}
|
|
133
|
+
|
|
43
134
|
function walk(
|
|
44
135
|
node: Node,
|
|
45
136
|
analysis: FileAnalysis,
|
|
@@ -52,15 +143,7 @@ function walk(
|
|
|
52
143
|
if (node.type === "DirElemConstructor") {
|
|
53
144
|
// Collect xmlns:prefix declarations from this element's attribute list,
|
|
54
145
|
// then push them onto the scope stack for the duration of this subtree.
|
|
55
|
-
const local =
|
|
56
|
-
const attrList = directChildOf(node, "DirAttributeList");
|
|
57
|
-
if (attrList && !isTerminal(attrList)) {
|
|
58
|
-
for (const c of (attrList as { children: Node[] }).children) {
|
|
59
|
-
if (c.type !== "QName") continue;
|
|
60
|
-
const attrName = firstTerminalValue(c);
|
|
61
|
-
if (attrName?.startsWith("xmlns:")) local.add(attrName.slice(6));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
146
|
+
const local = inlineXmlnsPrefixes(node);
|
|
64
147
|
inlineStack.push(local);
|
|
65
148
|
checkQName(directChildOf(node, "QName"), "element", analysis, inlineStack, out);
|
|
66
149
|
for (const c of children) walk(c, analysis, inlineStack, out);
|
|
@@ -100,6 +183,27 @@ export function findUndeclaredPrefixUsages(
|
|
|
100
183
|
if (!ast) return [];
|
|
101
184
|
const out: NamespaceDiagnostic[] = [];
|
|
102
185
|
walk(ast, analysis, [], out);
|
|
186
|
+
|
|
187
|
+
// Prefixes inside string literals resolve against the in-scope namespaces, which
|
|
188
|
+
// include xmlns:prefix declarations of enclosing direct element constructors.
|
|
189
|
+
const dirElems = findAll(ast, "DirElemConstructor").map((el) => ({
|
|
190
|
+
start: el.start ?? 0,
|
|
191
|
+
end: el.end ?? 0,
|
|
192
|
+
prefixes: inlineXmlnsPrefixes(el),
|
|
193
|
+
}));
|
|
194
|
+
for (const use of findDynamicPrefixUses(ast, analysis)) {
|
|
195
|
+
if (!use.required) continue;
|
|
196
|
+
if (dirElems.some((el) => el.start <= use.offset && use.offset < el.end && el.prefixes.has(use.prefix))) continue;
|
|
197
|
+
if (!resolvePrefix(use.prefix, analysis).startsWith("urn:xq-lsp:undeclared:")) continue;
|
|
198
|
+
out.push({
|
|
199
|
+
message: `Namespace prefix '${use.prefix}' is not declared`,
|
|
200
|
+
code: "FONS0004",
|
|
201
|
+
offset: use.offset,
|
|
202
|
+
length: use.prefix.length,
|
|
203
|
+
prefix: use.prefix,
|
|
204
|
+
usageKind: "element",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
103
207
|
return out;
|
|
104
208
|
}
|
|
105
209
|
|
package/src/server.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { findImportInsertPosition, findDeclareNsInsertPosition, computeRelativeP
|
|
|
35
35
|
import type { NamespaceUsageKind } from "./namespace-diagnostics.ts";
|
|
36
36
|
import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.ts";
|
|
37
37
|
import { runDiagnostics, runHints } from "./diagnostics.ts";
|
|
38
|
+
import { checkUnresolvedImports } from "./module-import-diagnostics.ts";
|
|
38
39
|
import {
|
|
39
40
|
getRuntimeAnalyses,
|
|
40
41
|
getRuntimePredeclaredNamespaces,
|
|
@@ -327,6 +328,8 @@ function computeDiagnostics(doc: TextDocument) {
|
|
|
327
328
|
const errorDiagRaw = hasAst && ast !== null ? runDiagnostics(ast, doc.getText(), analysis, resolvedImported) : [];
|
|
328
329
|
const hintDiagRaw = hasAst && ast !== null ? runHints(ast, analysis) : [];
|
|
329
330
|
|
|
331
|
+
const unresolvedImportRaw = hasAst ? checkUnresolvedImports(analysis, resolvedImported) : [];
|
|
332
|
+
|
|
330
333
|
function toLsp(d: TypeDiagnostic, severity: DiagnosticSeverity) {
|
|
331
334
|
return {
|
|
332
335
|
severity,
|
|
@@ -345,7 +348,9 @@ function computeDiagnostics(doc: TextDocument) {
|
|
|
345
348
|
const errorDiags = errorDiagRaw.filter((d) => d.code !== "XQST0081").map((d) => toLsp(d, DiagnosticSeverity.Error));
|
|
346
349
|
const hintDiags = hintDiagRaw.map((d) => toLsp(d, DiagnosticSeverity.Hint));
|
|
347
350
|
|
|
348
|
-
|
|
351
|
+
const unresolvedImportDiags = unresolvedImportRaw.map((d) => toLsp(d, DiagnosticSeverity.Warning));
|
|
352
|
+
|
|
353
|
+
return [...parseDiags, ...nsDiags, ...errorDiags, ...unresolvedImportDiags, ...hintDiags];
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
documents.onDidChangeContent((change) => {
|
|
@@ -588,7 +593,7 @@ connection.onCodeAction((params) => {
|
|
|
588
593
|
const only = params.context.only as CodeActionKind[] | undefined;
|
|
589
594
|
|
|
590
595
|
for (const diag of params.context.diagnostics) {
|
|
591
|
-
if (diag.code !== "XQST0081") continue;
|
|
596
|
+
if (diag.code !== "XQST0081" && diag.code !== "FONS0004") continue;
|
|
592
597
|
const data = diag.data as { prefix: string; usageKind: NamespaceUsageKind } | undefined;
|
|
593
598
|
if (!data?.prefix) continue;
|
|
594
599
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Node, NonTerminal } from "xq-parser";
|
|
2
2
|
import type { FileAnalysis } from "./types.ts";
|
|
3
3
|
import { qnameKey, formatQName } from "./types.ts";
|
|
4
|
+
import { findDynamicPrefixUses } from "./namespace-diagnostics.ts";
|
|
4
5
|
import { isTerminal, directChildOf, directChildrenOf, findAll, firstTerminalValue, parseEQName, resolvePrefix } from "./analyzer.ts";
|
|
5
6
|
import {
|
|
6
7
|
asFunctionCall,
|
|
@@ -198,7 +199,7 @@ function walk(
|
|
|
198
199
|
* two terminal types catches every usage site without enumerating each
|
|
199
200
|
* grammar production that can carry a prefix.
|
|
200
201
|
*/
|
|
201
|
-
function collectUsedPrefixes(ast: Node): Set<string> {
|
|
202
|
+
function collectUsedPrefixes(ast: Node, analysis: FileAnalysis): Set<string> {
|
|
202
203
|
const used = new Set<string>();
|
|
203
204
|
|
|
204
205
|
for (const node of findAll(ast, "QName")) {
|
|
@@ -217,9 +218,15 @@ function collectUsedPrefixes(ast: Node): Set<string> {
|
|
|
217
218
|
if (prefix !== "*") used.add(prefix);
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
collectDynamicPrefixUses(ast, analysis, used);
|
|
222
|
+
|
|
220
223
|
return used;
|
|
221
224
|
}
|
|
222
225
|
|
|
226
|
+
function collectDynamicPrefixUses(ast: Node, analysis: FileAnalysis, used: Set<string>): void {
|
|
227
|
+
for (const use of findDynamicPrefixUses(ast, analysis)) used.add(use.prefix);
|
|
228
|
+
}
|
|
229
|
+
|
|
223
230
|
// ── Public entry point ────────────────────────────────────────────────────────
|
|
224
231
|
|
|
225
232
|
/**
|
|
@@ -278,7 +285,7 @@ export function checkUnused(ast: Node, analysis: FileAnalysis): UnusedDiagnostic
|
|
|
278
285
|
|
|
279
286
|
// ── Check imports and namespace declarations ─────────────────────────────
|
|
280
287
|
|
|
281
|
-
const usedPrefixes = collectUsedPrefixes(ast);
|
|
288
|
+
const usedPrefixes = collectUsedPrefixes(ast, analysis);
|
|
282
289
|
|
|
283
290
|
for (const imp of analysis.imports) {
|
|
284
291
|
if (usedPrefixes.has(imp.prefix)) continue;
|
|
@@ -23,9 +23,12 @@ class ScopeStack {
|
|
|
23
23
|
|
|
24
24
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
25
25
|
|
|
26
|
-
function isKnown(qname: QName, scope: ScopeStack, moduleVarKeys: Set<string>): boolean {
|
|
26
|
+
function isKnown(qname: QName, scope: ScopeStack, moduleVarKeys: Set<string>, unresolvedImportUris: Set<string>): boolean {
|
|
27
27
|
// Variables with an undeclared-prefix URI are already caught by XQST0081; skip here.
|
|
28
28
|
if (qname.namespaceUri.startsWith("urn:xq-lsp:undeclared:")) return true;
|
|
29
|
+
// The import exists but its module was not analysed (outside the workspace,
|
|
30
|
+
// generated at install time, …): we cannot know what it declares, so stay silent.
|
|
31
|
+
if (unresolvedImportUris.has(qname.namespaceUri)) return true;
|
|
29
32
|
return scope.has(qnameKey(qname)) || moduleVarKeys.has(qnameKey(qname));
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -44,11 +47,12 @@ function addBinding(
|
|
|
44
47
|
scope: ScopeStack,
|
|
45
48
|
analysis: FileAnalysis,
|
|
46
49
|
moduleVarKeys: Set<string>,
|
|
50
|
+
unresolvedImportUris: Set<string>,
|
|
47
51
|
out: UndeclaredVariableDiagnostic[],
|
|
48
52
|
): void {
|
|
49
53
|
const b = asBinding(bindingNode, analysis);
|
|
50
54
|
if (!b) return;
|
|
51
|
-
if (b.initExpr) walk(b.initExpr, scope, analysis, moduleVarKeys, out);
|
|
55
|
+
if (b.initExpr) walk(b.initExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
52
56
|
scope.add(qnameKey(b.qname));
|
|
53
57
|
if (b.positionalVar) scope.add(qnameKey(b.positionalVar.qname));
|
|
54
58
|
}
|
|
@@ -60,6 +64,7 @@ function walk(
|
|
|
60
64
|
scope: ScopeStack,
|
|
61
65
|
analysis: FileAnalysis,
|
|
62
66
|
moduleVarKeys: Set<string>,
|
|
67
|
+
unresolvedImportUris: Set<string>,
|
|
63
68
|
out: UndeclaredVariableDiagnostic[],
|
|
64
69
|
): void {
|
|
65
70
|
if (isTerminal(node)) return;
|
|
@@ -72,7 +77,7 @@ function walk(
|
|
|
72
77
|
if (fn) {
|
|
73
78
|
scope.push();
|
|
74
79
|
for (const p of fn.params) scope.add(qnameKey(p.qname));
|
|
75
|
-
if (fn.body) walk(fn.body, scope, analysis, moduleVarKeys, out);
|
|
80
|
+
if (fn.body) walk(fn.body, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
76
81
|
scope.pop();
|
|
77
82
|
return;
|
|
78
83
|
}
|
|
@@ -80,7 +85,7 @@ function walk(
|
|
|
80
85
|
const varDecl = directChildOf(node, "VarDecl");
|
|
81
86
|
if (varDecl && !isTerminal(varDecl)) {
|
|
82
87
|
for (const child of (varDecl as NonTerminal).children)
|
|
83
|
-
walk(child, scope, analysis, moduleVarKeys, out);
|
|
88
|
+
walk(child, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
84
89
|
}
|
|
85
90
|
return;
|
|
86
91
|
}
|
|
@@ -99,19 +104,19 @@ function walk(
|
|
|
99
104
|
switch (clause.type) {
|
|
100
105
|
case "ForClause":
|
|
101
106
|
for (const b of directChildrenOf(clause, "ForBinding"))
|
|
102
|
-
addBinding(b, scope, analysis, moduleVarKeys, out);
|
|
107
|
+
addBinding(b, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
103
108
|
break;
|
|
104
109
|
case "LetClause":
|
|
105
110
|
for (const b of directChildrenOf(clause, "LetBinding"))
|
|
106
|
-
addBinding(b, scope, analysis, moduleVarKeys, out);
|
|
111
|
+
addBinding(b, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
107
112
|
break;
|
|
108
113
|
case "CountClause":
|
|
109
|
-
addBinding(clause, scope, analysis, moduleVarKeys, out);
|
|
114
|
+
addBinding(clause, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
110
115
|
break;
|
|
111
116
|
case "GroupByClause": {
|
|
112
117
|
const specList = directChildOf(clause, "GroupingSpecList");
|
|
113
118
|
for (const spec of specList ? directChildrenOf(specList, "GroupingSpec") : [])
|
|
114
|
-
addBinding(spec, scope, analysis, moduleVarKeys, out);
|
|
119
|
+
addBinding(spec, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
115
120
|
break;
|
|
116
121
|
}
|
|
117
122
|
case "WindowClause": {
|
|
@@ -121,7 +126,7 @@ function walk(
|
|
|
121
126
|
const b = inner ? asBinding(inner, analysis) : null;
|
|
122
127
|
if (b) {
|
|
123
128
|
// The "in" source expr is evaluated in the outer scope, before $w exists.
|
|
124
|
-
if (b.initExpr) walk(b.initExpr, scope, analysis, moduleVarKeys, out);
|
|
129
|
+
if (b.initExpr) walk(b.initExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
125
130
|
// Start/end condition vars ($current/$positional/$previous/$next) are visible
|
|
126
131
|
// across both conditions and the return clause — but $w itself is not yet bound
|
|
127
132
|
// while its own boundary conditions are being evaluated.
|
|
@@ -137,14 +142,14 @@ function walk(
|
|
|
137
142
|
if (wv.nextItem) scope.add(qnameKey(wv.nextItem));
|
|
138
143
|
}
|
|
139
144
|
const whenExpr = directChildOf(cond, "ExprSingle");
|
|
140
|
-
if (whenExpr) walk(whenExpr, scope, analysis, moduleVarKeys, out);
|
|
145
|
+
if (whenExpr) walk(whenExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
141
146
|
}
|
|
142
147
|
scope.add(qnameKey(b.qname));
|
|
143
148
|
}
|
|
144
149
|
break;
|
|
145
150
|
}
|
|
146
151
|
default:
|
|
147
|
-
walk(clause, scope, analysis, moduleVarKeys, out);
|
|
152
|
+
walk(clause, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
scope.pop();
|
|
@@ -159,7 +164,7 @@ function walk(
|
|
|
159
164
|
const qname = asVarName(child, analysis);
|
|
160
165
|
if (qname) scope.add(qnameKey(qname));
|
|
161
166
|
} else {
|
|
162
|
-
walk(child, scope, analysis, moduleVarKeys, out);
|
|
167
|
+
walk(child, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
scope.pop();
|
|
@@ -171,7 +176,7 @@ function walk(
|
|
|
171
176
|
if (!fn) break;
|
|
172
177
|
scope.push();
|
|
173
178
|
for (const p of fn.params) scope.add(qnameKey(p.qname));
|
|
174
|
-
if (fn.body) walk(fn.body, scope, analysis, moduleVarKeys, out);
|
|
179
|
+
if (fn.body) walk(fn.body, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
175
180
|
scope.pop();
|
|
176
181
|
return;
|
|
177
182
|
}
|
|
@@ -182,14 +187,14 @@ function walk(
|
|
|
182
187
|
|
|
183
188
|
case "TypeswitchExpr": {
|
|
184
189
|
const operand = directChildOf(node, "Expr");
|
|
185
|
-
if (operand) walk(operand, scope, analysis, moduleVarKeys, out);
|
|
190
|
+
if (operand) walk(operand, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
186
191
|
for (const caseClause of directChildrenOf(node, "CaseClause")) {
|
|
187
192
|
scope.push();
|
|
188
193
|
const varNameNode = directChildOf(caseClause, "VarName");
|
|
189
194
|
const qname = varNameNode ? asVarName(varNameNode, analysis) : null;
|
|
190
195
|
if (qname) scope.add(qnameKey(qname));
|
|
191
196
|
const returnExpr = directChildOf(caseClause, "ExprSingle");
|
|
192
|
-
if (returnExpr) walk(returnExpr, scope, analysis, moduleVarKeys, out);
|
|
197
|
+
if (returnExpr) walk(returnExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
193
198
|
scope.pop();
|
|
194
199
|
}
|
|
195
200
|
// `default $v return ...` — $v (optional) and the return expr are direct
|
|
@@ -199,7 +204,7 @@ function walk(
|
|
|
199
204
|
const defaultQname = defaultVarNode ? asVarName(defaultVarNode, analysis) : null;
|
|
200
205
|
if (defaultQname) scope.add(qnameKey(defaultQname));
|
|
201
206
|
const defaultExpr = directChildOf(node, "ExprSingle");
|
|
202
|
-
if (defaultExpr) walk(defaultExpr, scope, analysis, moduleVarKeys, out);
|
|
207
|
+
if (defaultExpr) walk(defaultExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
203
208
|
scope.pop();
|
|
204
209
|
return;
|
|
205
210
|
}
|
|
@@ -209,24 +214,24 @@ function walk(
|
|
|
209
214
|
if (!tx) break;
|
|
210
215
|
scope.push();
|
|
211
216
|
for (const b of tx.copyBindings) {
|
|
212
|
-
walk(b.initExpr, scope, analysis, moduleVarKeys, out);
|
|
217
|
+
walk(b.initExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
213
218
|
scope.add(qnameKey(b.qname));
|
|
214
219
|
}
|
|
215
|
-
if (tx.modifyExpr) walk(tx.modifyExpr, scope, analysis, moduleVarKeys, out);
|
|
216
|
-
if (tx.returnExpr) walk(tx.returnExpr, scope, analysis, moduleVarKeys, out);
|
|
220
|
+
if (tx.modifyExpr) walk(tx.modifyExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
221
|
+
if (tx.returnExpr) walk(tx.returnExpr, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
217
222
|
scope.pop();
|
|
218
223
|
return;
|
|
219
224
|
}
|
|
220
225
|
|
|
221
226
|
case "VarRef": {
|
|
222
227
|
const qname = asVarRef(node, analysis);
|
|
223
|
-
if (qname && !isKnown(qname, scope, moduleVarKeys))
|
|
228
|
+
if (qname && !isKnown(qname, scope, moduleVarKeys, unresolvedImportUris))
|
|
224
229
|
reportVarRef(node, qname, out);
|
|
225
230
|
return;
|
|
226
231
|
}
|
|
227
232
|
|
|
228
233
|
default:
|
|
229
|
-
for (const child of children) walk(child, scope, analysis, moduleVarKeys, out);
|
|
234
|
+
for (const child of children) walk(child, scope, analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
|
|
@@ -237,6 +242,8 @@ function walk(
|
|
|
237
242
|
* to a declared variable: function parameter, let/for binding, module-level
|
|
238
243
|
* `declare variable`, or an imported module variable.
|
|
239
244
|
*
|
|
245
|
+
* Variables whose prefix is bound by an import that did not resolve to an
|
|
246
|
+
* analysed module are skipped too (XQST0059 reports the import itself).
|
|
240
247
|
* Variables whose namespace prefix is undeclared are skipped — XQST0081
|
|
241
248
|
* already fires for those, and adding XPST0008 on top would be noisy.
|
|
242
249
|
* Variables inside catch clauses are also skipped since the implicit
|
|
@@ -252,7 +259,12 @@ export function checkUndeclaredVariables(
|
|
|
252
259
|
for (const imported of importedAnalyses.values())
|
|
253
260
|
for (const v of imported.moduleVariables) moduleVarKeys.add(qnameKey(v.qname));
|
|
254
261
|
|
|
262
|
+
// Imports whose module was not analysed: their variables are unknowable.
|
|
263
|
+
const unresolvedImportUris = new Set(
|
|
264
|
+
analysis.imports.map((i) => i.namespaceUri).filter((u) => !importedAnalyses.has(u)),
|
|
265
|
+
);
|
|
266
|
+
|
|
255
267
|
const out: UndeclaredVariableDiagnostic[] = [];
|
|
256
|
-
walk(ast, new ScopeStack(), analysis, moduleVarKeys, out);
|
|
268
|
+
walk(ast, new ScopeStack(), analysis, moduleVarKeys, unresolvedImportUris, out);
|
|
257
269
|
return out;
|
|
258
270
|
}
|