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

DrRataplan / xq-lsp / 27742561943

18 Jun 2026 07:01AM UTC coverage: 93.972% (+0.2%) from 93.77%
27742561943

push

github

DrRataplan
test: increase coverage for variable scoping and completion

- QuantifiedExpr (some/every) binding scope — flagged and not-flagged cases
- For positional variable ($x at $pos) visible in return
- CountClause variable visible in return
- Nested FLWOR outer variable visible in inner return
- Module variable visible inside function body alongside params
- Typed param completion shows detail field
- No-AST fallback (simple offset) path — positive and negative cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

872 of 1026 branches covered (84.99%)

Branch coverage included in aggregate %.

3228 of 3337 relevant lines covered (96.73%)

394188.02 hits per line

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

95.35
/src/completion-core.ts
1
import type { FileAnalysis, FunctionSymbol, VariableSymbol, ParamInfo } from "./types.ts";
3✔
2
import { formatQName } from "./types.ts";
3✔
3
import { resolvePrefix, nodeStackAtOffset, findAll } from "./analyzer.ts";
3✔
4

3✔
5
export interface CompletionContext {
3✔
6
        /** Text from start of document up to (and including) the cursor position */
3✔
7
        textBeforeCursor: string;
3✔
8
        /** Character offset of the cursor in the document */
3✔
9
        cursorOffset: number;
3✔
10
}
3✔
11

3✔
12
export interface CompletionEntry {
3✔
13
        label: string;
3✔
14
        kind: "function" | "variable";
3✔
15
        detail?: string;
3✔
16
        documentation?: string; // markdown
3✔
17
        insertText: string;
3✔
18
        isSnippet: boolean;
3✔
19
}
3✔
20

3✔
21
interface TokenInfo {
3✔
22
        kind: "variable" | "qualified-name" | "name";
3✔
23
        prefix: string;
3✔
24
        nsPrefix?: string;
3✔
25
}
3✔
26

3✔
27
function parseToken(textBefore: string): TokenInfo {
30✔
28
        const match = textBefore.match(/\$([\w:\-]*)$|(?:([\w\-]+):)([\w\-]*)$|([\w\-]+)$/);
30✔
29
        if (!match) return { kind: "name", prefix: "" };
30!
30
        if (match[0].startsWith("$")) return { kind: "variable", prefix: match[1] ?? "" };
30✔
31
        if (match[2] !== undefined) return { kind: "qualified-name", nsPrefix: match[2], prefix: match[3] ?? "" };
30!
32
        return { kind: "name", prefix: match[4] ?? "" };
30!
33
}
30✔
34

3✔
35
export function functionDoc(fn: FunctionSymbol): string {
3✔
36
        const name = formatQName(fn.qname);
388✔
37
        const paramStr = fn.params.map((p) => `$${p.name}${p.type ? " as " + p.type : ""}`).join(", ");
388✔
38
        const ret = fn.returnType ? ` as ${fn.returnType}` : "";
388✔
39
        const sig = `\`\`\`xquery\ndeclare function ${name}(${paramStr})${ret}\n\`\`\``;
388✔
40
        if (!fn.doc) return sig;
388✔
41
        const parts: string[] = [sig];
358✔
42
        if (fn.doc.description) parts.push(fn.doc.description);
388✔
43
        const paramDocs = fn.params.filter((p) => p.description);
358✔
44
        if (paramDocs.length > 0)
358✔
45
                parts.push(paramDocs.map((p) => `- \`$${p.name}\` — ${p.description}`).join("\n"));
388✔
46
        if (fn.doc.returns) parts.push(`**Returns:** ${fn.doc.returns}`);
388✔
47
        return parts.join("\n\n");
358✔
48
}
388✔
49

3✔
50
function insertText(fn: FunctionSymbol, snippets: boolean, fullName: string): string {
388✔
51
        if (fn.params.length === 0) return fullName + "()";
388✔
52
        const args = snippets
301✔
53
                ? fn.params.map((p, i) => `\${${i + 1}:\\$${p.name}}`).join(", ")
388✔
54
                : fn.params.map((p) => `$${p.name}`).join(", ");
388✔
55
        return `${fullName}(${args})`;
388✔
56
}
388✔
57

