@xano/xanoscript-language-server 11.8.4 → 11.8.5
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/.claude/settings.local.json +2 -1
- package/cache/documentCache.js +58 -10
- package/lexer/db.js +1 -2
- package/lexer/security.js +16 -0
- package/onCompletion/onCompletion.js +61 -1
- package/onDefinition/onDefinition.js +150 -0
- package/onDefinition/onDefinition.spec.js +313 -0
- package/onDidChangeContent/onDidChangeContent.js +52 -5
- package/onHover/functions.md +28 -0
- package/package.json +1 -1
- package/parser/base_parser.js +61 -3
- package/parser/clauses/middlewareClause.js +16 -0
- package/parser/definitions/columnDefinition.js +5 -0
- package/parser/functions/api/apiCallFn.js +5 -3
- package/parser/functions/controls/functionCallFn.js +5 -3
- package/parser/functions/controls/functionRunFn.js +61 -5
- package/parser/functions/controls/taskCallFn.js +5 -3
- package/parser/functions/db/captureFieldName.js +63 -0
- package/parser/functions/db/dbAddFn.js +5 -3
- package/parser/functions/db/dbAddOrEditFn.js +13 -3
- package/parser/functions/db/dbBulkAddFn.js +5 -3
- package/parser/functions/db/dbBulkDeleteFn.js +5 -3
- package/parser/functions/db/dbBulkPatchFn.js +5 -3
- package/parser/functions/db/dbBulkUpdateFn.js +5 -3
- package/parser/functions/db/dbDelFn.js +10 -3
- package/parser/functions/db/dbEditFn.js +13 -3
- package/parser/functions/db/dbGetFn.js +10 -3
- package/parser/functions/db/dbHasFn.js +9 -3
- package/parser/functions/db/dbPatchFn.js +10 -3
- package/parser/functions/db/dbQueryFn.js +29 -3
- package/parser/functions/db/dbSchemaFn.js +5 -3
- package/parser/functions/db/dbTruncateFn.js +5 -3
- package/parser/functions/middlewareCallFn.js +3 -1
- package/parser/functions/security/register.js +19 -9
- package/parser/functions/security/securityCreateAuthTokenFn.js +22 -0
- package/parser/functions/security/securityJweDecodeLegacyFn.js +24 -0
- package/parser/functions/security/securityJweDecodeLegacyFn.spec.js +26 -0
- package/parser/functions/security/securityJweEncodeLegacyFn.js +24 -0
- package/parser/functions/security/securityJweEncodeLegacyFn.spec.js +25 -0
- package/parser/functions/securityFn.js +2 -0
- package/parser/functions/varFn.js +1 -1
- package/parser/generic/asVariable.js +2 -0
- package/parser/generic/assignableVariableAs.js +1 -0
- package/parser/generic/assignableVariableProperty.js +5 -2
- package/parser/tests/variable_test/coverage_check.xs +293 -0
- package/parser/variableScanner.js +64 -0
- package/parser/variableValidator.js +44 -0
- package/parser/variableValidator.spec.js +179 -0
- package/server.js +164 -10
- package/utils.js +32 -0
- package/utils.spec.js +93 -1
- package/workspace/crossFileValidator.js +166 -0
- package/workspace/crossFileValidator.spec.js +654 -0
- package/workspace/referenceTracking.spec.js +420 -0
- package/workspace/workspaceIndex.js +149 -0
- package/workspace/workspaceIndex.spec.js +189 -0
package/cache/documentCache.js
CHANGED
|
@@ -7,10 +7,13 @@ import { getSchemeFromContent } from "../utils.js";
|
|
|
7
7
|
* Stores parse results keyed by document URI + version.
|
|
8
8
|
* Note: We cache a snapshot of parser state, not the parser instance itself,
|
|
9
9
|
* since the parser is a singleton that gets mutated on each parse.
|
|
10
|
+
*
|
|
11
|
+
* lexResult (token arrays) are NOT cached to reduce memory footprint.
|
|
12
|
+
* Lexing is fast (~1ms) and can be repeated when needed.
|
|
10
13
|
*/
|
|
11
14
|
class DocumentCache {
|
|
12
15
|
constructor() {
|
|
13
|
-
// Map<uri, { version: number,
|
|
16
|
+
// Map<uri, { version: number, textLength: number, parserState: Object, scheme: string }>
|
|
14
17
|
this.cache = new Map();
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -31,9 +34,9 @@ class DocumentCache {
|
|
|
31
34
|
cached.version === version &&
|
|
32
35
|
cached.textLength === textLength
|
|
33
36
|
) {
|
|
34
|
-
//
|
|
37
|
+
// Re-lex (cheap) to provide tokens; return cached parser state
|
|
35
38
|
return {
|
|
36
|
-
lexResult:
|
|
39
|
+
lexResult: lexDocument(text),
|
|
37
40
|
parser: cached.parserState,
|
|
38
41
|
scheme: cached.scheme,
|
|
39
42
|
};
|
|
@@ -44,21 +47,66 @@ class DocumentCache {
|
|
|
44
47
|
const lexResult = lexDocument(text);
|
|
45
48
|
const parser = xanoscriptParser(text, scheme, lexResult);
|
|
46
49
|
|
|
47
|
-
//
|
|
50
|
+
// Snapshot only the parser state we need - not the token arrays.
|
|
51
|
+
// Use structured clone to avoid retaining references to parser internals.
|
|
48
52
|
const parserState = {
|
|
49
|
-
errors:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
errors: parser.errors.map((e) => ({
|
|
54
|
+
message: e.message,
|
|
55
|
+
token: e.token
|
|
56
|
+
? {
|
|
57
|
+
startOffset: e.token.startOffset,
|
|
58
|
+
endOffset: e.token.endOffset,
|
|
59
|
+
}
|
|
60
|
+
: null,
|
|
61
|
+
})),
|
|
62
|
+
warnings: parser.warnings.map((w) => ({
|
|
63
|
+
message: w.message,
|
|
64
|
+
token: w.token
|
|
65
|
+
? {
|
|
66
|
+
startOffset: w.token.startOffset,
|
|
67
|
+
endOffset: w.token.endOffset,
|
|
68
|
+
}
|
|
69
|
+
: null,
|
|
70
|
+
})),
|
|
71
|
+
informations: parser.informations.map((i) => ({
|
|
72
|
+
message: i.message,
|
|
73
|
+
token: i.token
|
|
74
|
+
? {
|
|
75
|
+
startOffset: i.token.startOffset,
|
|
76
|
+
endOffset: i.token.endOffset,
|
|
77
|
+
}
|
|
78
|
+
: null,
|
|
79
|
+
})),
|
|
80
|
+
hints: parser.hints.map((h) => ({
|
|
81
|
+
message: h.message,
|
|
82
|
+
token: h.token
|
|
83
|
+
? {
|
|
84
|
+
startOffset: h.token.startOffset,
|
|
85
|
+
endOffset: h.token.endOffset,
|
|
86
|
+
}
|
|
87
|
+
: null,
|
|
88
|
+
})),
|
|
53
89
|
__symbolTable: parser.__symbolTable
|
|
54
|
-
?
|
|
90
|
+
? {
|
|
91
|
+
input: { ...parser.__symbolTable.input },
|
|
92
|
+
var: { ...parser.__symbolTable.var },
|
|
93
|
+
auth: { ...parser.__symbolTable.auth },
|
|
94
|
+
env: { ...parser.__symbolTable.env },
|
|
95
|
+
references: parser.__symbolTable.references.map((r) => ({
|
|
96
|
+
...r,
|
|
97
|
+
args: r.args ? { ...r.args } : undefined,
|
|
98
|
+
dataKeys: r.dataKeys ? [...r.dataKeys] : undefined,
|
|
99
|
+
})),
|
|
100
|
+
varDeclarations: parser.__symbolTable.varDeclarations.map((d) => ({
|
|
101
|
+
...d,
|
|
102
|
+
})),
|
|
103
|
+
}
|
|
55
104
|
: null,
|
|
56
105
|
};
|
|
57
106
|
|
|
58
107
|
const cacheEntry = {
|
|
59
108
|
version,
|
|
60
109
|
textLength,
|
|
61
|
-
lexResult,
|
|
62
110
|
parserState,
|
|
63
111
|
scheme,
|
|
64
112
|
};
|
package/lexer/db.js
CHANGED
|
@@ -190,9 +190,8 @@ export function mapTokenToType(token) {
|
|
|
190
190
|
case TruncateToken.name:
|
|
191
191
|
case DirectQueryToken.name:
|
|
192
192
|
case SetDatasourceToken.name:
|
|
193
|
-
return "variable";
|
|
194
193
|
case WhereToken.name:
|
|
195
|
-
return;
|
|
194
|
+
return "variable";
|
|
196
195
|
default:
|
|
197
196
|
return null;
|
|
198
197
|
}
|
package/lexer/security.js
CHANGED
|
@@ -85,6 +85,18 @@ export const JweEncodeToken = createTokenByName("jwe_encode", {
|
|
|
85
85
|
categories: [Identifier],
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
// jwe_decode_legacy
|
|
89
|
+
export const JweDecodeLegacyToken = createTokenByName("jwe_decode_legacy", {
|
|
90
|
+
longer_alt: Identifier,
|
|
91
|
+
categories: [Identifier],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// jwe_encode_legacy
|
|
95
|
+
export const JweEncodeLegacyToken = createTokenByName("jwe_encode_legacy", {
|
|
96
|
+
longer_alt: Identifier,
|
|
97
|
+
categories: [Identifier],
|
|
98
|
+
});
|
|
99
|
+
|
|
88
100
|
// jws_decode
|
|
89
101
|
export const JwsDecodeToken = createTokenByName("jws_decode", {
|
|
90
102
|
longer_alt: Identifier,
|
|
@@ -121,6 +133,8 @@ export const SecurityTokens = [
|
|
|
121
133
|
EncryptToken,
|
|
122
134
|
GeneratePassToken,
|
|
123
135
|
GenerateUuidToken,
|
|
136
|
+
JweDecodeLegacyToken,
|
|
137
|
+
JweEncodeLegacyToken,
|
|
124
138
|
JweDecodeToken,
|
|
125
139
|
JweEncodeToken,
|
|
126
140
|
JwsDecodeToken,
|
|
@@ -150,6 +164,8 @@ export function mapTokenToType(token) {
|
|
|
150
164
|
case GenerateUuidToken.name:
|
|
151
165
|
case JweDecodeToken.name:
|
|
152
166
|
case JweEncodeToken.name:
|
|
167
|
+
case JweDecodeLegacyToken.name:
|
|
168
|
+
case JweEncodeLegacyToken.name:
|
|
153
169
|
case JwsDecodeToken.name:
|
|
154
170
|
case JwsEncodeToken.name:
|
|
155
171
|
case RandomBytesToken.name:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mapToVirtualJS } from "../embedded/embeddedContent.js";
|
|
2
2
|
import { getSchemeFromContent } from "../utils.js";
|
|
3
|
+
import { workspaceIndex } from "../workspace/workspaceIndex.js";
|
|
3
4
|
import { getContentAssistSuggestions } from "./contentAssist.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -33,6 +34,65 @@ export function onCompletion(params, documents) {
|
|
|
33
34
|
|
|
34
35
|
// Otherwise, handle as regular XanoScript
|
|
35
36
|
const scheme = getSchemeFromContent(text);
|
|
37
|
+
const prefix = text.slice(0, offset);
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const suggestions = getContentAssistSuggestions(prefix, scheme);
|
|
40
|
+
|
|
41
|
+
// Add workspace object name completions
|
|
42
|
+
const workspaceCompletions = getWorkspaceCompletions(prefix);
|
|
43
|
+
if (workspaceCompletions) {
|
|
44
|
+
const existing = suggestions || [];
|
|
45
|
+
return [...existing, ...workspaceCompletions];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return suggestions;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get completions for cross-file references based on cursor context.
|
|
53
|
+
* @param {string} prefix - Text before cursor
|
|
54
|
+
* @returns {Array|null} Completion items or null if not in a reference context
|
|
55
|
+
*/
|
|
56
|
+
function getWorkspaceCompletions(prefix) {
|
|
57
|
+
// function.run " or function.call " context
|
|
58
|
+
if (/function\.(run|call)\s+"[^"]*$/.test(prefix)) {
|
|
59
|
+
return workspaceIndex.getAllNames("function").map((name) => ({
|
|
60
|
+
label: name,
|
|
61
|
+
kind: 3, // Function
|
|
62
|
+
detail: "function",
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// task.call " context
|
|
67
|
+
if (/task\.call\s+"[^"]*$/.test(prefix)) {
|
|
68
|
+
return workspaceIndex.getAllNames("task").map((name) => ({
|
|
69
|
+
label: name,
|
|
70
|
+
kind: 3,
|
|
71
|
+
detail: "task",
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// db.* table name context
|
|
76
|
+
if (
|
|
77
|
+
/\b(get|add|edit|delete|query|patch|has|truncate|add_or_edit|schema)\s+("?[^"\s{]*$)/.test(
|
|
78
|
+
prefix
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
return workspaceIndex.getAllNames("table").map((name) => ({
|
|
82
|
+
label: name,
|
|
83
|
+
kind: 7, // Class (for table)
|
|
84
|
+
detail: "table",
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// api.call " context
|
|
89
|
+
if (/api\.call\s+"[^"]*$/.test(prefix)) {
|
|
90
|
+
return workspaceIndex.getAllNames("query").map((name) => ({
|
|
91
|
+
label: name,
|
|
92
|
+
kind: 3,
|
|
93
|
+
detail: "query",
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
38
98
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { lexDocument } from "../lexer/lexer.js";
|
|
2
|
+
import { LongFormVariable, ShortFormVariable } from "../lexer/variables.js";
|
|
3
|
+
import { getVarName } from "../parser/generic/utils.js";
|
|
4
|
+
import { xanoscriptParser } from "../parser/parser.js";
|
|
5
|
+
|
|
6
|
+
// Token names that precede a table name reference
|
|
7
|
+
const DB_OPERATION_TOKENS = new Set([
|
|
8
|
+
"get",
|
|
9
|
+
"add",
|
|
10
|
+
"edit",
|
|
11
|
+
"delete",
|
|
12
|
+
"query",
|
|
13
|
+
"patch",
|
|
14
|
+
"has",
|
|
15
|
+
"truncate",
|
|
16
|
+
"add_or_edit",
|
|
17
|
+
"schema",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const SHORT_FORM = ShortFormVariable.name;
|
|
21
|
+
const LONG_FORM = LongFormVariable.name;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find the definition location for the symbol at the given offset.
|
|
25
|
+
* @param {string} text - Document text
|
|
26
|
+
* @param {number} offset - Cursor offset in document
|
|
27
|
+
* @param {import('../workspace/workspaceIndex.js').WorkspaceIndex} index
|
|
28
|
+
* @param {Object} [symbolTable] - Parsed symbol table (optional, for variable lookups)
|
|
29
|
+
* @returns {{ uri: string, offset?: number } | null}
|
|
30
|
+
*/
|
|
31
|
+
export function findDefinition(text, offset, index, symbolTable) {
|
|
32
|
+
const lexResult = lexDocument(text);
|
|
33
|
+
const tokens = lexResult.tokens;
|
|
34
|
+
|
|
35
|
+
// Find the token at the cursor position
|
|
36
|
+
let tokenIndex = -1;
|
|
37
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
38
|
+
if (tokens[i].startOffset <= offset && offset <= tokens[i].endOffset) {
|
|
39
|
+
tokenIndex = i;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (tokenIndex < 0) return null;
|
|
45
|
+
|
|
46
|
+
const token = tokens[tokenIndex];
|
|
47
|
+
const typeName = token.tokenType.name;
|
|
48
|
+
|
|
49
|
+
// Variable go-to-definition
|
|
50
|
+
if (typeName === SHORT_FORM || typeName === LONG_FORM) {
|
|
51
|
+
// Parse if no symbol table provided
|
|
52
|
+
const st =
|
|
53
|
+
symbolTable ||
|
|
54
|
+
xanoscriptParser(text, undefined, lexResult).__symbolTable;
|
|
55
|
+
return findVariableDefinition(tokens, tokenIndex, st);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For LongFormVariable ($var), check if cursor is on the identifier after $var.
|
|
59
|
+
// e.g., "$var.foo" — cursor on "foo" (an Identifier token after DotToken after LongFormVariable)
|
|
60
|
+
if (
|
|
61
|
+
tokenIndex >= 2 &&
|
|
62
|
+
tokens[tokenIndex - 1].image === "." &&
|
|
63
|
+
tokens[tokenIndex - 2].tokenType.name === LONG_FORM
|
|
64
|
+
) {
|
|
65
|
+
const st =
|
|
66
|
+
symbolTable ||
|
|
67
|
+
xanoscriptParser(text, undefined, lexResult).__symbolTable;
|
|
68
|
+
return findVariableDefinition(tokens, tokenIndex, st);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const name = getVarName(token);
|
|
72
|
+
|
|
73
|
+
// Look backwards to determine context (cross-file references)
|
|
74
|
+
for (let i = tokenIndex - 1; i >= 0 && i >= tokenIndex - 3; i--) {
|
|
75
|
+
const prevImage = tokens[i].image.toLowerCase();
|
|
76
|
+
|
|
77
|
+
if (prevImage === "run" || prevImage === "call") {
|
|
78
|
+
// Check what's before: function. or task. or api.
|
|
79
|
+
if (i >= 2 && tokens[i - 1].image === ".") {
|
|
80
|
+
const prefix = tokens[i - 2].image.toLowerCase();
|
|
81
|
+
if (prefix === "function") {
|
|
82
|
+
const entry = index.get("function", name);
|
|
83
|
+
return entry ? { uri: entry.uri } : null;
|
|
84
|
+
}
|
|
85
|
+
if (prefix === "task") {
|
|
86
|
+
const entry = index.get("task", name);
|
|
87
|
+
return entry ? { uri: entry.uri } : null;
|
|
88
|
+
}
|
|
89
|
+
if (prefix === "api") {
|
|
90
|
+
const entry = index.get("query", name);
|
|
91
|
+
return entry ? { uri: entry.uri } : null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (DB_OPERATION_TOKENS.has(prevImage)) {
|
|
97
|
+
const entry = index.get("table", name);
|
|
98
|
+
return entry ? { uri: entry.uri } : null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find the declaration offset for a variable token.
|
|
107
|
+
* @param {import('chevrotain').IToken[]} tokens
|
|
108
|
+
* @param {number} tokenIndex - Index of the token the cursor is on
|
|
109
|
+
* @param {Object} symbolTable
|
|
110
|
+
* @returns {{ offset: number } | null}
|
|
111
|
+
*/
|
|
112
|
+
function findVariableDefinition(tokens, tokenIndex, symbolTable) {
|
|
113
|
+
const declarations = symbolTable?.varDeclarations;
|
|
114
|
+
if (!declarations || declarations.length === 0) return null;
|
|
115
|
+
|
|
116
|
+
const token = tokens[tokenIndex];
|
|
117
|
+
const typeName = token.tokenType.name;
|
|
118
|
+
|
|
119
|
+
let varName;
|
|
120
|
+
if (typeName === SHORT_FORM) {
|
|
121
|
+
varName = token.image; // "$foo"
|
|
122
|
+
} else if (typeName === LONG_FORM) {
|
|
123
|
+
// $var.foo → resolve to $foo
|
|
124
|
+
if (tokenIndex + 2 < tokens.length) {
|
|
125
|
+
varName = `$${tokens[tokenIndex + 2].image}`;
|
|
126
|
+
} else {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// Cursor is on the identifier after "$var." (e.g., "foo" in "$var.foo")
|
|
131
|
+
varName = `$${token.image}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Find the first declaration for this variable
|
|
135
|
+
const decl = declarations.find((d) => d.name === varName);
|
|
136
|
+
if (!decl || decl.startOffset == null) return null;
|
|
137
|
+
|
|
138
|
+
// Don't navigate if cursor is already on the declaration
|
|
139
|
+
if (token.startOffset === decl.startOffset) return null;
|
|
140
|
+
// For LongFormVariable, also check if the identifier token matches
|
|
141
|
+
if (
|
|
142
|
+
typeName === LONG_FORM &&
|
|
143
|
+
tokenIndex + 2 < tokens.length &&
|
|
144
|
+
tokens[tokenIndex + 2].startOffset === decl.startOffset
|
|
145
|
+
) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { offset: decl.startOffset };
|
|
150
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { before, beforeEach, describe, it } from "mocha";
|
|
4
|
+
import { lexDocument } from "../lexer/lexer.js";
|
|
5
|
+
import { xanoscriptParser } from "../parser/parser.js";
|
|
6
|
+
import { WorkspaceIndex } from "../workspace/workspaceIndex.js";
|
|
7
|
+
import { findDefinition } from "./onDefinition.js";
|
|
8
|
+
|
|
9
|
+
describe("findDefinition", () => {
|
|
10
|
+
let index;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
index = new WorkspaceIndex();
|
|
14
|
+
index.addFile("file:///ws/users.xs", "table users {\n schema {\n }\n}");
|
|
15
|
+
index.addFile("file:///ws/helper.xs", 'function "helper" {\n}');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should find definition for function.run reference", () => {
|
|
19
|
+
const text = `function "caller" {
|
|
20
|
+
stack {
|
|
21
|
+
function.run "helper" as $result
|
|
22
|
+
}
|
|
23
|
+
}`;
|
|
24
|
+
const offset = text.indexOf('"helper"') + 1;
|
|
25
|
+
const result = findDefinition(text, offset, index);
|
|
26
|
+
expect(result).to.exist;
|
|
27
|
+
expect(result.uri).to.equal("file:///ws/helper.xs");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should find definition for db.get reference", () => {
|
|
31
|
+
const text = `function "caller" {
|
|
32
|
+
stack {
|
|
33
|
+
db.get users {
|
|
34
|
+
field_name = "user_id"
|
|
35
|
+
field_value = 1
|
|
36
|
+
} as $user
|
|
37
|
+
}
|
|
38
|
+
}`;
|
|
39
|
+
const offset = text.indexOf("users") + 2;
|
|
40
|
+
const result = findDefinition(text, offset, index);
|
|
41
|
+
expect(result).to.exist;
|
|
42
|
+
expect(result.uri).to.equal("file:///ws/users.xs");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return null when cursor is not on a reference", () => {
|
|
46
|
+
const text = `function "caller" {
|
|
47
|
+
stack {
|
|
48
|
+
}
|
|
49
|
+
}`;
|
|
50
|
+
const offset = text.indexOf("stack") + 2;
|
|
51
|
+
const result = findDefinition(text, offset, index);
|
|
52
|
+
expect(result).to.be.null;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should return null when reference target not in index", () => {
|
|
56
|
+
const text = `function "caller" {
|
|
57
|
+
stack {
|
|
58
|
+
function.run "unknown" as $result
|
|
59
|
+
}
|
|
60
|
+
}`;
|
|
61
|
+
const offset = text.indexOf('"unknown"') + 1;
|
|
62
|
+
const result = findDefinition(text, offset, index);
|
|
63
|
+
expect(result).to.be.null;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("findDefinition - variable go-to-definition with coverage_check.xs", () => {
|
|
68
|
+
let text;
|
|
69
|
+
let lexResult;
|
|
70
|
+
let st;
|
|
71
|
+
let index;
|
|
72
|
+
let tokens;
|
|
73
|
+
|
|
74
|
+
before(() => {
|
|
75
|
+
text = readFileSync(
|
|
76
|
+
new URL(
|
|
77
|
+
"../parser/tests/variable_test/coverage_check.xs",
|
|
78
|
+
import.meta.url,
|
|
79
|
+
),
|
|
80
|
+
"utf8",
|
|
81
|
+
);
|
|
82
|
+
lexResult = lexDocument(text);
|
|
83
|
+
const parser = xanoscriptParser(text, undefined, lexResult);
|
|
84
|
+
st = { ...parser.__symbolTable };
|
|
85
|
+
st.varDeclarations = [...parser.__symbolTable.varDeclarations];
|
|
86
|
+
tokens = lexResult.tokens;
|
|
87
|
+
index = new WorkspaceIndex();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function lineOf(offset) {
|
|
91
|
+
return text.substring(0, offset).split("\n").length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findAtLine(line, varImage) {
|
|
95
|
+
// Find token matching varImage on the given line
|
|
96
|
+
for (const t of tokens) {
|
|
97
|
+
if (t.image === varImage && lineOf(t.startOffset) === line) {
|
|
98
|
+
return findDefinition(text, t.startOffset, index, st);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Token '${varImage}' not found on line ${line}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Declarations should return NULL (already at definition) ---
|
|
105
|
+
|
|
106
|
+
it("L26: as $vehicle (declaration) -> null", () => {
|
|
107
|
+
expect(findAtLine(26, "$vehicle")).to.be.null;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("L35: var $unused_variable (declaration) -> null", () => {
|
|
111
|
+
expect(findAtLine(35, "$unused_variable")).to.be.null;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("L63: as $policy (declaration) -> null", () => {
|
|
115
|
+
expect(findAtLine(63, "$policy")).to.be.null;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("L109: each as $pc (declaration) -> null", () => {
|
|
119
|
+
expect(findAtLine(109, "$pc")).to.be.null;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("L160: each as $claim (declaration) -> null", () => {
|
|
123
|
+
expect(findAtLine(160, "$claim")).to.be.null;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- Built-in $db should return NULL ---
|
|
127
|
+
|
|
128
|
+
it("L19: $db (built-in) -> null", () => {
|
|
129
|
+
expect(findAtLine(19, "$db")).to.be.null;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// --- Usages should resolve to correct declaration line ---
|
|
133
|
+
|
|
134
|
+
it("L29: $vehicle in precondition -> decl at L26", () => {
|
|
135
|
+
const r = findAtLine(29, "$vehicle");
|
|
136
|
+
expect(r).to.exist;
|
|
137
|
+
expect(lineOf(r.offset)).to.equal(26);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("L42: $vehicle.vin in var value -> decl at L26", () => {
|
|
141
|
+
const r = findAtLine(42, "$vehicle");
|
|
142
|
+
expect(r).to.exist;
|
|
143
|
+
expect(lineOf(r.offset)).to.equal(26);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("L57: $vehicle.id in db.query where -> decl at L26", () => {
|
|
147
|
+
const r = findAtLine(57, "$vehicle");
|
|
148
|
+
expect(r).to.exist;
|
|
149
|
+
expect(lineOf(r.offset)).to.equal(26);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("L82: $policy in conditional if -> decl at L63", () => {
|
|
153
|
+
const r = findAtLine(82, "$policy");
|
|
154
|
+
expect(r).to.exist;
|
|
155
|
+
expect(lineOf(r.offset)).to.equal(63);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("L87: $policy_response in var.update -> decl at L66", () => {
|
|
159
|
+
const r = findAtLine(87, "$policy_response");
|
|
160
|
+
expect(r).to.exist;
|
|
161
|
+
expect(lineOf(r.offset)).to.equal(66);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("L89: $policy.policy_number in value -> decl at L63", () => {
|
|
165
|
+
const r = findAtLine(89, "$policy");
|
|
166
|
+
expect(r).to.exist;
|
|
167
|
+
expect(lineOf(r.offset)).to.equal(63);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("L108: $policy_coverages in foreach -> decl at L105", () => {
|
|
171
|
+
const r = findAtLine(108, "$policy_coverages");
|
|
172
|
+
expect(r).to.exist;
|
|
173
|
+
expect(lineOf(r.offset)).to.equal(105);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("L112: $pc.coverage_type_id in field_value -> decl at L109", () => {
|
|
177
|
+
const r = findAtLine(112, "$pc");
|
|
178
|
+
expect(r).to.exist;
|
|
179
|
+
expect(lineOf(r.offset)).to.equal(109);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("L118: $ctype.code in value -> decl at L113", () => {
|
|
183
|
+
const r = findAtLine(118, "$ctype");
|
|
184
|
+
expect(r).to.exist;
|
|
185
|
+
expect(lineOf(r.offset)).to.equal(113);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("L127: $coverages_response in array.push -> decl at L70", () => {
|
|
189
|
+
const r = findAtLine(127, "$coverages_response");
|
|
190
|
+
expect(r).to.exist;
|
|
191
|
+
expect(lineOf(r.offset)).to.equal(70);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("L128: $coverage_item in value -> decl at L116", () => {
|
|
195
|
+
const r = findAtLine(128, "$coverage_item");
|
|
196
|
+
expect(r).to.exist;
|
|
197
|
+
expect(lineOf(r.offset)).to.equal(116);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("L146: $all_claims in value -> decl at L142", () => {
|
|
201
|
+
const r = findAtLine(146, "$all_claims");
|
|
202
|
+
expect(r).to.exist;
|
|
203
|
+
expect(lineOf(r.offset)).to.equal(142);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("L163: $claim.incident_date in value -> decl at L160", () => {
|
|
207
|
+
const r = findAtLine(163, "$claim");
|
|
208
|
+
expect(r).to.exist;
|
|
209
|
+
expect(lineOf(r.offset)).to.equal(160);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("L168: $claim_date_ts in conditional if -> decl at L162", () => {
|
|
213
|
+
const r = findAtLine(168, "$claim_date_ts");
|
|
214
|
+
expect(r).to.exist;
|
|
215
|
+
expect(lineOf(r.offset)).to.equal(162);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("L168: $two_years_ago in conditional if -> decl at L150", () => {
|
|
219
|
+
const r = findAtLine(168, "$two_years_ago");
|
|
220
|
+
expect(r).to.exist;
|
|
221
|
+
expect(lineOf(r.offset)).to.equal(150);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("L179: $recent_claims_list in array.push -> decl at L155", () => {
|
|
225
|
+
const r = findAtLine(179, "$recent_claims_list");
|
|
226
|
+
expect(r).to.exist;
|
|
227
|
+
expect(lineOf(r.offset)).to.equal(155);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("L180: $recent_claim_item in value -> decl at L169", () => {
|
|
231
|
+
const r = findAtLine(180, "$recent_claim_item");
|
|
232
|
+
expect(r).to.exist;
|
|
233
|
+
expect(lineOf(r.offset)).to.equal(169);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("L188: $claims_summary in var.update -> decl at L74", () => {
|
|
237
|
+
const r = findAtLine(188, "$claims_summary");
|
|
238
|
+
expect(r).to.exist;
|
|
239
|
+
expect(lineOf(r.offset)).to.equal(74);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("L190: $total_claims_count in value -> decl at L145", () => {
|
|
243
|
+
const r = findAtLine(190, "$total_claims_count");
|
|
244
|
+
expect(r).to.exist;
|
|
245
|
+
expect(lineOf(r.offset)).to.equal(145);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("L216: $shop in conditional if -> decl at L212", () => {
|
|
249
|
+
const r = findAtLine(216, "$shop");
|
|
250
|
+
expect(r).to.exist;
|
|
251
|
+
expect(lineOf(r.offset)).to.equal(212);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("L217: $queried_by in var.update -> decl at L202", () => {
|
|
255
|
+
const r = findAtLine(217, "$queried_by");
|
|
256
|
+
expect(r).to.exist;
|
|
257
|
+
expect(lineOf(r.offset)).to.equal(202);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("L232: $vehicle_response in final result -> decl at L40", () => {
|
|
261
|
+
const r = findAtLine(232, "$vehicle_response");
|
|
262
|
+
expect(r).to.exist;
|
|
263
|
+
expect(lineOf(r.offset)).to.equal(40);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("L233: $policy_response in final result -> decl at L66", () => {
|
|
267
|
+
const r = findAtLine(233, "$policy_response");
|
|
268
|
+
expect(r).to.exist;
|
|
269
|
+
expect(lineOf(r.offset)).to.equal(66);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("L234: $coverages_response in final result -> decl at L70", () => {
|
|
273
|
+
const r = findAtLine(234, "$coverages_response");
|
|
274
|
+
expect(r).to.exist;
|
|
275
|
+
expect(lineOf(r.offset)).to.equal(70);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("L235: $claims_summary in final result -> decl at L74", () => {
|
|
279
|
+
const r = findAtLine(235, "$claims_summary");
|
|
280
|
+
expect(r).to.exist;
|
|
281
|
+
expect(lineOf(r.offset)).to.equal(74);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("L241: $queried_by in conditional if -> decl at L202", () => {
|
|
285
|
+
const r = findAtLine(241, "$queried_by");
|
|
286
|
+
expect(r).to.exist;
|
|
287
|
+
expect(lineOf(r.offset)).to.equal(202);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("L242: $result in var.update -> decl at L230", () => {
|
|
291
|
+
const r = findAtLine(242, "$result");
|
|
292
|
+
expect(r).to.exist;
|
|
293
|
+
expect(lineOf(r.offset)).to.equal(230);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("L243: $result in value expression -> decl at L230", () => {
|
|
297
|
+
const r = findAtLine(243, "$result");
|
|
298
|
+
expect(r).to.exist;
|
|
299
|
+
expect(lineOf(r.offset)).to.equal(230);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("L243: $queried_by in value expression -> decl at L202", () => {
|
|
303
|
+
const r = findAtLine(243, "$queried_by");
|
|
304
|
+
expect(r).to.exist;
|
|
305
|
+
expect(lineOf(r.offset)).to.equal(202);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("L251: $result in response = -> decl at L230", () => {
|
|
309
|
+
const r = findAtLine(251, "$result");
|
|
310
|
+
expect(r).to.exist;
|
|
311
|
+
expect(lineOf(r.offset)).to.equal(230);
|
|
312
|
+
});
|
|
313
|
+
});
|