• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DrRataplan / xq-lsp / 29053750979

09 Jul 2026 10:10PM UTC coverage: 93.052% (-0.1%) from 93.182%
29053750979

Pull #75

github

web-flow
Merge 7cebb91e7 into 6d44620e5
Pull Request #75: fix: rewrite imported variable prefix to the importing module's prefi…

1099 of 1281 branches covered (85.79%)

Branch coverage included in aggregate %.

292 of 312 new or added lines in 8 files covered. (93.59%)

12 existing lines in 2 files now uncovered.

3977 of 4174 relevant lines covered (95.28%)

431659.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.87
/src/variable-diagnostics.ts
1
import type { Node, NonTerminal } from "xq-parser";
5✔
2
import type { FileAnalysis, QName } from "./types.ts";
5✔
3
import { qnameKey } from "./types.ts";
5✔
4
import { isTerminal, directChildOf, directChildrenOf, firstTerminalValue, parseEQName, resolvePrefix } from "./analyzer.ts";
5✔
5
import { asFunctionDecl, asVarRef, asVarName, asBinding, asInlineFunctionExpr, asWindowVars, asTransformExpr } from "./ast-nodes.ts";
5✔
6

5✔
7
export interface UndeclaredVariableDiagnostic {
5✔
8
        message: string;
5✔
9
        code: "XPST0008";
5✔
10
        offset: number;
5✔
11
        length: number;
5✔
12
}
5✔
13

5✔
14
// ── Scope stack ───────────────────────────────────────────────────────────────
5✔
15

5✔
16
class ScopeStack {
5✔
17
        private frames: Array<Set<string>> = [];
5✔
18
        push(): void { this.frames.push(new Set()); }
5✔
19
        pop(): void { this.frames.pop(); }
5✔
20
        add(key: string): void { this.frames[this.frames.length - 1]?.add(key); }
5✔
21
        has(key: string): boolean { return this.frames.some((f) => f.has(key)); }
5✔
22
}
5✔
23

5✔
24
// ── Helpers ───────────────────────────────────────────────────────────────────
5✔
25

5✔
26
function isKnown(qname: QName, scope: ScopeStack, moduleVarKeys: Set<string>): boolean {
21,475✔
27
        // Variables with an undeclared-prefix URI are already caught by XQST0081; skip here.
21,475✔
28
        if (qname.namespaceUri.startsWith("urn:xq-lsp:undeclared:")) return true;
21,475✔
29
        return scope.has(qnameKey(qname)) || moduleVarKeys.has(qnameKey(qname));
21,475✔
30
}
21,475✔
31

5✔
32
function reportVarRef(node: Node, qname: QName, out: UndeclaredVariableDiagnostic[]): void {
142✔
33
        const name = qname.prefix ? `${qname.prefix}:${qname.localName}` : qname.localName;
142✔
34
        out.push({
142✔
35
                message: `Variable '$${name}' is not declared`,
142✔
36
                code: "XPST0008",
142✔
37
                offset: node.start ?? 0,
142!
38
                length: name.length + 1, // +1 for "$"
142✔
39
        });
142✔
40
}
142✔
41

5✔
42
function addBinding(
5,990✔
43
        bindingNode: Node,
5,990✔
44
        scope: ScopeStack,
5,990✔
45
        analysis: FileAnalysis,
5,990✔
46
        moduleVarKeys: Set<string>,
5,990✔
47
        out: UndeclaredVariableDiagnostic[],
5,990✔
48
): void {
5,990✔
49
        const b = asBinding(bindingNode, analysis);
5,990✔
50
        if (!b) return;
5,990✔
51
        if (b.initExpr) walk(b.initExpr, scope, analysis, moduleVarKeys, out);
5,990✔
52
        scope.add(qnameKey(b.qname));
4,245✔
53
        if (b.positionalVar) scope.add(qnameKey(b.positionalVar.qname));
5,990✔
54
}
5,990✔
55