3✔
58
function buildVariableEntry(v: VariableSymbol, filter: string): CompletionEntry | null {
11✔
59
        const label = `$${formatQName(v.qname)}`;
11✔
60
        if (filter && !formatQName(v.qname).toLowerCase().startsWith(filter.toLowerCase())) return null;
11!
61
        return { label, kind: "variable", insertText: label, isSnippet: false };
11✔
62
}
11✔
63

3✔
64
function buildParamEntry(p: ParamInfo, filter: string): CompletionEntry | null {
9✔
65
        if (filter && !p.name.toLowerCase().startsWith(filter.toLowerCase())) return null;
9✔
66
        return {
8✔
67
                label: `$${p.name}`,
8✔
68
                kind: "variable",
8✔
69
                detail: p.type ? `as ${p.type}` : undefined,
9✔
70
                insertText: `$${p.name}`,
9✔
71
                isSnippet: false,
9✔
72
        };
9✔
73
}
9✔
74

3✔
75
/** Find the function symbol enclosing the cursor using the AST. */
3✔
76
function findEnclosingFunction(
14✔
77
        ast: NonNullable<FileAnalysis["ast"]>,
14✔
78
        cursorOffset: number,
14✔
79
        functions: FunctionSymbol[],
14✔
80
): { fn: FunctionSymbol; bodyStart: number; bodyEnd: number } | null {
14✔
81
        const stack = nodeStackAtOffset(ast, cursorOffset);
14✔
82
        const bodyNode = stack.slice().reverse().find((n) => n.type === "FunctionBody");
14✔
83
        if (!bodyNode || bodyNode.start === undefined || bodyNode.end == null) return null;
14✔
84
        const annotatedNode = stack.find((n) => n.type === "AnnotatedDecl");
8✔
85
        if (!annotatedNode || annotatedNode.start === undefined) return null;
14!
86
        const fn = functions.find((f) => f.sourceOffset === annotatedNode.start);
8✔
87
        if (!fn) return null;
14!
88
        return { fn, bodyStart: bodyNode.start, bodyEnd: bodyNode.end };
8✔
89
}
14✔
90

3✔
91

3✔
92
function buildFunctionEntry(fn: FunctionSymbol, label: string, snippets: boolean): CompletionEntry {
388✔
93
        return {
388✔
94
                label,
388✔
95
                kind: "function",
388✔
96
                detail: `${label}#${fn.arity}`,
388✔
97
                documentation: functionDoc(fn),
388✔
98
                insertText: insertText(fn, snippets, label),
388✔
99
                isSnippet: snippets && fn.params.length > 0,
388✔
100
        };
388✔
101
}
388✔
102

