xq-lsp 1.8.3-next.430 → 1.8.3-next.437
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/namespace-diagnostics.d.ts +16 -1
- package/dist/namespace-diagnostics.js +105 -12
- package/dist/server.js +1 -1
- package/dist/unused-diagnostics.js +8 -2
- package/package.json +1 -1
- package/src/namespace-diagnostics.ts +116 -12
- package/src/server.ts +1 -1
- package/src/unused-diagnostics.ts +9 -2
|
@@ -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
|
@@ -524,7 +524,7 @@ connection.onCodeAction((params) => {
|
|
|
524
524
|
const actions = [];
|
|
525
525
|
const only = params.context.only;
|
|
526
526
|
for (const diag of params.context.diagnostics) {
|
|
527
|
-
if (diag.code !== "XQST0081")
|
|
527
|
+
if (diag.code !== "XQST0081" && diag.code !== "FONS0004")
|
|
528
528
|
continue;
|
|
529
529
|
const data = diag.data;
|
|
530
530
|
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;
|
package/package.json
CHANGED
|
@@ -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
|
@@ -588,7 +588,7 @@ connection.onCodeAction((params) => {
|
|
|
588
588
|
const only = params.context.only as CodeActionKind[] | undefined;
|
|
589
589
|
|
|
590
590
|
for (const diag of params.context.diagnostics) {
|
|
591
|
-
if (diag.code !== "XQST0081") continue;
|
|
591
|
+
if (diag.code !== "XQST0081" && diag.code !== "FONS0004") continue;
|
|
592
592
|
const data = diag.data as { prefix: string; usageKind: NamespaceUsageKind } | undefined;
|
|
593
593
|
if (!data?.prefix) continue;
|
|
594
594
|
|
|
@@ -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;
|