5✔
56
// ── Scope-aware AST walker ────────────────────────────────────────────────────
5✔
57

5✔
58
function walk(
5,788,227✔
59
        node: Node,
5,788,227✔
60
        scope: ScopeStack,
5,788,227✔
61
        analysis: FileAnalysis,
5,788,227✔
62
        moduleVarKeys: Set<string>,
5,788,227✔
63
        out: UndeclaredVariableDiagnostic[],
5,788,227✔
64
): void {
5,788,227✔
65
        if (isTerminal(node)) return;
5,788,227✔
66
        const { children } = node as NonTerminal;
5,136,248✔
67

5,136,248✔
68
        switch (node.type) {
5,136,248✔
69
                case "AnnotatedDecl": {
5,788,227✔
70
                        const fnDeclNode = directChildOf(node, "FunctionDecl");
4,553✔
71
                        const fn = fnDeclNode ? asFunctionDecl(fnDeclNode, analysis) : null;
4,553✔
72
                        if (fn) {
4,553✔
73
                                scope.push();
2,848✔
74
                                for (const p of fn.params) scope.add(qnameKey(p.qname));
2,848✔
75
                                if (fn.body) walk(fn.body, scope, analysis, moduleVarKeys, out);
2,848✔
76
                                scope.pop();
2,848✔
77
                                return;
2,848✔
78
                        }
2,848✔
79
                        // Module variable declaration: walk the initializer expression only
1,705✔
80
                        const varDecl = directChildOf(node, "VarDecl");
1,705✔
81
                        if (varDecl && !isTerminal(varDecl)) {
4,553✔
82
                                for (const child of (varDecl as NonTerminal).children)
1,705✔
83
                                        walk(child, scope, analysis, moduleVarKeys, out);
1,705✔
84
                        }
1,705✔
85
                        return;
1,705✔
86
                }
1,705✔
87

5,788,227✔
88
                case "FLWORExpr": {
5,788,227✔
89
                        scope.push();
4,697✔
90
                        for (const child of children) {
4,697✔
91
                                if (isTerminal(child)) continue;
11,027!
92
                                // Unwrap InitialClause/IntermediateClause wrappers (may be multiply nested)
11,027✔
93
                                let clause: Node = child;
11,027✔
94
                                while (!isTerminal(clause) && (clause.type === "InitialClause" || clause.type === "IntermediateClause")) {
11,027✔
95
                                        const inner: Node | undefined = (clause as NonTerminal).children.find((c) => !isTerminal(c));
7,281✔
96
                                        if (!inner) break;
7,281!
97
                                        clause = inner;
7,281✔
98
                                }
7,281✔
99
                                switch (clause.type) {
11,027✔
100
                                        case "ForClause":
11,027✔
101
                                                for (const b of directChildrenOf(clause, "ForBinding"))
2,191✔
102
                                                        addBinding(b, scope, analysis, moduleVarKeys, out);
2,191✔
103
                                                break;
2,191✔
104
                                        case "LetClause":
11,027✔
105
                                                for (const b of directChildrenOf(clause, "LetBinding"))
3,323✔
106
                                                        addBinding(b, scope, analysis, moduleVarKeys, out);
3,323✔
107
                                                break;
3,323✔
108
                                        case "CountClause":
11,027✔
109
                                                addBinding(clause, scope, analysis, moduleVarKeys, out);
25✔
110
                                                break;
25✔
111
                                        case "GroupByClause": {
11,027✔
112
                                                const specList = directChildOf(clause, "GroupingSpecList");
69✔
113
                                                for (const spec of specList ? directChildrenOf(specList, "GroupingSpec") : [])
69✔
114
                                                        addBinding(spec, scope, analysis, moduleVarKeys, out);
69✔
115
                                                break;
69✔
116
                                        }
69✔
117
                                        case "WindowClause": {
11,027✔
118
                                                const inner =
134✔
119
                                                        directChildOf(clause, "TumblingWindowClause") ??
134✔
120
                                                        directChildOf(clause, "SlidingWindowClause");
134✔
121
                                                const b = inner ? asBinding(inner, analysis) : null;
134✔
122
                                                if (b) {
134✔
123
                                                        // The "in" source expr is evaluated in the outer scope, before $w exists.
134✔
124
                                                        if (b.initExpr) walk(b.initExpr, scope, analysis, moduleVarKeys, out);
134✔
125
                                                        // Start/end condition vars ($current/$positional/$previous/$next) are visible
134✔
126
                                                        // across both conditions and the return clause — but $w itself is not yet bound
134✔
127
                                                        // while its own boundary conditions are being evaluated.
134✔
128
                                                        for (const condType of ["WindowStartCondition", "WindowEndCondition"] as const) {
134✔
129
                                                                const cond = inner ? directChildOf(inner, condType) : undefined;
268✔
130
                                                                if (!cond) continue;
268✔
131
                                                                const windowVarsNode = directChildOf(cond, "WindowVars");
263✔
132
                                                                const wv = windowVarsNode ? asWindowVars(windowVarsNode, analysis) : null;
268✔
133
                                                                if (wv) {
268✔
134
                                                                        if (wv.currentItem) scope.add(qnameKey(wv.currentItem));
263✔
135
                                                                        if (wv.positionalVar) scope.add(qnameKey(wv.positionalVar.qname));
263✔
136
                                                                        if (wv.previousItem) scope.add(qnameKey(wv.previousItem));
263✔
137
                                                                        if (wv.nextItem) scope.add(qnameKey(wv.nextItem));
263✔
138
                                                                }
263✔
139
                                                                const whenExpr = directChildOf(cond, "ExprSingle");
263✔
140
                                                                if (whenExpr) walk(whenExpr, scope, analysis, moduleVarKeys, out);
263✔
141
                                                        }
268✔
142
                                                        scope.add(qnameKey(b.qname));
134✔
143
                                                }
134✔
144
                                                break;
134✔
145
                                        }
134✔
146
                                        default:
11,027✔
147
                                                walk(clause, scope, analysis, moduleVarKeys, out);
5,285✔
148
                                }
11,027✔
149
                        }
11,027✔
150
                        scope.pop();
4,697✔
151
                        return;
4,697✔
152
                }
4,697✔
153

5,788,227✔
154
                case "QuantifiedExpr": {
5,788,227✔
155
                        scope.push();
1,913✔
156
                        for (const child of children) {
1,913✔
157
                                if (isTerminal(child)) continue;
13,824✔
158
                                if (child.type === "VarName") {
13,824✔
159
                                        const qname = asVarName(child, analysis);
1,990✔
160
                                        if (qname) scope.add(qnameKey(qname));
1,990✔
161
                                } else {
13,824✔
162
                                        walk(child, scope, analysis, moduleVarKeys, out);
3,951✔
163
                                }
3,951✔
164
                        }
13,824✔
165
                        scope.pop();
1,913✔
166
                        return;
1,913✔
167
                }
1,913✔
168

5,788,227✔
169
                case "InlineFunctionExpr": {
5,788,227✔
170
                        scope.push();
398✔
171
                        const paramList = directChildOf(node, "ParamList");
398✔
172
                        for (const param of paramList ? (paramList as NonTerminal).children.filter((c) => c.type === "Param") : []) {
398✔
173
                                const nameNode = directChildOf(param, "EQName");
548✔
174
                                const rawName = nameNode ? firstTerminalValue(nameNode) : null;
548!
175
                                if (!rawName) continue;
548!
176
                                const { prefix, localName, uri } = parseEQName(rawName);
548✔
177
                                scope.add(qnameKey({ prefix, localName, namespaceUri: uri ?? (prefix ? resolvePrefix(prefix, analysis) : "") }));
548✔
178
                        }
548✔
179
                        const body = directChildOf(node, "FunctionBody");
398✔
180
                        if (body) walk(body, scope, analysis, moduleVarKeys, out);
398✔
181
                        scope.pop();
398✔
182
                        return;
398✔
183
                }
398✔
184

5,788,227✔
185
                case "CatchClause":
5,788,227✔
186
                        // Skip: implicit $err:* variables inside catch clauses are not tracked in the AST.
189✔
187
                        return;
189✔
188

5,788,227✔
189
                case "InlineFunctionExpr": {
5,788,227!
UNCOV
190
                        const fn = asInlineFunctionExpr(node, analysis);
×
UNCOV
191
                        if (!fn) break;
×
UNCOV
192
                        scope.push();
×
UNCOV
193
                        for (const p of fn.params) scope.add(qnameKey(p.qname));
×
UNCOV
194
                        if (fn.body) walk(fn.body, scope, analysis, moduleVarKeys, out);
×
UNCOV
195
                        scope.pop();
×
UNCOV
196
                        return;
×
UNCOV
197
                }
×
198

5,788,227✔
199
                case "XQUF_TransformExpr": {
5,788,227✔
200
                        const tx = asTransformExpr(node, analysis);
145✔
201
                        if (!tx) break;
145✔
202
                        scope.push();
145✔
203
                        for (const b of tx.copyBindings) {
145✔
204
                                walk(b.initExpr, scope, analysis, moduleVarKeys, out);
149✔
205
                                scope.add(qnameKey(b.qname));
149✔
206
                        }
149✔
207
                        if (tx.modifyExpr) walk(tx.modifyExpr, scope, analysis, moduleVarKeys, out);
145✔
208
                        if (tx.returnExpr) walk(tx.returnExpr, scope, analysis, moduleVarKeys, out);
145✔
209
                        scope.pop();
145✔
210
                        return;
145✔
211
                }
145✔
212

5,788,227✔
213
                case "VarRef": {
5,788,227✔
214
                        const qname = asVarRef(node, analysis);
21,475✔
215
                        if (qname && !isKnown(qname, scope, moduleVarKeys))
21,475✔
216
                                reportVarRef(node, qname, out);
21,475✔
217
                        return;
21,475✔
218
                }
21,475✔
219

5,788,227✔
220
                default:
5,788,227✔
221
                        for (const child of children) walk(child, scope, analysis, moduleVarKeys, out);
5,102,878✔
222
        }
5,102,878✔
223
}
5,788,227✔
224

