xq-lsp 1.6.0 → 1.7.0
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/CHANGELOG.md +28 -0
- package/dist/analyzer.js +1 -0
- package/dist/ast-nodes.d.ts +29 -0
- package/dist/ast-nodes.js +63 -10
- package/dist/completion-core.d.ts +15 -2
- package/dist/completion-core.js +86 -13
- package/dist/completion.d.ts +8 -2
- package/dist/completion.js +36 -9
- package/dist/definition.d.ts +1 -1
- package/dist/definition.js +85 -9
- package/dist/diagnostics.js +4 -0
- package/dist/duplicate-function-diagnostics.d.ts +17 -0
- package/dist/duplicate-function-diagnostics.js +47 -0
- package/dist/module-import-diagnostics.d.ts +19 -0
- package/dist/module-import-diagnostics.js +35 -0
- package/dist/namespace-diagnostics.d.ts +7 -1
- package/dist/namespace-diagnostics.js +54 -13
- package/dist/qt4-worker.d.ts +8 -0
- package/dist/qt4-worker.js +35 -4
- package/dist/references.d.ts +56 -0
- package/dist/references.js +496 -0
- package/dist/server.js +119 -21
- package/dist/types.d.ts +1 -0
- package/dist/unused-diagnostics.js +18 -1
- package/dist/variable-diagnostics.js +77 -7
- package/package.json +1 -1
- package/src/analyzer.ts +1 -0
- package/src/ast-nodes.ts +89 -10
- package/src/completion-core.ts +100 -12
- package/src/completion.ts +52 -9
- package/src/definition.ts +85 -8
- package/src/diagnostics.ts +4 -0
- package/src/duplicate-function-diagnostics.ts +55 -0
- package/src/module-import-diagnostics.ts +43 -0
- package/src/namespace-diagnostics.ts +53 -12
- package/src/qt4-worker.ts +51 -4
- package/src/references.ts +602 -0
- package/src/server.ts +136 -16
- package/src/types.ts +1 -0
- package/src/unused-diagnostics.ts +17 -1
- package/src/variable-diagnostics.ts +65 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.6.0...lsp-v1.7.0) (2026-07-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add document highlight support (textDocument/documentHighlight) ([6d90ad7](https://github.com/DrRataplan/xq-lsp/commit/6d90ad7b63e50b10b68705e819320035318a56da))
|
|
9
|
+
* add find-references support via cross-file glob scan ([f0b0085](https://github.com/DrRataplan/xq-lsp/commit/f0b00852265c68ec95b96d86781eef41cb9dd940))
|
|
10
|
+
* add rename support (textDocument/rename, prepareRename) ([d0cb55d](https://github.com/DrRataplan/xq-lsp/commit/d0cb55dbcba1d5b8520f7a2080818cdc420e06e6))
|
|
11
|
+
* add XQST0034 diagnostic for double-declared functions ([6d44620](https://github.com/DrRataplan/xq-lsp/commit/6d44620e5b41e7de2c0a548086abec0b45ae22ff))
|
|
12
|
+
* add XQST0059 diagnostic for module import namespace mismatches ([d8111fc](https://github.com/DrRataplan/xq-lsp/commit/d8111fcbf034d23306a53cfe28bd7c3dd43ce3a7))
|
|
13
|
+
* auto-import and auto-declare-namespace via completion additionalTextEdits ([e1a9a93](https://github.com/DrRataplan/xq-lsp/commit/e1a9a935dea941bf35a3dc580ee77f7e88dd22c1))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* don't blanket-skip XQUpdate-tagged QT4 tests ([f2e45f1](https://github.com/DrRataplan/xq-lsp/commit/f2e45f18022873cb3bb5105eb727b484bedfd518))
|
|
19
|
+
* don't count literal fn:error() code calls as expected static errors ([0bead1b](https://github.com/DrRataplan/xq-lsp/commit/0bead1b8044c52daa2d7f2a11a24f8512d6ee2a9))
|
|
20
|
+
* fall back to glob-resolved namespace when import location hint fails ([6ce03db](https://github.com/DrRataplan/xq-lsp/commit/6ce03db6d9c5deb1fa501617c29b6005f2540102))
|
|
21
|
+
* go-to-definition for function params and imported variables ([415cfdd](https://github.com/DrRataplan/xq-lsp/commit/415cfdd4717b71af66041a7727641f18d091175d))
|
|
22
|
+
* go-to-definition for inline function params and dashed variable names ([a4e7279](https://github.com/DrRataplan/xq-lsp/commit/a4e7279d4b27357bb27e7b67f2ed0551650ef66e))
|
|
23
|
+
* handle InlineFunctionExpr scope in variable and unused-variable walkers ([de19b18](https://github.com/DrRataplan/xq-lsp/commit/de19b186fd367dc1450de1554205e8c07d0e40fd))
|
|
24
|
+
* insert auto-import after last existing import, skip multi-line docblocks ([12227a9](https://github.com/DrRataplan/xq-lsp/commit/12227a916dcdafeb2ba5ddef21e82b4e2f6acbca))
|
|
25
|
+
* load external <test file> queries and skip unsupported QT4 features ([4cbdc58](https://github.com/DrRataplan/xq-lsp/commit/4cbdc589b9af65f9043e3c0de6b5ffe950f3c7fe))
|
|
26
|
+
* make computeRelativePath browser-safe and build the demo in CI ([033a057](https://github.com/DrRataplan/xq-lsp/commit/033a057849cbc492f8e243cf8f4e908200673033))
|
|
27
|
+
* model QT4 environment-bound variables to cut XPST0008 false positives ([5dcb404](https://github.com/DrRataplan/xq-lsp/commit/5dcb4040bd79c0b7dc8edac5086b2289b1582b9e))
|
|
28
|
+
* rewrite imported variable prefix to the importing module's prefix in completions ([f7c5ccc](https://github.com/DrRataplan/xq-lsp/commit/f7c5ccc337af75bae0c2d881210ba3373c90bb71))
|
|
29
|
+
* scope inline function params, window vars, and copy bindings for XPST0008 ([7391a94](https://github.com/DrRataplan/xq-lsp/commit/7391a94c2607a028b6636ad6fcdadb29ba1356e5))
|
|
30
|
+
|
|
3
31
|
## [1.6.0](https://github.com/DrRataplan/xq-lsp/compare/lsp-v1.5.0...lsp-v1.6.0) (2026-07-09)
|
|
4
32
|
|
|
5
33
|
|
package/dist/analyzer.js
CHANGED
package/dist/ast-nodes.d.ts
CHANGED
|
@@ -27,6 +27,35 @@ export interface FunctionDeclShape {
|
|
|
27
27
|
}
|
|
28
28
|
/** Inner shape of a FunctionDecl node: name, resolved params, and function body. */
|
|
29
29
|
export declare function asFunctionDecl(node: Node, analysis: FileAnalysis): FunctionDeclShape | null;
|
|
30
|
+
export interface InlineFunctionExprShape {
|
|
31
|
+
params: Array<{
|
|
32
|
+
nameNode: Node;
|
|
33
|
+
qname: QName;
|
|
34
|
+
}>;
|
|
35
|
+
body: Node | null;
|
|
36
|
+
}
|
|
37
|
+
export declare function asInlineFunctionExpr(node: Node, analysis: FileAnalysis): InlineFunctionExprShape | null;
|
|
38
|
+
export interface WindowVarsShape {
|
|
39
|
+
currentItem?: QName;
|
|
40
|
+
positionalVar?: {
|
|
41
|
+
nameNode: Node;
|
|
42
|
+
qname: QName;
|
|
43
|
+
};
|
|
44
|
+
previousItem?: QName;
|
|
45
|
+
nextItem?: QName;
|
|
46
|
+
}
|
|
47
|
+
/** Resolves the `$current at $pos previous $prev next $next` bindings of a WindowVars node. */
|
|
48
|
+
export declare function asWindowVars(node: Node, analysis: FileAnalysis): WindowVarsShape | null;
|
|
49
|
+
export interface CopyBindingShape {
|
|
50
|
+
qname: QName;
|
|
51
|
+
initExpr: Node;
|
|
52
|
+
}
|
|
53
|
+
export interface TransformExprShape {
|
|
54
|
+
copyBindings: CopyBindingShape[];
|
|
55
|
+
modifyExpr: Node | null;
|
|
56
|
+
returnExpr: Node | null;
|
|
57
|
+
}
|
|
58
|
+
export declare function asTransformExpr(node: Node, analysis: FileAnalysis): TransformExprShape | null;
|
|
30
59
|
/** QName, name node, arity, and annotation local-names for an AnnotatedDecl containing a FunctionDecl, or null. */
|
|
31
60
|
export declare function asFunctionDeclaration(node: Node, analysis: FileAnalysis): {
|
|
32
61
|
qname: QName;
|
package/dist/ast-nodes.js
CHANGED
|
@@ -18,6 +18,26 @@ function resolveQName(raw, analysis, defaultNs) {
|
|
|
18
18
|
const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
|
|
19
19
|
return { prefix, localName, namespaceUri };
|
|
20
20
|
}
|
|
21
|
+
/** Resolves a variable QName from a node whose first descendant terminal is the bare name (no "$"). */
|
|
22
|
+
function qnameFromRawTerminal(node, analysis) {
|
|
23
|
+
const rawName = firstTerminalValue(node);
|
|
24
|
+
if (!rawName)
|
|
25
|
+
return null;
|
|
26
|
+
const { prefix, localName, uri } = parseEQName(rawName);
|
|
27
|
+
return { prefix, localName, namespaceUri: uri ?? (prefix ? resolvePrefix(prefix, analysis) : "") };
|
|
28
|
+
}
|
|
29
|
+
function extractParamsFromParamList(paramList, analysis) {
|
|
30
|
+
const paramNodes = paramList ? directChildrenOf(paramList, "Param") : [];
|
|
31
|
+
return paramNodes.flatMap((p) => {
|
|
32
|
+
const pNameNode = directChildOf(p, "EQName");
|
|
33
|
+
if (!pNameNode)
|
|
34
|
+
return [];
|
|
35
|
+
const qname = qnameFromRawTerminal(pNameNode, analysis);
|
|
36
|
+
if (!qname)
|
|
37
|
+
return [];
|
|
38
|
+
return [{ nameNode: pNameNode, qname }];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
21
41
|
export function asFunctionCall(node, analysis) {
|
|
22
42
|
if (node.type !== "FunctionCall")
|
|
23
43
|
return null;
|
|
@@ -90,18 +110,51 @@ export function asFunctionDecl(node, analysis) {
|
|
|
90
110
|
return null;
|
|
91
111
|
const { prefix, localName, uri } = parseEQName(rawName);
|
|
92
112
|
const namespaceUri = uri ?? resolvePrefix(prefix, analysis);
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
const params = extractParamsFromParamList(directChildOf(node, "ParamList"), analysis);
|
|
114
|
+
return { nameNode, qname: { prefix, localName, namespaceUri }, params, body: directChildOf(node, "FunctionBody") ?? null };
|
|
115
|
+
}
|
|
116
|
+
export function asInlineFunctionExpr(node, analysis) {
|
|
117
|
+
if (node.type !== "InlineFunctionExpr")
|
|
118
|
+
return null;
|
|
119
|
+
const params = extractParamsFromParamList(directChildOf(node, "ParamList"), analysis);
|
|
120
|
+
return { params, body: directChildOf(node, "FunctionBody") ?? null };
|
|
121
|
+
}
|
|
122
|
+
/** Resolves the `$current at $pos previous $prev next $next` bindings of a WindowVars node. */
|
|
123
|
+
export function asWindowVars(node, analysis) {
|
|
124
|
+
if (node.type !== "WindowVars")
|
|
125
|
+
return null;
|
|
126
|
+
const result = {};
|
|
127
|
+
const currentItemNode = directChildOf(node, "CurrentItem");
|
|
128
|
+
if (currentItemNode)
|
|
129
|
+
result.currentItem = qnameFromRawTerminal(currentItemNode, analysis) ?? undefined;
|
|
130
|
+
const posVarNode = directChildOf(node, "PositionalVar");
|
|
131
|
+
if (posVarNode)
|
|
132
|
+
result.positionalVar = asPositionalVar(posVarNode, analysis) ?? undefined;
|
|
133
|
+
const prevNode = directChildOf(node, "PreviousItem");
|
|
134
|
+
if (prevNode)
|
|
135
|
+
result.previousItem = qnameFromRawTerminal(prevNode, analysis) ?? undefined;
|
|
136
|
+
const nextNode = directChildOf(node, "NextItem");
|
|
137
|
+
if (nextNode)
|
|
138
|
+
result.nextItem = qnameFromRawTerminal(nextNode, analysis) ?? undefined;
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
export function asTransformExpr(node, analysis) {
|
|
142
|
+
if (node.type !== "XQUF_TransformExpr")
|
|
143
|
+
return null;
|
|
144
|
+
const bindingList = directChildOf(node, "XQUF_CopyBindingList");
|
|
145
|
+
const bindingNodes = bindingList ? directChildrenOf(bindingList, "XQUF_CopyBinding") : [];
|
|
146
|
+
const copyBindings = bindingNodes.flatMap((b) => {
|
|
147
|
+
const nameNode = directChildOf(b, "VarName");
|
|
148
|
+
const initExpr = directChildOf(b, "ExprSingle");
|
|
149
|
+
if (!nameNode || !initExpr)
|
|
99
150
|
return [];
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
151
|
+
const qname = qnameFromRawTerminal(nameNode, analysis);
|
|
152
|
+
if (!qname)
|
|
153
|
+
return [];
|
|
154
|
+
return [{ qname, initExpr }];
|
|
103
155
|
});
|
|
104
|
-
|
|
156
|
+
const [modifyExpr, returnExpr] = directChildrenOf(node, "ExprSingle");
|
|
157
|
+
return { copyBindings, modifyExpr: modifyExpr ?? null, returnExpr: returnExpr ?? null };
|
|
105
158
|
}
|
|
106
159
|
// ── AnnotatedDecl wrappers ────────────────────────────────────────────────────
|
|
107
160
|
/** QName, name node, arity, and annotation local-names for an AnnotatedDecl containing a FunctionDecl, or null. */
|
|
@@ -5,13 +5,26 @@ export interface CompletionContext {
|
|
|
5
5
|
/** Character offset of the cursor in the document */
|
|
6
6
|
cursorOffset: number;
|
|
7
7
|
}
|
|
8
|
+
/** Carries enough information for the LSP adapter to build the additionalTextEdit. */
|
|
9
|
+
export interface AdditionalEdit {
|
|
10
|
+
kind: "import-module" | "declare-namespace";
|
|
11
|
+
prefix: string;
|
|
12
|
+
namespaceUri: string;
|
|
13
|
+
/** Source URI of the module file, used to compute the relative `at` path. */
|
|
14
|
+
sourceUri?: string;
|
|
15
|
+
}
|
|
8
16
|
export interface CompletionEntry {
|
|
9
17
|
label: string;
|
|
10
|
-
kind: "function" | "variable";
|
|
18
|
+
kind: "function" | "variable" | "namespace";
|
|
11
19
|
detail?: string;
|
|
12
20
|
documentation?: string;
|
|
13
21
|
insertText: string;
|
|
14
22
|
isSnippet: boolean;
|
|
23
|
+
additionalEdit?: AdditionalEdit;
|
|
15
24
|
}
|
|
16
25
|
export declare function functionDoc(fn: FunctionSymbol): string;
|
|
17
|
-
export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis
|
|
26
|
+
export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis,
|
|
27
|
+
/** Modules available in the workspace but not yet imported in the current file. Keyed by namespace URI. */
|
|
28
|
+
availableAnalyses?: Map<string, FileAnalysis>,
|
|
29
|
+
/** Known namespace prefix → URI mappings (from config / glob scans) for pure XML namespaces. */
|
|
30
|
+
knownNamespaces?: Map<string, string>): CompletionEntry[];
|
package/dist/completion-core.js
CHANGED
|
@@ -76,7 +76,15 @@ function buildFunctionEntry(fn, label, snippets) {
|
|
|
76
76
|
isSnippet: snippets && fn.params.length > 0,
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
/** Return the source URI for a module's analysis (taken from the first known symbol). */
|
|
80
|
+
function moduleSourceUri(analysis) {
|
|
81
|
+
return analysis.functions[0]?.sourceUri ?? analysis.moduleVariables[0]?.sourceUri;
|
|
82
|
+
}
|
|
83
|
+
export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets = false, lastValidAnalysis,
|
|
84
|
+
/** Modules available in the workspace but not yet imported in the current file. Keyed by namespace URI. */
|
|
85
|
+
availableAnalyses,
|
|
86
|
+
/** Known namespace prefix → URI mappings (from config / glob scans) for pure XML namespaces. */
|
|
87
|
+
knownNamespaces) {
|
|
80
88
|
const token = parseToken(ctx.textBeforeCursor);
|
|
81
89
|
const items = [];
|
|
82
90
|
if (token.kind === "variable") {
|
|
@@ -132,29 +140,94 @@ export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets
|
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
|
-
// Imported module-level variables
|
|
143
|
+
// Imported module-level variables — rewrite prefix to the one used in the current module
|
|
136
144
|
for (const analysis of importedAnalyses.values()) {
|
|
137
145
|
for (const v of analysis.moduleVariables) {
|
|
138
|
-
const
|
|
146
|
+
const localPrefix = currentAnalysis.imports.find((i) => i.namespaceUri === v.qname.namespaceUri)?.prefix;
|
|
147
|
+
const displayVar = localPrefix !== undefined && localPrefix !== v.qname.prefix
|
|
148
|
+
? { ...v, qname: { ...v.qname, prefix: localPrefix } }
|
|
149
|
+
: v;
|
|
150
|
+
const e = buildVariableEntry(displayVar, filter);
|
|
139
151
|
if (e)
|
|
140
152
|
items.push(e);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
155
|
+
// Variables from modules available in the workspace but not yet imported
|
|
156
|
+
if (availableAnalyses) {
|
|
157
|
+
const colonIdx = filter.indexOf(":");
|
|
158
|
+
if (colonIdx >= 0) {
|
|
159
|
+
const nsPrefix = filter.slice(0, colonIdx);
|
|
160
|
+
const localFilter = filter.slice(colonIdx + 1).toLowerCase();
|
|
161
|
+
const nsUri = resolvePrefix(nsPrefix, currentAnalysis);
|
|
162
|
+
if (nsUri.startsWith("urn:xq-lsp:undeclared:")) {
|
|
163
|
+
for (const [moduleNsUri, analysis] of availableAnalyses) {
|
|
164
|
+
if (analysis.modulePrefix !== nsPrefix)
|
|
165
|
+
continue;
|
|
166
|
+
const sourceUri = moduleSourceUri(analysis);
|
|
167
|
+
for (const v of analysis.moduleVariables) {
|
|
168
|
+
if (localFilter && !v.qname.localName.toLowerCase().startsWith(localFilter))
|
|
169
|
+
continue;
|
|
170
|
+
const displayQName = { ...v.qname, prefix: nsPrefix };
|
|
171
|
+
items.push({
|
|
172
|
+
label: `$${formatQName(displayQName)}`,
|
|
173
|
+
kind: "variable",
|
|
174
|
+
insertText: `$${formatQName(displayQName)}`,
|
|
175
|
+
isSnippet: false,
|
|
176
|
+
additionalEdit: { kind: "import-module", prefix: nsPrefix, namespaceUri: moduleNsUri, sourceUri },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
143
183
|
return items;
|
|
144
184
|
}
|
|
145
185
|
if (token.kind === "qualified-name" && token.nsPrefix) {
|
|
146
186
|
const targetUri = resolvePrefix(token.nsPrefix, currentAnalysis);
|
|
147
187
|
const filter = token.prefix.toLowerCase();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
if (!targetUri.startsWith("urn:xq-lsp:undeclared:")) {
|
|
189
|
+
// Prefix is declared — show functions from current + imported analyses (no additionalEdit needed)
|
|
190
|
+
const allFns = [
|
|
191
|
+
...currentAnalysis.functions,
|
|
192
|
+
...[...importedAnalyses.values()].flatMap((a) => a.functions),
|
|
193
|
+
];
|
|
194
|
+
for (const fn of allFns) {
|
|
195
|
+
if (fn.qname.namespaceUri !== targetUri)
|
|
196
|
+
continue;
|
|
197
|
+
if (filter && !fn.qname.localName.toLowerCase().includes(filter))
|
|
198
|
+
continue;
|
|
199
|
+
items.push(buildFunctionEntry(fn, fn.qname.localName, snippets));
|
|
200
|
+
}
|
|
201
|
+
return items;
|
|
202
|
+
}
|
|
203
|
+
// Prefix is undeclared — look for matching modules in the workspace
|
|
204
|
+
let foundModule = false;
|
|
205
|
+
if (availableAnalyses) {
|
|
206
|
+
for (const [moduleNsUri, analysis] of availableAnalyses) {
|
|
207
|
+
if (analysis.modulePrefix !== token.nsPrefix)
|
|
208
|
+
continue;
|
|
209
|
+
foundModule = true;
|
|
210
|
+
const sourceUri = moduleSourceUri(analysis);
|
|
211
|
+
for (const fn of analysis.functions) {
|
|
212
|
+
if (filter && !fn.qname.localName.toLowerCase().includes(filter))
|
|
213
|
+
continue;
|
|
214
|
+
items.push({
|
|
215
|
+
...buildFunctionEntry(fn, fn.qname.localName, snippets),
|
|
216
|
+
additionalEdit: { kind: "import-module", prefix: token.nsPrefix, namespaceUri: moduleNsUri, sourceUri },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Pure XML namespace (no module functions): offer a declare-namespace item if prefix is known
|
|
222
|
+
if (!foundModule && knownNamespaces?.has(token.nsPrefix)) {
|
|
223
|
+
const nsUri = knownNamespaces.get(token.nsPrefix);
|
|
224
|
+
items.push({
|
|
225
|
+
label: `declare namespace ${token.nsPrefix} = "${nsUri}"`,
|
|
226
|
+
kind: "namespace",
|
|
227
|
+
insertText: token.prefix,
|
|
228
|
+
isSnippet: false,
|
|
229
|
+
additionalEdit: { kind: "declare-namespace", prefix: token.nsPrefix, namespaceUri: nsUri },
|
|
230
|
+
});
|
|
158
231
|
}
|
|
159
232
|
return items;
|
|
160
233
|
}
|
package/dist/completion.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { CompletionItem } from "vscode-languageserver/node.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { FileAnalysis } from "./types.ts";
|
|
3
|
+
import type { CompletionContext } from "./completion-core.ts";
|
|
3
4
|
export type { CompletionContext } from "./completion-core.ts";
|
|
4
|
-
export
|
|
5
|
+
export interface EditContext {
|
|
6
|
+
docText: string;
|
|
7
|
+
docUri: string;
|
|
8
|
+
generateLocationHints: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function getCompletions(ctx: CompletionContext, currentAnalysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>, snippets?: boolean, lastValidAnalysis?: FileAnalysis, availableAnalyses?: Map<string, FileAnalysis>, knownNamespaces?: Map<string, string>, editContext?: EditContext): CompletionItem[];
|
package/dist/completion.js
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import { CompletionItem, CompletionItemKind, InsertTextFormat, MarkupKind } from "vscode-languageserver/node.js";
|
|
2
2
|
import { getCompletions as getCoreCompletions } from "./completion-core.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import { findImportInsertPosition, findDeclareNsInsertPosition, computeRelativePath } from "./namespace-diagnostics.js";
|
|
4
|
+
export function getCompletions(ctx, currentAnalysis, importedAnalyses, snippets, lastValidAnalysis, availableAnalyses, knownNamespaces, editContext) {
|
|
5
|
+
return getCoreCompletions(ctx, currentAnalysis, importedAnalyses, snippets, lastValidAnalysis, availableAnalyses, knownNamespaces).map((e) => {
|
|
6
|
+
const item = {
|
|
7
|
+
label: e.label,
|
|
8
|
+
kind: e.kind === "function"
|
|
9
|
+
? CompletionItemKind.Function
|
|
10
|
+
: e.kind === "namespace"
|
|
11
|
+
? CompletionItemKind.Module
|
|
12
|
+
: CompletionItemKind.Variable,
|
|
13
|
+
detail: e.detail,
|
|
14
|
+
documentation: e.documentation ? { kind: MarkupKind.Markdown, value: e.documentation } : undefined,
|
|
15
|
+
insertText: e.insertText,
|
|
16
|
+
insertTextFormat: e.isSnippet ? InsertTextFormat.Snippet : InsertTextFormat.PlainText,
|
|
17
|
+
};
|
|
18
|
+
if (e.additionalEdit && editContext) {
|
|
19
|
+
const { docText, docUri, generateLocationHints } = editContext;
|
|
20
|
+
const insertPos = e.additionalEdit.kind === "import-module"
|
|
21
|
+
? findImportInsertPosition(docText)
|
|
22
|
+
: findDeclareNsInsertPosition(docText);
|
|
23
|
+
let newText;
|
|
24
|
+
if (e.additionalEdit.kind === "import-module") {
|
|
25
|
+
const atPath = generateLocationHints && e.additionalEdit.sourceUri
|
|
26
|
+
? computeRelativePath(docUri, e.additionalEdit.sourceUri)
|
|
27
|
+
: undefined;
|
|
28
|
+
newText = atPath
|
|
29
|
+
? `import module namespace ${e.additionalEdit.prefix} = "${e.additionalEdit.namespaceUri}" at "${atPath}";\n`
|
|
30
|
+
: `import module namespace ${e.additionalEdit.prefix} = "${e.additionalEdit.namespaceUri}";\n`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
newText = `declare namespace ${e.additionalEdit.prefix} = "${e.additionalEdit.namespaceUri}";\n`;
|
|
34
|
+
}
|
|
35
|
+
item.additionalTextEdits = [{ range: { start: insertPos, end: insertPos }, newText }];
|
|
36
|
+
}
|
|
37
|
+
return item;
|
|
38
|
+
});
|
|
12
39
|
}
|
package/dist/definition.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Location } from "vscode-languageserver/node.js";
|
|
2
2
|
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
3
|
import type { FileAnalysis } from "./types.ts";
|
|
4
|
-
export declare function getDefinition(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis
|
|
4
|
+
export declare function getDefinition(doc: TextDocument, offset: number, current: FileAnalysis, imported: Map<string, FileAnalysis>): Location | null;
|
package/dist/definition.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
import { Location } from "vscode-languageserver/node.js";
|
|
4
|
-
import { resolvePrefix, parseEQName } from "./analyzer.js";
|
|
4
|
+
import { resolvePrefix, parseEQName, nodeStackAtOffset, directChildOf, directChildrenOf, firstTerminalValue } from "./analyzer.js";
|
|
5
|
+
import { asVarRef } from "./ast-nodes.js";
|
|
5
6
|
function wordAt(text, offset) {
|
|
6
7
|
let start = offset;
|
|
7
8
|
let end = offset;
|
|
@@ -16,21 +17,96 @@ function positionInText(text, offset) {
|
|
|
16
17
|
const lines = before.split("\n");
|
|
17
18
|
return { line: lines.length - 1, character: lines[lines.length - 1].length };
|
|
18
19
|
}
|
|
19
|
-
export function getDefinition(doc, offset, current, imported
|
|
20
|
+
export function getDefinition(doc, offset, current, imported) {
|
|
20
21
|
const text = doc.getText();
|
|
21
22
|
const { word, start } = wordAt(text, offset);
|
|
22
23
|
if (!word)
|
|
23
24
|
return null;
|
|
24
25
|
const hasDollar = start > 0 && text[start - 1] === "$";
|
|
25
26
|
if (hasDollar) {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Use AST to extract the exact variable name (handles dashes and other valid NCName chars).
|
|
28
|
+
// Fall back to the text-scanned `word` only when no AST is available.
|
|
29
|
+
let varLocalName;
|
|
30
|
+
let varNsUri;
|
|
31
|
+
let astStack = null;
|
|
32
|
+
if (current.ast) {
|
|
33
|
+
astStack = nodeStackAtOffset(current.ast, offset);
|
|
34
|
+
const varRefNode = [...astStack].reverse().find((n) => n.type === "VarRef");
|
|
35
|
+
const qname = varRefNode ? asVarRef(varRefNode, current) : null;
|
|
36
|
+
if (qname) {
|
|
37
|
+
varLocalName = qname.localName;
|
|
38
|
+
varNsUri = qname.namespaceUri;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Cursor is on $ but AST didn't yield a VarRef (e.g. mid-edit) — fall back
|
|
42
|
+
const { prefix, localName, uri } = parseEQName(word);
|
|
43
|
+
varLocalName = localName;
|
|
44
|
+
varNsUri = uri ?? (prefix ? resolvePrefix(prefix, current) : "");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const { prefix, localName, uri } = parseEQName(word);
|
|
49
|
+
varLocalName = localName;
|
|
50
|
+
varNsUri = uri ?? (prefix ? resolvePrefix(prefix, current) : "");
|
|
51
|
+
}
|
|
52
|
+
// Local bindings and module-level variables in current file
|
|
28
53
|
const allVars = [...current.moduleVariables, ...current.localBindings];
|
|
29
54
|
const v = allVars.find((v) => v.qname.namespaceUri === varNsUri && v.qname.localName === varLocalName);
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
if (v) {
|
|
56
|
+
const pos = doc.positionAt(v.offset);
|
|
57
|
+
return Location.create(doc.uri, { start: pos, end: pos });
|
|
58
|
+
}
|
|
59
|
+
// Params of inline function expressions enclosing the cursor (not tracked in analysis)
|
|
60
|
+
if (!varNsUri && astStack) {
|
|
61
|
+
for (let i = astStack.length - 1; i >= 0; i--) {
|
|
62
|
+
if (astStack[i].type !== "InlineFunctionExpr")
|
|
63
|
+
continue;
|
|
64
|
+
const paramList = directChildOf(astStack[i], "ParamList");
|
|
65
|
+
for (const param of paramList ? directChildrenOf(paramList, "Param") : []) {
|
|
66
|
+
const nameNode = directChildOf(param, "EQName");
|
|
67
|
+
const rawName = nameNode ? firstTerminalValue(nameNode) : null;
|
|
68
|
+
if (!rawName)
|
|
69
|
+
continue;
|
|
70
|
+
const { prefix, localName } = parseEQName(rawName);
|
|
71
|
+
if (prefix || localName !== varLocalName)
|
|
72
|
+
continue;
|
|
73
|
+
if (nameNode?.start !== undefined) {
|
|
74
|
+
const pos = doc.positionAt(nameNode.start);
|
|
75
|
+
return Location.create(doc.uri, { start: pos, end: pos });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Params of declared functions in the current file (prefer nearest enclosing by source offset)
|
|
81
|
+
if (!varNsUri) {
|
|
82
|
+
const enclosingFn = current.functions
|
|
83
|
+
.filter((f) => f.sourceOffset !== undefined && f.sourceOffset <= offset)
|
|
84
|
+
.at(-1);
|
|
85
|
+
const matchingFn = enclosingFn ?? current.functions[0];
|
|
86
|
+
const param = matchingFn?.params.find((p) => p.name === varLocalName);
|
|
87
|
+
if (param?.sourceOffset !== undefined) {
|
|
88
|
+
const pos = doc.positionAt(param.sourceOffset);
|
|
89
|
+
return Location.create(doc.uri, { start: pos, end: pos });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Imported module-level variables
|
|
93
|
+
for (const [, analysis] of imported) {
|
|
94
|
+
const iv = analysis.moduleVariables.find((v) => v.qname.namespaceUri === varNsUri && v.qname.localName === varLocalName);
|
|
95
|
+
if (!iv)
|
|
96
|
+
continue;
|
|
97
|
+
const uri = iv.sourceUri;
|
|
98
|
+
let pos = { line: 0, character: 0 };
|
|
99
|
+
try {
|
|
100
|
+
const filePath = fileURLToPath(uri);
|
|
101
|
+
const srcText = fs.readFileSync(filePath, "utf-8");
|
|
102
|
+
pos = positionInText(srcText, iv.offset);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* file not readable, use line 0 */
|
|
106
|
+
}
|
|
107
|
+
return Location.create(uri, { start: pos, end: pos });
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
34
110
|
}
|
|
35
111
|
// Function definition — resolve by namespace URI, search current file then all imports
|
|
36
112
|
const { prefix: wordPrefix, localName: wordLocalName, uri: wordDirectUri } = parseEQName(word);
|
|
@@ -49,7 +125,7 @@ export function getDefinition(doc, offset, current, imported, resolveUri) {
|
|
|
49
125
|
const fn = analysis.functions.find((f) => f.qname.namespaceUri === targetUri && f.qname.localName === wordLocalName);
|
|
50
126
|
if (!fn)
|
|
51
127
|
continue;
|
|
52
|
-
const uri =
|
|
128
|
+
const uri = fn.sourceUri;
|
|
53
129
|
let pos = { line: 0, character: 0 };
|
|
54
130
|
if (fn.sourceOffset !== undefined) {
|
|
55
131
|
try {
|
package/dist/diagnostics.js
CHANGED
|
@@ -3,6 +3,8 @@ import { checkFunctionCalls } from "./functioncall-diagnostics.js";
|
|
|
3
3
|
import { findUndeclaredPrefixUsages } from "./namespace-diagnostics.js";
|
|
4
4
|
import { checkContextItemUsage } from "./context-item-diagnostics.js";
|
|
5
5
|
import { checkUndeclaredVariables } from "./variable-diagnostics.js";
|
|
6
|
+
import { checkDuplicateFunctions } from "./duplicate-function-diagnostics.js";
|
|
7
|
+
import { checkModuleImportTargets } from "./module-import-diagnostics.js";
|
|
6
8
|
import { checkUnused } from "./unused-diagnostics.js";
|
|
7
9
|
/**
|
|
8
10
|
* Run all error-level diagnostics (XQuery static/dynamic error codes).
|
|
@@ -17,6 +19,8 @@ export function runDiagnostics(ast, text, analysis, imports) {
|
|
|
17
19
|
...findUndeclaredPrefixUsages(ast, analysis),
|
|
18
20
|
...checkContextItemUsage(ast),
|
|
19
21
|
...checkUndeclaredVariables(ast, analysis, imports),
|
|
22
|
+
...checkDuplicateFunctions(ast, analysis, imports),
|
|
23
|
+
...checkModuleImportTargets(analysis, imports),
|
|
20
24
|
];
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Node } from "xq-parser";
|
|
2
|
+
import type { TypeDiagnostic, FileAnalysis } from "./types.ts";
|
|
3
|
+
export interface DuplicateFunctionDiagnostic extends TypeDiagnostic {
|
|
4
|
+
code: "XQST0034";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Walk the AST and report every function declaration whose expanded QName and
|
|
8
|
+
* arity (parameter count) collide with another declaration — either another
|
|
9
|
+
* declaration in this file, or one already provided by an imported module.
|
|
10
|
+
* Per XQST0034, return type, parameter types, and parameter names are
|
|
11
|
+
* irrelevant to the clash: only the expanded QName and arity matter.
|
|
12
|
+
*
|
|
13
|
+
* All declarations that share a colliding key are reported (not just the
|
|
14
|
+
* later ones), since the file only stops erroring once every collision is
|
|
15
|
+
* resolved.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkDuplicateFunctions(ast: Node, analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): DuplicateFunctionDiagnostic[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { qnameKey, formatQName } from "./types.js";
|
|
2
|
+
import { findAll } from "./analyzer.js";
|
|
3
|
+
import { asFunctionDeclaration } from "./ast-nodes.js";
|
|
4
|
+
/**
|
|
5
|
+
* Walk the AST and report every function declaration whose expanded QName and
|
|
6
|
+
* arity (parameter count) collide with another declaration — either another
|
|
7
|
+
* declaration in this file, or one already provided by an imported module.
|
|
8
|
+
* Per XQST0034, return type, parameter types, and parameter names are
|
|
9
|
+
* irrelevant to the clash: only the expanded QName and arity matter.
|
|
10
|
+
*
|
|
11
|
+
* All declarations that share a colliding key are reported (not just the
|
|
12
|
+
* later ones), since the file only stops erroring once every collision is
|
|
13
|
+
* resolved.
|
|
14
|
+
*/
|
|
15
|
+
export function checkDuplicateFunctions(ast, analysis, importedAnalyses) {
|
|
16
|
+
const importedKeys = new Set();
|
|
17
|
+
for (const imported of importedAnalyses.values())
|
|
18
|
+
for (const f of imported.functions)
|
|
19
|
+
importedKeys.add(`${qnameKey(f.qname)}#${f.arity}`);
|
|
20
|
+
const groups = new Map();
|
|
21
|
+
for (const annotated of findAll(ast, "AnnotatedDecl")) {
|
|
22
|
+
const fn = asFunctionDeclaration(annotated, analysis);
|
|
23
|
+
if (!fn)
|
|
24
|
+
continue;
|
|
25
|
+
const key = `${qnameKey(fn.qname)}#${fn.arity}`;
|
|
26
|
+
const entry = { nameNode: fn.nameNode, displayName: formatQName(fn.qname), arity: fn.arity };
|
|
27
|
+
const group = groups.get(key);
|
|
28
|
+
if (group)
|
|
29
|
+
group.push(entry);
|
|
30
|
+
else
|
|
31
|
+
groups.set(key, [entry]);
|
|
32
|
+
}
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const [key, decls] of groups) {
|
|
35
|
+
if (decls.length <= 1 && !importedKeys.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
for (const { nameNode, displayName, arity } of decls) {
|
|
38
|
+
out.push({
|
|
39
|
+
message: `Function '${displayName}' is declared more than once (${arity} parameter${arity === 1 ? "" : "s"})`,
|
|
40
|
+
code: "XQST0034",
|
|
41
|
+
offset: nameNode.start ?? 0,
|
|
42
|
+
length: displayName.length,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FileAnalysis, TypeDiagnostic } from "./types.ts";
|
|
2
|
+
export interface ModuleImportDiagnostic extends TypeDiagnostic {
|
|
3
|
+
code: "XQST0059";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Report module imports whose target module resolves to a real file but
|
|
7
|
+
* whose target namespace (from that file's own `module namespace` decl)
|
|
8
|
+
* does not match the namespace URI written in the import statement.
|
|
9
|
+
*
|
|
10
|
+
* `importedAnalyses` is keyed by `atPath` when the import has a location
|
|
11
|
+
* hint, and always also by `namespaceUri` — mirroring how `resolveContext`
|
|
12
|
+
* in server.ts populates the map (a resolved import is recorded under both
|
|
13
|
+
* keys). Preferring `atPath` when present and falling back to
|
|
14
|
+
* `namespaceUri` lets this check reuse the same map without caring how the
|
|
15
|
+
* caller resolved it. An import missing from the map entirely means
|
|
16
|
+
* resolution didn't happen at all, which is a different problem and not
|
|
17
|
+
* checked here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkModuleImportTargets(analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): ModuleImportDiagnostic[];
|