@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Levenshtein distance between two strings.
|
|
3
|
+
*/
|
|
4
|
+
function levenshtein(a, b) {
|
|
5
|
+
const m = a.length;
|
|
6
|
+
const n = b.length;
|
|
7
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1));
|
|
8
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
9
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
10
|
+
for (let i = 1; i <= m; i++) {
|
|
11
|
+
for (let j = 1; j <= n; j++) {
|
|
12
|
+
dp[i][j] =
|
|
13
|
+
a[i - 1] === b[j - 1]
|
|
14
|
+
? dp[i - 1][j - 1]
|
|
15
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return dp[m][n];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find the closest match from candidates using prefix check + edit distance.
|
|
23
|
+
*/
|
|
24
|
+
function findClosestMatch(input, candidates) {
|
|
25
|
+
const inputLower = input.toLowerCase();
|
|
26
|
+
|
|
27
|
+
// Prefix match: if input is a prefix of exactly one candidate, suggest it
|
|
28
|
+
const prefixMatches = candidates.filter((c) =>
|
|
29
|
+
c.toLowerCase().startsWith(inputLower)
|
|
30
|
+
);
|
|
31
|
+
if (prefixMatches.length === 1) return prefixMatches[0];
|
|
32
|
+
|
|
33
|
+
// Levenshtein fallback
|
|
34
|
+
const maxDistance = Math.max(3, Math.floor(input.length / 2));
|
|
35
|
+
let best = null;
|
|
36
|
+
let bestDist = Infinity;
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const dist = levenshtein(inputLower, candidate.toLowerCase());
|
|
39
|
+
if (dist < bestDist) {
|
|
40
|
+
bestDist = dist;
|
|
41
|
+
best = candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return bestDist <= maxDistance ? best : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate cross-file references against the workspace index.
|
|
49
|
+
* @param {Array<{refType: string, name: string, startOffset: number, endOffset: number}>} references
|
|
50
|
+
* @param {import('./workspaceIndex.js').WorkspaceIndex} index
|
|
51
|
+
* @returns {Array<{message: string, startOffset: number, endOffset: number}>} warnings
|
|
52
|
+
*/
|
|
53
|
+
export function crossFileValidate(references, index) {
|
|
54
|
+
if (!references || references.length === 0) return [];
|
|
55
|
+
|
|
56
|
+
const warnings = [];
|
|
57
|
+
for (const ref of references) {
|
|
58
|
+
if (!ref.name) {
|
|
59
|
+
warnings.push({
|
|
60
|
+
message: `Empty ${ref.refType} reference`,
|
|
61
|
+
startOffset: ref.startOffset,
|
|
62
|
+
endOffset: ref.endOffset,
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!index.has(ref.refType, ref.name)) {
|
|
68
|
+
let message = `Unknown ${ref.refType} "${ref.name}"`;
|
|
69
|
+
const allNames = index.getAllNames(ref.refType);
|
|
70
|
+
const suggestion = findClosestMatch(ref.name, allNames);
|
|
71
|
+
if (suggestion) {
|
|
72
|
+
message += `. Did you mean "${suggestion}"?`;
|
|
73
|
+
}
|
|
74
|
+
warnings.push({
|
|
75
|
+
message,
|
|
76
|
+
startOffset: ref.startOffset,
|
|
77
|
+
endOffset: ref.endOffset,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate field_name against table columns
|
|
83
|
+
if (ref.refType === "table" && ref.fieldName) {
|
|
84
|
+
const entry = index.get(ref.refType, ref.name);
|
|
85
|
+
const columns = entry?.inputs;
|
|
86
|
+
if (columns && Object.keys(columns).length > 0) {
|
|
87
|
+
const columnNames = Object.keys(columns);
|
|
88
|
+
if (!columnNames.includes(ref.fieldName)) {
|
|
89
|
+
let message = `Unknown column "${ref.fieldName}" in table "${ref.name}"`;
|
|
90
|
+
const suggestion = findClosestMatch(ref.fieldName, columnNames);
|
|
91
|
+
if (suggestion) {
|
|
92
|
+
message += `. Did you mean "${suggestion}"?`;
|
|
93
|
+
}
|
|
94
|
+
warnings.push({
|
|
95
|
+
message,
|
|
96
|
+
startOffset: ref.fieldNameStartOffset ?? ref.startOffset,
|
|
97
|
+
endOffset: ref.fieldNameEndOffset ?? ref.endOffset,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validate data keys against table columns
|
|
104
|
+
if (ref.refType === "table" && ref.dataKeys) {
|
|
105
|
+
const entry = index.get(ref.refType, ref.name);
|
|
106
|
+
const columns = entry?.inputs;
|
|
107
|
+
if (columns && Object.keys(columns).length > 0) {
|
|
108
|
+
const columnNames = Object.keys(columns);
|
|
109
|
+
for (const dk of ref.dataKeys) {
|
|
110
|
+
if (!columnNames.includes(dk.name)) {
|
|
111
|
+
let message = `Unknown column "${dk.name}" in table "${ref.name}"`;
|
|
112
|
+
const suggestion = findClosestMatch(dk.name, columnNames);
|
|
113
|
+
if (suggestion) {
|
|
114
|
+
message += `. Did you mean "${suggestion}"?`;
|
|
115
|
+
}
|
|
116
|
+
warnings.push({
|
|
117
|
+
message,
|
|
118
|
+
startOffset: dk.startOffset ?? ref.startOffset,
|
|
119
|
+
endOffset: dk.endOffset ?? ref.endOffset,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate input keys if args are present
|
|
127
|
+
if (ref.args) {
|
|
128
|
+
const entry = index.get(ref.refType, ref.name);
|
|
129
|
+
const declaredInputs = entry?.inputs;
|
|
130
|
+
if (!declaredInputs || Object.keys(declaredInputs).length === 0) continue;
|
|
131
|
+
|
|
132
|
+
const declaredKeys = Object.keys(declaredInputs);
|
|
133
|
+
for (const [argName, argInfo] of Object.entries(ref.args)) {
|
|
134
|
+
if (!(argName in declaredInputs)) {
|
|
135
|
+
let message = `Unknown input "${argName}" for ${ref.refType} "${ref.name}"`;
|
|
136
|
+
const suggestion = findClosestMatch(argName, declaredKeys);
|
|
137
|
+
if (suggestion) {
|
|
138
|
+
message += `. Did you mean "${suggestion}"?`;
|
|
139
|
+
}
|
|
140
|
+
warnings.push({
|
|
141
|
+
message,
|
|
142
|
+
startOffset: argInfo?.startOffset ?? ref.startOffset,
|
|
143
|
+
endOffset: argInfo?.endOffset ?? ref.endOffset,
|
|
144
|
+
});
|
|
145
|
+
} else if (argInfo?.type) {
|
|
146
|
+
// Type check: compare literal type against declared type
|
|
147
|
+
const declared = declaredInputs[argName];
|
|
148
|
+
const isEnumWithText =
|
|
149
|
+
declared?.type === "enum" && argInfo.type === "text";
|
|
150
|
+
if (
|
|
151
|
+
declared?.type &&
|
|
152
|
+
declared.type !== argInfo.type &&
|
|
153
|
+
!isEnumWithText
|
|
154
|
+
) {
|
|
155
|
+
warnings.push({
|
|
156
|
+
message: `Type mismatch for input "${argName}": expected ${declared.type}, got ${argInfo.type}`,
|
|
157
|
+
startOffset: argInfo?.startOffset ?? ref.startOffset,
|
|
158
|
+
endOffset: argInfo?.endOffset ?? ref.endOffset,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return warnings;
|
|
166
|
+
}
|