5✔
225
// ── Public entry point ────────────────────────────────────────────────────────
5✔
226

5✔
227
/**
5✔
228
 * Walk the AST and report every variable reference that cannot be resolved
5✔
229
 * to a declared variable: function parameter, let/for binding, module-level
5✔
230
 * `declare variable`, or an imported module variable.
5✔
231
 *
5✔
232
 * Variables whose namespace prefix is undeclared are skipped — XQST0081
5✔
233
 * already fires for those, and adding XPST0008 on top would be noisy.
5✔
234
 * Variables inside catch clauses are also skipped since the implicit
5✔
235
 * `$err:*` bindings are not represented in the AST.
5✔
236
 */
5✔
237
export function checkUndeclaredVariables(
5✔
238
        ast: Node,
29,667✔
239
        analysis: FileAnalysis,
29,667✔
240
        importedAnalyses: Map<string, FileAnalysis>,
29,667✔
241
): UndeclaredVariableDiagnostic[] {
29,667✔
242
        const moduleVarKeys = new Set<string>();
29,667✔
243
        for (const v of analysis.moduleVariables) moduleVarKeys.add(qnameKey(v.qname));
29,667✔
244
        for (const imported of importedAnalyses.values())
29,667✔
245
                for (const v of imported.moduleVariables) moduleVarKeys.add(qnameKey(v.qname));
29,667✔
246

29,667✔
247
        const out: UndeclaredVariableDiagnostic[] = [];
29,667✔
248
        walk(ast, new ScopeStack(), analysis, moduleVarKeys, out);
29,667✔
249
        return out;
29,667✔
250
}
29,667✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc