@xano/xanoscript-language-server 11.8.3 → 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 +9 -1
- 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/table_trigger_parser.js +21 -0
- package/parser/table_trigger_parser.spec.js +29 -0
- 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
|
@@ -13,6 +13,12 @@ export const QueryToken = createTokenByName("query", {
|
|
|
13
13
|
categories: [Identifier],
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
// where
|
|
17
|
+
export const WhereToken = createTokenByName("where", {
|
|
18
|
+
longer_alt: Identifier,
|
|
19
|
+
categories: [Identifier],
|
|
20
|
+
});
|
|
21
|
+
|
|
16
22
|
// get
|
|
17
23
|
export const GetToken = createTokenByName("get", {
|
|
18
24
|
longer_alt: Identifier,
|
|
@@ -156,6 +162,7 @@ export const DbTokens = [
|
|
|
156
162
|
MysqlToken,
|
|
157
163
|
PostgresToken,
|
|
158
164
|
OracleToken,
|
|
165
|
+
WhereToken,
|
|
159
166
|
];
|
|
160
167
|
|
|
161
168
|
export function mapTokenToType(token) {
|
|
@@ -183,7 +190,8 @@ export function mapTokenToType(token) {
|
|
|
183
190
|
case TruncateToken.name:
|
|
184
191
|
case DirectQueryToken.name:
|
|
185
192
|
case SetDatasourceToken.name:
|
|
186
|
-
|
|
193
|
+
case WhereToken.name:
|
|
194
|
+
return "variable";
|
|
187
195
|
default:
|
|
188
196
|
return null;
|
|
189
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
|
+
}
|