3✔
103
export function getCompletions(
3✔
104
        ctx: CompletionContext,
30✔
105
        currentAnalysis: FileAnalysis,
30✔
106
        importedAnalyses: Map<string, FileAnalysis>,
30✔
107
        snippets = false,
30✔
108
        lastValidAnalysis?: FileAnalysis,
30✔
109
): CompletionEntry[] {
30✔
110
        const token = parseToken(ctx.textBeforeCursor);
30✔
111
        const items: CompletionEntry[] = [];
30✔
112

30✔
113
        if (token.kind === "variable") {
30✔
114
                const filter = token.prefix;
16✔
115

16✔
116
                // Module-level variables are always visible
16✔
117
                for (const v of currentAnalysis.moduleVariables) {
16✔
118
                        const e = buildVariableEntry(v, filter);
5✔
119
                        if (e) items.push(e);
5✔
120
                }
5✔
121

16✔
122
                // Prefer the current AST; fall back to the last successfully-parsed one for scope analysis
16✔
123
                const scopeAst = currentAnalysis.ast ?? lastValidAnalysis?.ast;
16✔
124
                if (scopeAst) {
16✔
125
                        const enclosing = findEnclosingFunction(scopeAst, ctx.cursorOffset, currentAnalysis.functions);
14✔
126
                        if (enclosing) {
14✔
127
                                // Inside a function body: params + local bindings within this body before cursor
8✔
128
                                for (const p of enclosing.fn.params) {
8✔
129
                                        const e = buildParamEntry(p, filter);
9✔
130
                                        if (e) items.push(e);
9✔
131
                                }
9✔
132
                                for (const v of currentAnalysis.localBindings) {
8✔
133
                                        if (v.offset > enclosing.bodyStart && v.offset < ctx.cursorOffset && v.offset < enclosing.bodyEnd) {
3✔
134
                                                const e = buildVariableEntry(v, filter);
1✔
135
                                                if (e) items.push(e);
1✔
136
                                        }
1✔
137
                                }
3✔
138
                        } else {
14✔
139
                                // In query body: exclude local bindings that live inside function bodies
6✔
140
                                const functionBodyRanges = findAll(scopeAst, "FunctionBody")
6✔
141
                                        .filter((b) => b.start !== undefined && b.end != null)
6✔
142
                                        .map((b) => ({ start: b.start as number, end: b.end as number }));
6✔
143
                                for (const v of currentAnalysis.localBindings) {
6✔
144
                                        if (v.offset >= ctx.cursorOffset) continue;
5✔
145
                                        if (functionBodyRanges.some((r) => v.offset >= r.start && v.offset <= r.end)) continue;
5✔
146
                                        const e = buildVariableEntry(v, filter);
4✔
147
                                        if (e) items.push(e);
4✔
148
                                }
5✔
149
                        }
6✔
150
                } else {
16✔
151
                        // No AST ever available: simple offset-based filter, no function-param completions
2✔
152
                        for (const v of currentAnalysis.localBindings) {
2✔
153
                                if (v.offset < ctx.cursorOffset) {
2✔
154
                                        const e = buildVariableEntry(v, filter);
1✔
155
                                        if (e) items.push(e);
1✔
156
                                }
1✔
157
                        }
2✔
158
                }
2✔
159

16✔
160
                // Imported module-level variables are always visible
16✔
161
                for (const analysis of importedAnalyses.values()) {
16✔
162
                        for (const v of analysis.moduleVariables) {
16✔
163
                                const e = buildVariableEntry(v, filter);
16✔
164
                                if (e) items.push(e);
16✔
165
                        }
16✔
166
                }
16✔
167
                return items;
16✔
168
        }
16✔
169

8✔
170
        if (token.kind === "qualified-name" && token.nsPrefix) {
30✔
171
                const targetUri = resolvePrefix(token.nsPrefix, currentAnalysis);
7✔
172
                const filter = token.prefix.toLowerCase();
7✔
173
                const allFns = [
7✔
174
                        ...currentAnalysis.functions,
7✔
175
                        ...[...importedAnalyses.values()].flatMap((a) => a.functions),
7✔
176
                ];
7✔
177
                for (const fn of allFns) {
7✔
178
                        if (fn.qname.namespaceUri !== targetUri) continue;
7✔
179
                        if (filter && !fn.qname.localName.toLowerCase().includes(filter)) continue;
7!
180
                        items.push(buildFunctionEntry(fn, fn.qname.localName, snippets));
7✔
181
                }
7✔
182
                return items;
7✔
183
        }
7✔
184

1✔
185
        const filter = token.prefix.toLowerCase();
1✔
186
        const defaultUri = currentAnalysis.defaultFunctionNamespace;
1✔
187

1✔
188
        for (const fn of currentAnalysis.functions) {
30!
189
                const label = formatQName(fn.qname);
×
190
                if (filter && !label.toLowerCase().includes(filter)) continue;
×
191
                items.push(buildFunctionEntry(fn, label, snippets));
×
192
        }
✔
193

1✔
194
        for (const analysis of importedAnalyses.values()) {
1✔
195
                for (const fn of analysis.functions) {
1✔
196
                        const label =
323✔
197
                                fn.qname.namespaceUri === defaultUri ? fn.qname.localName : formatQName(fn.qname);
323✔
198
                        if (filter && !label.toLowerCase().includes(filter)) continue;
323✔
199
                        items.push(buildFunctionEntry(fn, label, snippets));
1✔
200
                }
1✔
201
        }
1✔
202

1✔
203
        return items;
1✔
204
}
30✔
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