@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.
Files changed (58) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/cache/documentCache.js +58 -10
  3. package/lexer/db.js +9 -1
  4. package/lexer/security.js +16 -0
  5. package/onCompletion/onCompletion.js +61 -1
  6. package/onDefinition/onDefinition.js +150 -0
  7. package/onDefinition/onDefinition.spec.js +313 -0
  8. package/onDidChangeContent/onDidChangeContent.js +52 -5
  9. package/onHover/functions.md +28 -0
  10. package/package.json +1 -1
  11. package/parser/base_parser.js +61 -3
  12. package/parser/clauses/middlewareClause.js +16 -0
  13. package/parser/definitions/columnDefinition.js +5 -0
  14. package/parser/functions/api/apiCallFn.js +5 -3
  15. package/parser/functions/controls/functionCallFn.js +5 -3
  16. package/parser/functions/controls/functionRunFn.js +61 -5
  17. package/parser/functions/controls/taskCallFn.js +5 -3
  18. package/parser/functions/db/captureFieldName.js +63 -0
  19. package/parser/functions/db/dbAddFn.js +5 -3
  20. package/parser/functions/db/dbAddOrEditFn.js +13 -3
  21. package/parser/functions/db/dbBulkAddFn.js +5 -3
  22. package/parser/functions/db/dbBulkDeleteFn.js +5 -3
  23. package/parser/functions/db/dbBulkPatchFn.js +5 -3
  24. package/parser/functions/db/dbBulkUpdateFn.js +5 -3
  25. package/parser/functions/db/dbDelFn.js +10 -3
  26. package/parser/functions/db/dbEditFn.js +13 -3
  27. package/parser/functions/db/dbGetFn.js +10 -3
  28. package/parser/functions/db/dbHasFn.js +9 -3
  29. package/parser/functions/db/dbPatchFn.js +10 -3
  30. package/parser/functions/db/dbQueryFn.js +29 -3
  31. package/parser/functions/db/dbSchemaFn.js +5 -3
  32. package/parser/functions/db/dbTruncateFn.js +5 -3
  33. package/parser/functions/middlewareCallFn.js +3 -1
  34. package/parser/functions/security/register.js +19 -9
  35. package/parser/functions/security/securityCreateAuthTokenFn.js +22 -0
  36. package/parser/functions/security/securityJweDecodeLegacyFn.js +24 -0
  37. package/parser/functions/security/securityJweDecodeLegacyFn.spec.js +26 -0
  38. package/parser/functions/security/securityJweEncodeLegacyFn.js +24 -0
  39. package/parser/functions/security/securityJweEncodeLegacyFn.spec.js +25 -0
  40. package/parser/functions/securityFn.js +2 -0
  41. package/parser/functions/varFn.js +1 -1
  42. package/parser/generic/asVariable.js +2 -0
  43. package/parser/generic/assignableVariableAs.js +1 -0
  44. package/parser/generic/assignableVariableProperty.js +5 -2
  45. package/parser/table_trigger_parser.js +21 -0
  46. package/parser/table_trigger_parser.spec.js +29 -0
  47. package/parser/tests/variable_test/coverage_check.xs +293 -0
  48. package/parser/variableScanner.js +64 -0
  49. package/parser/variableValidator.js +44 -0
  50. package/parser/variableValidator.spec.js +179 -0
  51. package/server.js +164 -10
  52. package/utils.js +32 -0
  53. package/utils.spec.js +93 -1
  54. package/workspace/crossFileValidator.js +166 -0
  55. package/workspace/crossFileValidator.spec.js +654 -0
  56. package/workspace/referenceTracking.spec.js +420 -0
  57. package/workspace/workspaceIndex.js +149 -0
  58. 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
+ }