xq-lsp 1.8.2 → 1.8.3-next.430
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/README.md +16 -9
- package/builtins/builtins-fn.xq +1 -1
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +43 -3
- package/dist/ast-nodes.d.ts +38 -0
- package/dist/ast-nodes.js +71 -0
- package/dist/diagnostics.js +2 -0
- package/dist/functioncall-diagnostics.d.ts +6 -3
- package/dist/functioncall-diagnostics.js +76 -17
- package/dist/inlay-hints.js +28 -82
- package/dist/module-siblings.d.ts +11 -0
- package/dist/module-siblings.js +16 -0
- package/dist/predeclared-namespaces.js +1 -0
- package/dist/prolog-diagnostics.d.ts +10 -0
- package/dist/prolog-diagnostics.js +113 -0
- package/dist/qt4-worker.js +5 -3
- package/dist/runtimes/fonto/fonto.xq +95 -0
- package/dist/semantic-tokens.d.ts +29 -0
- package/dist/semantic-tokens.js +254 -0
- package/dist/server.js +40 -7
- package/dist/typechecker.d.ts +48 -1
- package/dist/typechecker.js +1215 -122
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/analyzer.ts +45 -3
- package/src/ast-nodes.ts +85 -0
- package/src/diagnostics.ts +2 -0
- package/src/functioncall-diagnostics.ts +93 -15
- package/src/inlay-hints.ts +42 -93
- package/src/module-siblings.ts +17 -0
- package/src/predeclared-namespaces.ts +1 -0
- package/src/prolog-diagnostics.ts +137 -0
- package/src/qt4-worker.ts +6 -4
- package/src/runtimes/fonto/fonto.xq +95 -0
- package/src/semantic-tokens.ts +289 -0
- package/src/server.ts +40 -6
- package/src/typechecker.ts +1265 -123
- package/src/types.ts +2 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declarations that *other* files contribute to a module namespace. XQuery lets one module be
|
|
3
|
+
* implemented by several module files sharing a target namespace, and some runtimes (e.g. Fonto)
|
|
4
|
+
* merge them, so a file can call a function declared in a sibling without importing it.
|
|
5
|
+
*
|
|
6
|
+
* `merged` is the glob index entry for the namespace, which may include the current file's own
|
|
7
|
+
* on-disk declarations. Those are dropped so they aren't reported as duplicates of the (possibly
|
|
8
|
+
* edited) in-memory versions, nor resolved to stale positions. Returns null when nothing is left.
|
|
9
|
+
*/
|
|
10
|
+
export function siblingModuleAnalysis(merged, currentUri) {
|
|
11
|
+
const functions = merged.functions.filter((f) => f.sourceUri !== currentUri);
|
|
12
|
+
const moduleVariables = merged.moduleVariables.filter((v) => v.sourceUri !== currentUri);
|
|
13
|
+
if (functions.length === 0 && moduleVariables.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
return { ...merged, functions, moduleVariables };
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Node } from "xq-parser";
|
|
2
|
+
import type { TypeDiagnostic, FileAnalysis } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Report an unsupported `xquery version` (XQST0031), serialization option
|
|
5
|
+
* misuse (XQST0108/0109/0110) and prolog components that the spec allows only once: setters such as
|
|
6
|
+
* `declare base-uri`, namespace prefixes, default element/function namespaces,
|
|
7
|
+
* decimal formats, and duplicate function parameter names.
|
|
8
|
+
* Only the second and later occurrences are flagged.
|
|
9
|
+
*/
|
|
10
|
+
export declare function checkDuplicatePrologDecls(ast: Node, analysis: FileAnalysis): TypeDiagnostic[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { qnameKey } from "./types.js";
|
|
2
|
+
import { findAll } from "./analyzer.js";
|
|
3
|
+
import { asVersionDecl, asOptionDecl, asNamespaceDecl, asDefaultNamespaceDecl, asDecimalFormatDecl, asFunctionDecl, asInlineFunctionExpr, } from "./ast-nodes.js";
|
|
4
|
+
/** The second and later items sharing a key with an earlier item. */
|
|
5
|
+
function repeats(items, key) {
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
return items.filter((item) => {
|
|
8
|
+
const k = key(item);
|
|
9
|
+
if (seen.has(k))
|
|
10
|
+
return true;
|
|
11
|
+
seen.add(k);
|
|
12
|
+
return false;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function diagnostic(node, code, message) {
|
|
16
|
+
const start = node.start ?? 0;
|
|
17
|
+
return { message, code, offset: start, length: (node.end ?? start) - start };
|
|
18
|
+
}
|
|
19
|
+
/** Setter declarations that may appear at most once per prolog. */
|
|
20
|
+
const SINGLE_USE_DECLS = [
|
|
21
|
+
{ type: "BaseURIDecl", code: "XQST0032", what: "base URI" },
|
|
22
|
+
{ type: "DefaultCollationDecl", code: "XQST0038", what: "default collation" },
|
|
23
|
+
{ type: "OrderingModeDecl", code: "XQST0065", what: "ordering mode" },
|
|
24
|
+
{ type: "ConstructionDecl", code: "XQST0067", what: "construction mode" },
|
|
25
|
+
{ type: "BoundarySpaceDecl", code: "XQST0068", what: "boundary-space policy" },
|
|
26
|
+
{ type: "EmptyOrderDecl", code: "XQST0069", what: "default empty order" },
|
|
27
|
+
];
|
|
28
|
+
const XMLNS_OUTPUT = "http://www.w3.org/2010/xslt-xquery-serialization";
|
|
29
|
+
/** Serialization parameters that may be set with `declare option output:*` (use-character-maps may not). */
|
|
30
|
+
const PROLOG_SERIALIZATION_PARAMS = new Set([
|
|
31
|
+
"allow-duplicate-names",
|
|
32
|
+
"byte-order-mark",
|
|
33
|
+
"cdata-section-elements",
|
|
34
|
+
"doctype-public",
|
|
35
|
+
"doctype-system",
|
|
36
|
+
"encoding",
|
|
37
|
+
"escape-uri-attributes",
|
|
38
|
+
"html-version",
|
|
39
|
+
"include-content-type",
|
|
40
|
+
"indent",
|
|
41
|
+
"item-separator",
|
|
42
|
+
"json-node-output-method",
|
|
43
|
+
"media-type",
|
|
44
|
+
"method",
|
|
45
|
+
"normalization-form",
|
|
46
|
+
"omit-xml-declaration",
|
|
47
|
+
"parameter-document",
|
|
48
|
+
"standalone",
|
|
49
|
+
"suppress-indentation",
|
|
50
|
+
"undeclare-prefixes",
|
|
51
|
+
"version",
|
|
52
|
+
]);
|
|
53
|
+
const SUPPORTED_XQUERY_VERSIONS = new Set(["1.0", "3.0", "3.1", "4.0"]);
|
|
54
|
+
/**
|
|
55
|
+
* Report an unsupported `xquery version` (XQST0031), serialization option
|
|
56
|
+
* misuse (XQST0108/0109/0110) and prolog components that the spec allows only once: setters such as
|
|
57
|
+
* `declare base-uri`, namespace prefixes, default element/function namespaces,
|
|
58
|
+
* decimal formats, and duplicate function parameter names.
|
|
59
|
+
* Only the second and later occurrences are flagged.
|
|
60
|
+
*/
|
|
61
|
+
export function checkDuplicatePrologDecls(ast, analysis) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const node of findAll(ast, "VersionDecl")) {
|
|
64
|
+
const decl = asVersionDecl(node);
|
|
65
|
+
if (decl && !SUPPORTED_XQUERY_VERSIONS.has(decl.version))
|
|
66
|
+
out.push(diagnostic(decl.versionNode, "XQST0031", `XQuery version '${decl.version}' is not supported`));
|
|
67
|
+
}
|
|
68
|
+
const isLibraryModule = findAll(ast, "LibraryModule").length > 0;
|
|
69
|
+
const outputOptions = findAll(ast, "OptionDecl").flatMap((node) => {
|
|
70
|
+
const decl = asOptionDecl(node, analysis);
|
|
71
|
+
return decl?.qname.namespaceUri === XMLNS_OUTPUT ? [{ node, name: decl.qname.localName }] : [];
|
|
72
|
+
});
|
|
73
|
+
for (const { node, name } of outputOptions) {
|
|
74
|
+
if (isLibraryModule)
|
|
75
|
+
out.push(diagnostic(node, "XQST0108", "Serialization parameters may only be declared in a main module"));
|
|
76
|
+
else if (!PROLOG_SERIALIZATION_PARAMS.has(name))
|
|
77
|
+
out.push(diagnostic(node, "XQST0109", `'${name}' is not a serialization parameter that can be declared in the prolog`));
|
|
78
|
+
}
|
|
79
|
+
for (const { node, name } of repeats(outputOptions, (o) => o.name))
|
|
80
|
+
out.push(diagnostic(node, "XQST0110", `Serialization parameter '${name}' is declared more than once`));
|
|
81
|
+
for (const { type, code, what } of SINGLE_USE_DECLS)
|
|
82
|
+
for (const node of repeats(findAll(ast, type), () => type))
|
|
83
|
+
out.push(diagnostic(node, code, `The ${what} is declared more than once`));
|
|
84
|
+
const namespaceDecls = findAll(ast, "NamespaceDecl").flatMap((node) => {
|
|
85
|
+
const decl = asNamespaceDecl(node);
|
|
86
|
+
return decl ? [{ node, ...decl }] : [];
|
|
87
|
+
});
|
|
88
|
+
for (const { node, prefix } of repeats(namespaceDecls, (d) => d.prefix))
|
|
89
|
+
out.push(diagnostic(node, "XQST0033", `Namespace prefix '${prefix}' is declared more than once`));
|
|
90
|
+
const defaultNsDecls = findAll(ast, "DefaultNamespaceDecl").flatMap((node) => {
|
|
91
|
+
const decl = asDefaultNamespaceDecl(node);
|
|
92
|
+
return decl ? [{ node, ...decl }] : [];
|
|
93
|
+
});
|
|
94
|
+
for (const { node, kind } of repeats(defaultNsDecls, (d) => d.kind))
|
|
95
|
+
out.push(diagnostic(node, "XQST0066", `The default ${kind} namespace is declared more than once`));
|
|
96
|
+
const decimalFormats = findAll(ast, "DecimalFormatDecl").flatMap((node) => {
|
|
97
|
+
const decl = asDecimalFormatDecl(node, analysis);
|
|
98
|
+
return decl ? [{ node, ...decl }] : [];
|
|
99
|
+
});
|
|
100
|
+
for (const { node } of repeats(decimalFormats, (d) => d.key))
|
|
101
|
+
out.push(diagnostic(node, "XQST0111", "This decimal format is declared more than once"));
|
|
102
|
+
for (const { properties } of decimalFormats)
|
|
103
|
+
for (const { nameNode, name } of repeats(properties, (p) => p.name))
|
|
104
|
+
out.push(diagnostic(nameNode, "XQST0114", `Decimal-format property '${name}' is specified more than once`));
|
|
105
|
+
const paramLists = [
|
|
106
|
+
...findAll(ast, "FunctionDecl").map((n) => asFunctionDecl(n, analysis)?.params),
|
|
107
|
+
...findAll(ast, "InlineFunctionExpr").map((n) => asInlineFunctionExpr(n, analysis)?.params),
|
|
108
|
+
];
|
|
109
|
+
for (const params of paramLists)
|
|
110
|
+
for (const { nameNode, qname } of repeats(params ?? [], (p) => qnameKey(p.qname)))
|
|
111
|
+
out.push(diagnostic(nameNode, "XQST0039", `Parameter '$${qname.localName}' is declared more than once`));
|
|
112
|
+
return out;
|
|
113
|
+
}
|
package/dist/qt4-worker.js
CHANGED
|
@@ -59,13 +59,15 @@ const batch = workerData;
|
|
|
59
59
|
const results = batch.map((tc) => {
|
|
60
60
|
const got = collectCodes(tc.query, tc.envNamespaces, tc.envVariables, tc.moduleCatalog);
|
|
61
61
|
const hasError = got.length > 0;
|
|
62
|
+
// A static analyzer may detect a dynamic error early: when the test expects a
|
|
63
|
+
// dynamic error (e.g. XPTY0004) and that is exactly what we reported, it's a pass.
|
|
62
64
|
const outcome = tc.expected === "static-error"
|
|
63
65
|
? hasError
|
|
64
66
|
? "pass"
|
|
65
67
|
: "false-negative"
|
|
66
|
-
: hasError
|
|
67
|
-
? "
|
|
68
|
-
: "
|
|
68
|
+
: !hasError || got.every((c) => c === tc.expectedCode)
|
|
69
|
+
? "pass"
|
|
70
|
+
: "false-positive";
|
|
69
71
|
return { testSetSlug: tc.testSetSlug, testCase: tc.testCase, outcome, expectedCode: tc.expectedCode, got };
|
|
70
72
|
});
|
|
71
73
|
parentPort.postMessage(results);
|
|
@@ -162,3 +162,98 @@ declare function fonto:document($remoteDocumentId as xs:string?) as node()? exte
|
|
|
162
162
|
: @see https://documentation.fontoxml.com/latest/fonto-remote-document-state-03df124b223a
|
|
163
163
|
:)
|
|
164
164
|
declare function fonto:remote-document-state($remoteDocumentId as xs:string) as map(*)? external;
|
|
165
|
+
|
|
166
|
+
(:~
|
|
167
|
+
: Returns the remoteDocumentId of the document that contains the given node.
|
|
168
|
+
: Throws an error when the node is not related to a document that has a remote document id.
|
|
169
|
+
: @param $node The node of which the remote document id should be looked up.
|
|
170
|
+
: @see https://documentation.fontoxml.com/latest/fonto-remote-document-id-c4f2b7d3ca21
|
|
171
|
+
:)
|
|
172
|
+
declare function fonto:remote-document-id($node as node()) as xs:string external;
|
|
173
|
+
|
|
174
|
+
(:~
|
|
175
|
+
: Returns the Fonto NodeId of the given node. Do not use this in operations.
|
|
176
|
+
: @param $node The node.
|
|
177
|
+
: @see https://documentation.fontoxml.com/latest/fonto-node-id-7b60a9382817
|
|
178
|
+
:)
|
|
179
|
+
declare function fonto:node-id($node as node()) as xs:string external;
|
|
180
|
+
|
|
181
|
+
(:~
|
|
182
|
+
: Returns true if the given node is read-only for any reason.
|
|
183
|
+
: @param $node The node to check.
|
|
184
|
+
: @see https://documentation.fontoxml.com/latest/fonto-is-node-read-only-72ca327d3704
|
|
185
|
+
:)
|
|
186
|
+
declare function fonto:is-node-read-only($node as node()) as xs:boolean external;
|
|
187
|
+
|
|
188
|
+
(:~
|
|
189
|
+
: Returns the hierarchy source node for the given hierarchy node id.
|
|
190
|
+
: @param $hierarchyNodeId The hierarchy node id.
|
|
191
|
+
: @see https://documentation.fontoxml.com/latest/fonto-hierarchy-source-node-346968934801
|
|
192
|
+
:)
|
|
193
|
+
declare function fonto:hierarchy-source-node($hierarchyNodeId as xs:string?) as node()? external;
|
|
194
|
+
|
|
195
|
+
(:~
|
|
196
|
+
: Evaluates the configured hierarchyChildNodesQuery for the given node.
|
|
197
|
+
: Returns the empty sequence if no node is given or no query is configured.
|
|
198
|
+
: @param $node The node for which to retrieve the hierarchy child nodes.
|
|
199
|
+
: @see https://documentation.fontoxml.com/latest/fonto-hierarchy-child-nodes-e46c9fb8f2c0
|
|
200
|
+
:)
|
|
201
|
+
declare function fonto:hierarchy-child-nodes($node as node()?) as node()* external;
|
|
202
|
+
|
|
203
|
+
(:~
|
|
204
|
+
: Returns the contents of the "title" of the given node, as configured with titleQuery.
|
|
205
|
+
: @param $node The node to resolve the contents of the title of.
|
|
206
|
+
: @see https://documentation.fontoxml.com/latest/fonto-title-content-1acac0f6c610
|
|
207
|
+
:)
|
|
208
|
+
declare function fonto:title-content($node as node()) as xs:string? external;
|
|
209
|
+
|
|
210
|
+
(:~
|
|
211
|
+
: Returns the direction of the "title" of the given node, as configured with titleDirectionQuery.
|
|
212
|
+
: @param $node The node to resolve the direction of the title of.
|
|
213
|
+
: @see https://documentation.fontoxml.com/latest/fonto-title-content-direction-975b104b0a82
|
|
214
|
+
:)
|
|
215
|
+
declare function fonto:title-content-direction($node as node()) as xs:string external;
|
|
216
|
+
|
|
217
|
+
(:~
|
|
218
|
+
: Returns the effective text direction ('ltr' or 'rtl') for the given node.
|
|
219
|
+
: Returns 'ltr' when no direction is configured or no node is passed.
|
|
220
|
+
: @param $node The node for which to get the effective direction.
|
|
221
|
+
: @see https://documentation.fontoxml.com/latest/fonto-direction-193c5dccadfc
|
|
222
|
+
:)
|
|
223
|
+
declare function fonto:direction($node as node()?) as xs:string external;
|
|
224
|
+
|
|
225
|
+
(:~
|
|
226
|
+
: Returns true if the given node is a table according to any table definition.
|
|
227
|
+
: @param $node The node to check.
|
|
228
|
+
: @see https://documentation.fontoxml.com/latest/fonto-is-table-a4faf751893d
|
|
229
|
+
:)
|
|
230
|
+
declare function fonto:is-table($node as node()) as xs:boolean external;
|
|
231
|
+
|
|
232
|
+
(:~
|
|
233
|
+
: Returns true if the given node is a table cell according to any table definition.
|
|
234
|
+
: @param $node The node to check.
|
|
235
|
+
: @see https://documentation.fontoxml.com/latest/fonto-is-table-cell-7d82e5b1f409
|
|
236
|
+
:)
|
|
237
|
+
declare function fonto:is-table-cell($node as node()) as xs:boolean external;
|
|
238
|
+
|
|
239
|
+
(:~
|
|
240
|
+
: Returns true for any CALS table element (tgroup) or CALS table figure element (table).
|
|
241
|
+
: @param $node The node to check.
|
|
242
|
+
: @see https://documentation.fontoxml.com/latest/fonto-is-cals-table-fd98acb9a49f
|
|
243
|
+
:)
|
|
244
|
+
declare function fonto:is-cals-table($node as node()?) as xs:boolean external;
|
|
245
|
+
|
|
246
|
+
(:~
|
|
247
|
+
: Returns true for any XHTML table element.
|
|
248
|
+
: @param $node The node to check.
|
|
249
|
+
: @see https://documentation.fontoxml.com/latest/fonto-is-xhtml-table-5e4d4e55a9df
|
|
250
|
+
:)
|
|
251
|
+
declare function fonto:is-xhtml-table($node as node()?) as xs:boolean external;
|
|
252
|
+
|
|
253
|
+
(:~
|
|
254
|
+
: Returns the column specification.
|
|
255
|
+
: @param $columnSpecifications The column specifications node.
|
|
256
|
+
: @param $columnIdentifier The column identifier.
|
|
257
|
+
: @see https://documentation.fontoxml.com/latest/fonto-column-spec-0d3684963eb6
|
|
258
|
+
:)
|
|
259
|
+
declare function fonto:column-spec($columnSpecifications as node(), $columnIdentifier as xs:string) as item()? external;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Node } from "xq-parser";
|
|
2
|
+
import type { FileAnalysis } from "./types.ts";
|
|
3
|
+
export declare const TOKEN_TYPES: readonly ["namespace", "function", "parameter", "variable"];
|
|
4
|
+
export declare const TOKEN_MODIFIERS: readonly ["defaultLibrary", "unused"];
|
|
5
|
+
export type SemanticTokenType = (typeof TOKEN_TYPES)[number];
|
|
6
|
+
export type SemanticTokenModifier = (typeof TOKEN_MODIFIERS)[number];
|
|
7
|
+
export declare const SEMANTIC_TOKENS_LEGEND: {
|
|
8
|
+
tokenTypes: ("function" | "variable" | "namespace" | "parameter")[];
|
|
9
|
+
tokenModifiers: ("defaultLibrary" | "unused")[];
|
|
10
|
+
};
|
|
11
|
+
export interface EncodedToken {
|
|
12
|
+
line: number;
|
|
13
|
+
character: number;
|
|
14
|
+
length: number;
|
|
15
|
+
tokenType: number;
|
|
16
|
+
tokenModifiers: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Delta-encode already-positioned tokens into the flat array required by
|
|
20
|
+
* `SemanticTokens.data` (see the LSP spec's `textDocument/semanticTokens/full`).
|
|
21
|
+
* Tokens are sorted by (line, character) first since the format is cumulative.
|
|
22
|
+
*/
|
|
23
|
+
export declare function encodeSemanticTokens(tokens: EncodedToken[]): number[];
|
|
24
|
+
/**
|
|
25
|
+
* Walk the AST once and produce the delta-encoded `data` array for
|
|
26
|
+
* `textDocument/semanticTokens/full`. Returns `[]` when `ast` is null
|
|
27
|
+
* (the file failed to parse — the regex-fallback analysis has no AST to walk).
|
|
28
|
+
*/
|
|
29
|
+
export declare function getSemanticTokensData(ast: Node | null, text: string, analysis: FileAnalysis, importedAnalyses: Map<string, FileAnalysis>): number[];
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { isTerminal, directChildOf, directChildrenOf, firstTerminalValue, BUILTIN_PREFIXES } from "./analyzer.js";
|
|
2
|
+
import { asFunctionCall, asNamedFunctionRef, asFunctionDecl, asVarDecl } from "./ast-nodes.js";
|
|
3
|
+
import { checkUnused } from "./unused-diagnostics.js";
|
|
4
|
+
// ── Legend ────────────────────────────────────────────────────────────────────
|
|
5
|
+
export const TOKEN_TYPES = ["namespace", "function", "parameter", "variable"];
|
|
6
|
+
export const TOKEN_MODIFIERS = ["defaultLibrary", "unused"];
|
|
7
|
+
export const SEMANTIC_TOKENS_LEGEND = {
|
|
8
|
+
tokenTypes: [...TOKEN_TYPES],
|
|
9
|
+
tokenModifiers: [...TOKEN_MODIFIERS],
|
|
10
|
+
};
|
|
11
|
+
const TOKEN_TYPE_INDEX = Object.fromEntries(TOKEN_TYPES.map((t, i) => [t, i]));
|
|
12
|
+
const TOKEN_MODIFIER_BIT = Object.fromEntries(TOKEN_MODIFIERS.map((m, i) => [m, 1 << i]));
|
|
13
|
+
function modifierBitmask(mods) {
|
|
14
|
+
return mods.reduce((acc, m) => acc | TOKEN_MODIFIER_BIT[m], 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Delta-encode already-positioned tokens into the flat array required by
|
|
18
|
+
* `SemanticTokens.data` (see the LSP spec's `textDocument/semanticTokens/full`).
|
|
19
|
+
* Tokens are sorted by (line, character) first since the format is cumulative.
|
|
20
|
+
*/
|
|
21
|
+
export function encodeSemanticTokens(tokens) {
|
|
22
|
+
const sorted = [...tokens].sort((a, b) => a.line - b.line || a.character - b.character);
|
|
23
|
+
const data = [];
|
|
24
|
+
let prevLine = 0;
|
|
25
|
+
let prevChar = 0;
|
|
26
|
+
for (const t of sorted) {
|
|
27
|
+
const deltaLine = t.line - prevLine;
|
|
28
|
+
const deltaChar = deltaLine === 0 ? t.character - prevChar : t.character;
|
|
29
|
+
data.push(deltaLine, deltaChar, t.length, t.tokenType, t.tokenModifiers);
|
|
30
|
+
prevLine = t.line;
|
|
31
|
+
prevChar = t.character;
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
// ── Offset → line/character ──────────────────────────────────────────────────
|
|
36
|
+
function computeLineStarts(text) {
|
|
37
|
+
const starts = [0];
|
|
38
|
+
for (let i = 0; i < text.length; i++)
|
|
39
|
+
if (text[i] === "\n")
|
|
40
|
+
starts.push(i + 1);
|
|
41
|
+
return starts;
|
|
42
|
+
}
|
|
43
|
+
function offsetToPosition(lineStarts, offset) {
|
|
44
|
+
let lo = 0;
|
|
45
|
+
let hi = lineStarts.length - 1;
|
|
46
|
+
while (lo < hi) {
|
|
47
|
+
const mid = (lo + hi + 1) >> 1;
|
|
48
|
+
if (lineStarts[mid] <= offset)
|
|
49
|
+
lo = mid;
|
|
50
|
+
else
|
|
51
|
+
hi = mid - 1;
|
|
52
|
+
}
|
|
53
|
+
return { line: lo, character: offset - lineStarts[lo] };
|
|
54
|
+
}
|
|
55
|
+
/** Mirrors resolvePrefix's precedence order (analyzer.ts) without re-resolving the URI. */
|
|
56
|
+
function isDefaultLibraryPrefix(prefix, analysis) {
|
|
57
|
+
if (analysis.imports.some((i) => i.prefix === prefix))
|
|
58
|
+
return false;
|
|
59
|
+
if (prefix === analysis.modulePrefix && analysis.moduleNamespaceUri)
|
|
60
|
+
return false;
|
|
61
|
+
const nd = analysis.namespaceDecls.find((n) => n.prefix === prefix);
|
|
62
|
+
if (nd)
|
|
63
|
+
return nd.offset === -1; // runtime-injected predeclared namespace, e.g. eXist-db
|
|
64
|
+
return prefix in BUILTIN_PREFIXES;
|
|
65
|
+
}
|
|
66
|
+
const builtinKey = (q) => `${q.namespaceUri}|${q.localName}`;
|
|
67
|
+
function buildBuiltinKeys(analysis, importedAnalyses) {
|
|
68
|
+
const keys = new Set();
|
|
69
|
+
for (const a of [analysis, ...importedAnalyses.values()])
|
|
70
|
+
for (const f of a.functions)
|
|
71
|
+
if (f.sourceUri.startsWith("builtin:"))
|
|
72
|
+
keys.add(builtinKey(f.qname));
|
|
73
|
+
return keys;
|
|
74
|
+
}
|
|
75
|
+
/** Splits a raw EQName/VarName/FunctionEQName node's text into its prefix and local-name spans. */
|
|
76
|
+
function splitPrefixedName(node) {
|
|
77
|
+
const raw = firstTerminalValue(node);
|
|
78
|
+
if (!raw)
|
|
79
|
+
return null;
|
|
80
|
+
if (raw.startsWith("Q{")) {
|
|
81
|
+
const close = raw.indexOf("}");
|
|
82
|
+
if (close < 0)
|
|
83
|
+
return null;
|
|
84
|
+
return { local: { offset: node.start + close + 1, length: raw.length - close - 1 } };
|
|
85
|
+
}
|
|
86
|
+
const colon = raw.indexOf(":");
|
|
87
|
+
if (colon <= 0)
|
|
88
|
+
return { local: { offset: node.start, length: raw.length } };
|
|
89
|
+
return {
|
|
90
|
+
prefix: { offset: node.start, length: colon },
|
|
91
|
+
local: { offset: node.start + colon + 1, length: raw.length - colon - 1 },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function pushNameTokens(node, kind, analysis, modifiers, out) {
|
|
95
|
+
const split = splitPrefixedName(node);
|
|
96
|
+
if (!split)
|
|
97
|
+
return;
|
|
98
|
+
if (split.prefix) {
|
|
99
|
+
const raw = firstTerminalValue(node) ?? "";
|
|
100
|
+
const prefixText = raw.slice(0, split.prefix.length);
|
|
101
|
+
const nsModifiers = isDefaultLibraryPrefix(prefixText, analysis) ? ["defaultLibrary"] : [];
|
|
102
|
+
out.push({ offset: split.prefix.offset, length: split.prefix.length, type: "namespace", modifiers: nsModifiers });
|
|
103
|
+
}
|
|
104
|
+
out.push({ offset: split.local.offset, length: split.local.length, type: kind, modifiers });
|
|
105
|
+
}
|
|
106
|
+
/** Element/attribute constructors: only the namespace-prefix part is classifiable (no "element" token type). */
|
|
107
|
+
function pushNamespaceOnly(node, analysis, out) {
|
|
108
|
+
if (!node)
|
|
109
|
+
return;
|
|
110
|
+
const raw = firstTerminalValue(node);
|
|
111
|
+
if (!raw || raw.startsWith("Q{") || raw.startsWith("xmlns"))
|
|
112
|
+
return;
|
|
113
|
+
const colon = raw.indexOf(":");
|
|
114
|
+
if (colon <= 0)
|
|
115
|
+
return;
|
|
116
|
+
const modifiers = isDefaultLibraryPrefix(raw.slice(0, colon), analysis) ? ["defaultLibrary"] : [];
|
|
117
|
+
out.push({ offset: node.start, length: colon, type: "namespace", modifiers });
|
|
118
|
+
}
|
|
119
|
+
function buildUnusedOffsets(ast, analysis) {
|
|
120
|
+
const offsets = new Set();
|
|
121
|
+
for (const d of checkUnused(ast, analysis))
|
|
122
|
+
if (d.code === "xq-lsp:unused-function" || d.code === "xq-lsp:unused-variable")
|
|
123
|
+
offsets.add(d.offset);
|
|
124
|
+
return offsets;
|
|
125
|
+
}
|
|
126
|
+
// ── AST walk ──────────────────────────────────────────────────────────────────
|
|
127
|
+
function walk(node, analysis, builtinKeys, unusedOffsets, out) {
|
|
128
|
+
if (isTerminal(node))
|
|
129
|
+
return;
|
|
130
|
+
const { children } = node;
|
|
131
|
+
switch (node.type) {
|
|
132
|
+
case "FunctionCall": {
|
|
133
|
+
const call = asFunctionCall(node, analysis);
|
|
134
|
+
const eqname = directChildOf(node, "FunctionEQName");
|
|
135
|
+
if (call && eqname) {
|
|
136
|
+
const mods = builtinKeys.has(builtinKey(call.qname)) ? ["defaultLibrary"] : [];
|
|
137
|
+
pushNameTokens(eqname, "function", analysis, mods, out);
|
|
138
|
+
}
|
|
139
|
+
for (const c of children)
|
|
140
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
case "NamedFunctionRef": {
|
|
144
|
+
const ref = asNamedFunctionRef(node, analysis);
|
|
145
|
+
const eqname = directChildOf(node, "EQName");
|
|
146
|
+
if (ref && eqname) {
|
|
147
|
+
const mods = builtinKeys.has(builtinKey(ref.qname)) ? ["defaultLibrary"] : [];
|
|
148
|
+
pushNameTokens(eqname, "function", analysis, mods, out);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
case "VarRef": {
|
|
153
|
+
const varNameNode = directChildOf(node, "VarName");
|
|
154
|
+
if (varNameNode)
|
|
155
|
+
pushNameTokens(varNameNode, "variable", analysis, [], out);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
case "AnnotatedDecl": {
|
|
159
|
+
const fnDeclNode = directChildOf(node, "FunctionDecl");
|
|
160
|
+
if (fnDeclNode) {
|
|
161
|
+
const fn = asFunctionDecl(fnDeclNode, analysis);
|
|
162
|
+
if (fn) {
|
|
163
|
+
const mods = unusedOffsets.has(fn.nameNode.start) ? ["unused"] : [];
|
|
164
|
+
pushNameTokens(fn.nameNode, "function", analysis, mods, out);
|
|
165
|
+
const paramList = directChildOf(fnDeclNode, "ParamList");
|
|
166
|
+
for (const p of paramList ? directChildrenOf(paramList, "Param") : []) {
|
|
167
|
+
const peq = directChildOf(p, "EQName");
|
|
168
|
+
if (peq)
|
|
169
|
+
pushNameTokens(peq, "parameter", analysis, [], out);
|
|
170
|
+
}
|
|
171
|
+
if (fn.body)
|
|
172
|
+
walk(fn.body, analysis, builtinKeys, unusedOffsets, out);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const varDeclNode = directChildOf(node, "VarDecl");
|
|
177
|
+
if (varDeclNode) {
|
|
178
|
+
const v = asVarDecl(varDeclNode, analysis);
|
|
179
|
+
if (v) {
|
|
180
|
+
const mods = unusedOffsets.has(v.nameNode.start) ? ["unused"] : [];
|
|
181
|
+
pushNameTokens(v.nameNode, "variable", analysis, mods, out);
|
|
182
|
+
}
|
|
183
|
+
for (const c of varDeclNode.children)
|
|
184
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const c of children)
|
|
188
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case "LetBinding":
|
|
192
|
+
case "ForBinding": {
|
|
193
|
+
const varNameNode = directChildOf(node, "VarName");
|
|
194
|
+
if (varNameNode)
|
|
195
|
+
pushNameTokens(varNameNode, "variable", analysis, [], out);
|
|
196
|
+
for (const c of children)
|
|
197
|
+
if (c !== varNameNode)
|
|
198
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
case "InlineFunctionExpr": {
|
|
202
|
+
const paramList = directChildOf(node, "ParamList");
|
|
203
|
+
for (const p of paramList ? directChildrenOf(paramList, "Param") : []) {
|
|
204
|
+
const peq = directChildOf(p, "EQName");
|
|
205
|
+
if (peq)
|
|
206
|
+
pushNameTokens(peq, "parameter", analysis, [], out);
|
|
207
|
+
}
|
|
208
|
+
const body = directChildOf(node, "FunctionBody");
|
|
209
|
+
if (body)
|
|
210
|
+
walk(body, analysis, builtinKeys, unusedOffsets, out);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case "DirElemConstructor":
|
|
214
|
+
pushNamespaceOnly(directChildOf(node, "QName"), analysis, out);
|
|
215
|
+
for (const c of children)
|
|
216
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
217
|
+
return;
|
|
218
|
+
case "CompElemConstructor":
|
|
219
|
+
case "CompAttrConstructor":
|
|
220
|
+
pushNamespaceOnly(directChildOf(node, "EQName"), analysis, out);
|
|
221
|
+
for (const c of children)
|
|
222
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
223
|
+
return;
|
|
224
|
+
default:
|
|
225
|
+
for (const c of children)
|
|
226
|
+
walk(c, analysis, builtinKeys, unusedOffsets, out);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ── Public entry point ────────────────────────────────────────────────────────
|
|
230
|
+
/**
|
|
231
|
+
* Walk the AST once and produce the delta-encoded `data` array for
|
|
232
|
+
* `textDocument/semanticTokens/full`. Returns `[]` when `ast` is null
|
|
233
|
+
* (the file failed to parse — the regex-fallback analysis has no AST to walk).
|
|
234
|
+
*/
|
|
235
|
+
export function getSemanticTokensData(ast, text, analysis, importedAnalyses) {
|
|
236
|
+
if (!ast)
|
|
237
|
+
return [];
|
|
238
|
+
const builtinKeys = buildBuiltinKeys(analysis, importedAnalyses);
|
|
239
|
+
const unusedOffsets = buildUnusedOffsets(ast, analysis);
|
|
240
|
+
const rawTokens = [];
|
|
241
|
+
walk(ast, analysis, builtinKeys, unusedOffsets, rawTokens);
|
|
242
|
+
const lineStarts = computeLineStarts(text);
|
|
243
|
+
const encoded = rawTokens.map((t) => {
|
|
244
|
+
const pos = offsetToPosition(lineStarts, t.offset);
|
|
245
|
+
return {
|
|
246
|
+
line: pos.line,
|
|
247
|
+
character: pos.character,
|
|
248
|
+
length: t.length,
|
|
249
|
+
tokenType: TOKEN_TYPE_INDEX[t.type],
|
|
250
|
+
tokenModifiers: modifierBitmask(t.modifiers),
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
return encodeSemanticTokens(encoded);
|
|
254
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createConnection, TextDocuments, ProposedFeatures, TextDocumentSyncKind, DiagnosticSeverity, CodeAction, CodeActionKind, TextEdit, ResponseError, ErrorCodes, } from "vscode-languageserver/node.js";
|
|
2
|
+
import { createConnection, TextDocuments, ProposedFeatures, TextDocumentSyncKind, DiagnosticSeverity, DocumentDiagnosticReportKind, CodeAction, CodeActionKind, TextEdit, ResponseError, ErrorCodes, } from "vscode-languageserver/node.js";
|
|
3
3
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as fs from "fs";
|
|
@@ -21,6 +21,8 @@ import { runDiagnostics, runHints } from "./diagnostics.js";
|
|
|
21
21
|
import { getRuntimeAnalyses, getRuntimePredeclaredNamespaces, withPredeclaredNs, getRuntimePredeclaredVariables, withPredeclaredVariables, } from "./runtimes.js";
|
|
22
22
|
import { buildOrganizeImportsEdit, buildExtractVariableEdit, buildExtractFunctionEdit } from "./refactor-actions.js";
|
|
23
23
|
import { getWorkspaceSymbols } from "./workspace-symbol.js";
|
|
24
|
+
import { siblingModuleAnalysis } from "./module-siblings.js";
|
|
25
|
+
import { getSemanticTokensData, SEMANTIC_TOKENS_LEGEND } from "./semantic-tokens.js";
|
|
24
26
|
const connection = createConnection(ProposedFeatures.all);
|
|
25
27
|
const documents = new TextDocuments(TextDocument);
|
|
26
28
|
const analysisCache = new Map();
|
|
@@ -215,6 +217,13 @@ function resolveContext(currentUri, rawAnalysis) {
|
|
|
215
217
|
result.set(imp.namespaceUri, imported);
|
|
216
218
|
}
|
|
217
219
|
}
|
|
220
|
+
// Other files implementing the same module namespace are in scope without an import.
|
|
221
|
+
if (rawAnalysis.moduleNamespaceUri) {
|
|
222
|
+
const merged = globAnalyses.get(rawAnalysis.moduleNamespaceUri);
|
|
223
|
+
const siblings = merged && siblingModuleAnalysis(merged, currentUri);
|
|
224
|
+
if (siblings)
|
|
225
|
+
result.set(rawAnalysis.moduleNamespaceUri, siblings);
|
|
226
|
+
}
|
|
218
227
|
// Add pre-declared runtime analyses directly — available without explicit import
|
|
219
228
|
for (const nd of predeclaredNs) {
|
|
220
229
|
if (!result.has(nd.namespaceUri)) {
|
|
@@ -257,11 +266,12 @@ connection.onInitialize((params) => {
|
|
|
257
266
|
documentLinkProvider: { resolveProvider: false },
|
|
258
267
|
callHierarchyProvider: true,
|
|
259
268
|
inlayHintProvider: true,
|
|
269
|
+
diagnosticProvider: { interFileDependencies: true, workspaceDiagnostics: false },
|
|
270
|
+
semanticTokensProvider: { legend: SEMANTIC_TOKENS_LEGEND, full: true },
|
|
260
271
|
},
|
|
261
272
|
};
|
|
262
273
|
});
|
|
263
|
-
|
|
264
|
-
const doc = change.document;
|
|
274
|
+
function computeDiagnostics(doc) {
|
|
265
275
|
const { analysis: rawAnalysis, parseDiagnostics: parseDiags, hasAst, ast } = analyzeDocumentFull(doc);
|
|
266
276
|
const { analysis, imported: resolvedImported } = resolveContext(doc.uri, rawAnalysis);
|
|
267
277
|
// Run all error-level diagnostics and hints via the central registry.
|
|
@@ -285,10 +295,25 @@ documents.onDidChangeContent((change) => {
|
|
|
285
295
|
// nsDiags already covers XQST0081 (with the code-action data field), so exclude those here.
|
|
286
296
|
const errorDiags = errorDiagRaw.filter((d) => d.code !== "XQST0081").map((d) => toLsp(d, DiagnosticSeverity.Error));
|
|
287
297
|
const hintDiags = hintDiagRaw.map((d) => toLsp(d, DiagnosticSeverity.Hint));
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
});
|
|
298
|
+
return [...parseDiags, ...nsDiags, ...errorDiags, ...hintDiags];
|
|
299
|
+
}
|
|
300
|
+
documents.onDidChangeContent((change) => {
|
|
301
|
+
connection.sendDiagnostics({ uri: change.document.uri, diagnostics: computeDiagnostics(change.document) });
|
|
302
|
+
});
|
|
303
|
+
// LSP 3.17 pull diagnostics. Falls back to reading the file from disk so clients that edit
|
|
304
|
+
// files outside an editor session (no didOpen) can still ask "does this file have errors?".
|
|
305
|
+
connection.languages.diagnostics.on((params) => {
|
|
306
|
+
let doc = documents.get(params.textDocument.uri);
|
|
307
|
+
if (!doc) {
|
|
308
|
+
try {
|
|
309
|
+
const text = fs.readFileSync(uriToPath(params.textDocument.uri), "utf-8");
|
|
310
|
+
doc = TextDocument.create(params.textDocument.uri, "xquery", 0, text);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return { kind: DocumentDiagnosticReportKind.Full, items: [] };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { kind: DocumentDiagnosticReportKind.Full, items: computeDiagnostics(doc) };
|
|
292
317
|
});
|
|
293
318
|
connection.onCompletion((params) => {
|
|
294
319
|
const doc = documents.get(params.textDocument.uri);
|
|
@@ -479,6 +504,14 @@ function matchesOnly(kind, only) {
|
|
|
479
504
|
function toEditRange(doc, edit) {
|
|
480
505
|
return { range: { start: doc.positionAt(edit.start), end: doc.positionAt(edit.end) }, newText: edit.newText };
|
|
481
506
|
}
|
|
507
|
+
connection.languages.semanticTokens.on((params) => {
|
|
508
|
+
const doc = documents.get(params.textDocument.uri);
|
|
509
|
+
if (!doc)
|
|
510
|
+
return { data: [] };
|
|
511
|
+
const { analysis: rawAnalysis, ast } = analyzeDocumentFull(doc);
|
|
512
|
+
const { analysis, imported } = resolveContext(doc.uri, rawAnalysis);
|
|
513
|
+
return { data: getSemanticTokensData(ast, doc.getText(), analysis, imported) };
|
|
514
|
+
});
|
|
482
515
|
connection.onCodeAction((params) => {
|
|
483
516
|
const doc = documents.get(params.textDocument.uri);
|
|
484
517
|
if (!doc)
|