@xano/xanoscript-language-server 11.8.4 → 11.9.0

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 (59) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/cache/documentCache.js +58 -10
  3. package/lexer/comment.js +14 -24
  4. package/lexer/db.js +1 -2
  5. package/lexer/security.js +16 -0
  6. package/onCompletion/onCompletion.js +61 -1
  7. package/onDefinition/onDefinition.js +150 -0
  8. package/onDefinition/onDefinition.spec.js +313 -0
  9. package/onDidChangeContent/onDidChangeContent.js +53 -6
  10. package/onHover/functions.md +28 -0
  11. package/package.json +1 -1
  12. package/parser/base_parser.js +61 -3
  13. package/parser/clauses/middlewareClause.js +16 -0
  14. package/parser/definitions/columnDefinition.js +5 -0
  15. package/parser/functions/api/apiCallFn.js +5 -3
  16. package/parser/functions/controls/functionCallFn.js +5 -3
  17. package/parser/functions/controls/functionRunFn.js +61 -5
  18. package/parser/functions/controls/taskCallFn.js +5 -3
  19. package/parser/functions/db/captureFieldName.js +63 -0
  20. package/parser/functions/db/dbAddFn.js +5 -3
  21. package/parser/functions/db/dbAddOrEditFn.js +13 -3
  22. package/parser/functions/db/dbBulkAddFn.js +5 -3
  23. package/parser/functions/db/dbBulkDeleteFn.js +5 -3
  24. package/parser/functions/db/dbBulkPatchFn.js +5 -3
  25. package/parser/functions/db/dbBulkUpdateFn.js +5 -3
  26. package/parser/functions/db/dbDelFn.js +10 -3
  27. package/parser/functions/db/dbEditFn.js +13 -3
  28. package/parser/functions/db/dbGetFn.js +10 -3
  29. package/parser/functions/db/dbHasFn.js +9 -3
  30. package/parser/functions/db/dbPatchFn.js +10 -3
  31. package/parser/functions/db/dbQueryFn.js +29 -3
  32. package/parser/functions/db/dbSchemaFn.js +5 -3
  33. package/parser/functions/db/dbTruncateFn.js +5 -3
  34. package/parser/functions/middlewareCallFn.js +3 -1
  35. package/parser/functions/security/register.js +19 -9
  36. package/parser/functions/security/securityCreateAuthTokenFn.js +22 -0
  37. package/parser/functions/security/securityJweDecodeLegacyFn.js +24 -0
  38. package/parser/functions/security/securityJweDecodeLegacyFn.spec.js +26 -0
  39. package/parser/functions/security/securityJweEncodeLegacyFn.js +24 -0
  40. package/parser/functions/security/securityJweEncodeLegacyFn.spec.js +25 -0
  41. package/parser/functions/securityFn.js +2 -0
  42. package/parser/functions/varFn.js +1 -1
  43. package/parser/generic/asVariable.js +2 -0
  44. package/parser/generic/assignableVariableAs.js +1 -0
  45. package/parser/generic/assignableVariableProperty.js +5 -2
  46. package/parser/task_parser.js +2 -1
  47. package/parser/tests/task/valid_sources/create_leak.xs +165 -0
  48. package/parser/tests/variable_test/coverage_check.xs +293 -0
  49. package/parser/variableScanner.js +64 -0
  50. package/parser/variableValidator.js +44 -0
  51. package/parser/variableValidator.spec.js +179 -0
  52. package/server.js +206 -18
  53. package/utils.js +32 -0
  54. package/utils.spec.js +93 -1
  55. package/workspace/crossFileValidator.js +166 -0
  56. package/workspace/crossFileValidator.spec.js +654 -0
  57. package/workspace/referenceTracking.spec.js +420 -0
  58. package/workspace/workspaceIndex.js +149 -0
  59. package/workspace/workspaceIndex.spec.js +189 -0
@@ -0,0 +1,420 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { xanoscriptParser } from "../parser/parser.js";
4
+
5
+ describe("reference tracking in __symbolTable", () => {
6
+ it("should track function.run reference", () => {
7
+ const parser = xanoscriptParser(`function "caller" {
8
+ stack {
9
+ function.run "my_target" as $result
10
+ }
11
+ }`);
12
+ const refs = parser.__symbolTable.references;
13
+ expect(refs).to.have.length(1);
14
+ expect(refs[0].refType).to.equal("function");
15
+ expect(refs[0].name).to.equal("my_target");
16
+ expect(refs[0].startOffset).to.be.a("number");
17
+ });
18
+
19
+ it("should track function.call reference", () => {
20
+ const parser = xanoscriptParser(`function "caller" {
21
+ stack {
22
+ function.call "my_target" as $result
23
+ }
24
+ }`);
25
+ const refs = parser.__symbolTable.references;
26
+ expect(refs).to.have.length(1);
27
+ expect(refs[0].refType).to.equal("function");
28
+ expect(refs[0].name).to.equal("my_target");
29
+ });
30
+
31
+ it("should track db.get table reference", () => {
32
+ const parser = xanoscriptParser(`function "caller" {
33
+ stack {
34
+ db.get users {
35
+ field_name = id
36
+ field_value = 1
37
+ } as $user
38
+ }
39
+ }`);
40
+ const refs = parser.__symbolTable.references;
41
+ expect(refs).to.have.length(1);
42
+ expect(refs[0].refType).to.equal("table");
43
+ expect(refs[0].name).to.equal("users");
44
+ });
45
+
46
+ it("should track db.query table reference", () => {
47
+ const parser = xanoscriptParser(`function "caller" {
48
+ stack {
49
+ db.query users {
50
+ return {
51
+ type = list
52
+ }
53
+ } as $users
54
+ }
55
+ }`);
56
+ const refs = parser.__symbolTable.references;
57
+ expect(refs).to.have.length(1);
58
+ expect(refs[0].refType).to.equal("table");
59
+ expect(refs[0].name).to.equal("users");
60
+ });
61
+
62
+ it("should track multiple references in one file", () => {
63
+ const parser = xanoscriptParser(`function "caller" {
64
+ stack {
65
+ db.get users {
66
+ field_name = "user_id"
67
+ field_value = 1
68
+ } as $user
69
+ function.run "helper" as $result
70
+ }
71
+ }`);
72
+ const refs = parser.__symbolTable.references;
73
+ expect(refs).to.have.length(2);
74
+ expect(refs[0].refType).to.equal("table");
75
+ expect(refs[0].name).to.equal("users");
76
+ expect(refs[1].refType).to.equal("function");
77
+ expect(refs[1].name).to.equal("helper");
78
+ });
79
+
80
+ it("should track task.call reference", () => {
81
+ const parser = xanoscriptParser(`function "caller" {
82
+ stack {
83
+ task.call "my_task" as $result
84
+ }
85
+ }`);
86
+ const refs = parser.__symbolTable.references;
87
+ expect(refs).to.have.length(1);
88
+ expect(refs[0].refType).to.equal("task");
89
+ expect(refs[0].name).to.equal("my_task");
90
+ });
91
+
92
+ it("should track db.bulk.delete table reference", () => {
93
+ const parser = xanoscriptParser(`function "caller" {
94
+ stack {
95
+ db.bulk.delete users {
96
+ field_name = id
97
+ field_value = [1, 2, 3]
98
+ }
99
+ }
100
+ }`);
101
+ const refs = parser.__symbolTable.references;
102
+ expect(refs).to.have.length(1);
103
+ expect(refs[0].refType).to.equal("table");
104
+ expect(refs[0].name).to.equal("users");
105
+ });
106
+
107
+ it("should track string literal table names", () => {
108
+ const parser = xanoscriptParser(`function "caller" {
109
+ stack {
110
+ db.get "user table" {
111
+ field_name = id
112
+ field_value = 1
113
+ } as $user
114
+ }
115
+ }`);
116
+ const refs = parser.__symbolTable.references;
117
+ expect(refs).to.have.length(1);
118
+ expect(refs[0].name).to.equal("user table");
119
+ });
120
+
121
+ it("should capture input args on function.run reference", () => {
122
+ const parser = xanoscriptParser(`function "caller" {
123
+ stack {
124
+ function.run "my_target" {
125
+ input = { user_id: 1, name: "test" }
126
+ } as $result
127
+ }
128
+ }`);
129
+ const refs = parser.__symbolTable.references;
130
+ expect(refs).to.have.length(1);
131
+ expect(refs[0].refType).to.equal("function");
132
+ expect(refs[0].args).to.be.an("object");
133
+ expect(refs[0].args).to.have.property("user_id");
134
+ expect(refs[0].args).to.have.property("name");
135
+ });
136
+
137
+ it("should capture literal types on input args", () => {
138
+ const parser = xanoscriptParser(`function "caller" {
139
+ stack {
140
+ function.run "my_target" {
141
+ input = { count: 100, name: "test", rate: 3.14, active: true }
142
+ } as $result
143
+ }
144
+ }`);
145
+ const refs = parser.__symbolTable.references;
146
+ expect(refs).to.have.length(1);
147
+ const args = refs[0].args;
148
+ expect(args.count.type).to.equal("int");
149
+ expect(args.name.type).to.equal("text");
150
+ expect(args.rate.type).to.equal("decimal");
151
+ expect(args.active.type).to.equal("bool");
152
+ });
153
+
154
+ it("should capture input args with variable values as unknown type", () => {
155
+ const parser = xanoscriptParser(`function "caller" {
156
+ stack {
157
+ function.run "my_target" {
158
+ input = { user_id: $var1 }
159
+ } as $result
160
+ }
161
+ }`);
162
+ const refs = parser.__symbolTable.references;
163
+ expect(refs).to.have.length(1);
164
+ expect(refs[0].args).to.have.property("user_id");
165
+ expect(refs[0].args.user_id.type).to.be.null;
166
+ });
167
+
168
+ it("should not have args when function.run has no input", () => {
169
+ const parser = xanoscriptParser(`function "caller" {
170
+ stack {
171
+ function.run "my_target" as $result
172
+ }
173
+ }`);
174
+ const refs = parser.__symbolTable.references;
175
+ expect(refs).to.have.length(1);
176
+ expect(refs[0].args).to.be.undefined;
177
+ });
178
+
179
+ it("should not have args when function.run has non-input attributes only", () => {
180
+ const parser = xanoscriptParser(`function "caller" {
181
+ stack {
182
+ function.run "my_target" {
183
+ description = "testing"
184
+ } as $result
185
+ }
186
+ }`);
187
+ const refs = parser.__symbolTable.references;
188
+ expect(refs).to.have.length(1);
189
+ expect(refs[0].args).to.be.undefined;
190
+ });
191
+
192
+ it("should capture field_name on db.get reference with string literal", () => {
193
+ const parser = xanoscriptParser(`function "caller" {
194
+ stack {
195
+ db.get users {
196
+ field_name = "id"
197
+ field_value = 1
198
+ } as $user
199
+ }
200
+ }`);
201
+ const refs = parser.__symbolTable.references;
202
+ expect(refs).to.have.length(1);
203
+ expect(refs[0].refType).to.equal("table");
204
+ expect(refs[0].fieldName).to.equal("id");
205
+ });
206
+
207
+ it("should capture field_name on db.edit reference with string literal", () => {
208
+ const parser = xanoscriptParser(`function "caller" {
209
+ stack {
210
+ db.edit users {
211
+ field_name = "id"
212
+ field_value = 1
213
+ data = { name: "test" }
214
+ } as $user
215
+ }
216
+ }`);
217
+ const refs = parser.__symbolTable.references;
218
+ expect(refs).to.have.length(1);
219
+ expect(refs[0].fieldName).to.equal("id");
220
+ });
221
+
222
+ it("should not capture field_name when value is a variable expression", () => {
223
+ const parser = xanoscriptParser(`function "caller" {
224
+ stack {
225
+ db.get users {
226
+ field_name = $input.field
227
+ field_value = 1
228
+ } as $user
229
+ }
230
+ }`);
231
+ const refs = parser.__symbolTable.references;
232
+ expect(refs).to.have.length(1);
233
+ expect(refs[0].fieldName).to.be.undefined;
234
+ });
235
+
236
+ it("should not have field_name when db function has no field_name attribute", () => {
237
+ const parser = xanoscriptParser(`function "caller" {
238
+ stack {
239
+ db.query users {
240
+ return {
241
+ type = list
242
+ }
243
+ } as $users
244
+ }
245
+ }`);
246
+ const refs = parser.__symbolTable.references;
247
+ expect(refs).to.have.length(1);
248
+ expect(refs[0].fieldName).to.be.undefined;
249
+ });
250
+
251
+ it("should track middleware references from query middleware clause", () => {
252
+ const parser = xanoscriptParser(`query foo verb=GET {
253
+ input {
254
+ text user_id filters=trim
255
+ }
256
+
257
+ stack {
258
+ }
259
+
260
+ response = null
261
+
262
+ middleware = {pre: [{name: "auth_middle"}], post: [{name: "log_middle"}]}
263
+ }`);
264
+ const refs = parser.__symbolTable.references;
265
+ const mwRefs = refs.filter((r) => r.refType === "middleware");
266
+ expect(mwRefs).to.have.length(2);
267
+ expect(mwRefs[0].name).to.equal("auth_middle");
268
+ expect(mwRefs[1].name).to.equal("log_middle");
269
+ });
270
+
271
+ it("should track middleware.call reference", () => {
272
+ const parser = xanoscriptParser(`workflow_test test_middleware {
273
+ stack {
274
+ middleware.call "auth_check" {
275
+ input = {
276
+ vars: $input,
277
+ type: "pre"
278
+ }
279
+ } as $result
280
+ }
281
+ }`);
282
+ const refs = parser.__symbolTable.references;
283
+ const mwRefs = refs.filter((r) => r.refType === "middleware");
284
+ expect(mwRefs).to.have.length(1);
285
+ expect(mwRefs[0].name).to.equal("auth_check");
286
+ });
287
+
288
+ it("should track data keys on db.edit reference", () => {
289
+ const parser = xanoscriptParser(`function "caller" {
290
+ stack {
291
+ db.edit users {
292
+ field_name = "id"
293
+ field_value = 1
294
+ data = { name: "test", email: "foo@bar.com" }
295
+ } as $user
296
+ }
297
+ }`);
298
+ const refs = parser.__symbolTable.references;
299
+ expect(refs).to.have.length(1);
300
+ expect(refs[0].refType).to.equal("table");
301
+ expect(refs[0].dataKeys).to.be.an("array");
302
+ const keyNames = refs[0].dataKeys.map((k) => k.name);
303
+ expect(keyNames).to.include("name");
304
+ expect(keyNames).to.include("email");
305
+ });
306
+
307
+ it("should not have dataKeys when db function has no data attribute", () => {
308
+ const parser = xanoscriptParser(`function "caller" {
309
+ stack {
310
+ db.get users {
311
+ field_name = "id"
312
+ field_value = 1
313
+ } as $user
314
+ }
315
+ }`);
316
+ const refs = parser.__symbolTable.references;
317
+ expect(refs).to.have.length(1);
318
+ expect(refs[0].dataKeys).to.be.undefined;
319
+ });
320
+
321
+ it("should track join table references in db.query", () => {
322
+ const parser = xanoscriptParser(`function "caller" {
323
+ stack {
324
+ db.query users {
325
+ join = {
326
+ profile: {
327
+ type: "left"
328
+ table: "user_profiles"
329
+ }
330
+ }
331
+ return = {type: "list"}
332
+ } as $users
333
+ }
334
+ }`);
335
+ const refs = parser.__symbolTable.references;
336
+ const tableRefs = refs.filter((r) => r.refType === "table");
337
+ expect(tableRefs).to.have.length(2);
338
+ expect(tableRefs[0].name).to.equal("users");
339
+ expect(tableRefs[1].name).to.equal("user_profiles");
340
+ });
341
+
342
+ it("should track addon name references in db.query", () => {
343
+ const parser = xanoscriptParser(`function "caller" {
344
+ stack {
345
+ db.query users {
346
+ addon = [{name: "get_posts", as: "_posts"}]
347
+ return = {type: "list"}
348
+ } as $users
349
+ }
350
+ }`);
351
+ const refs = parser.__symbolTable.references;
352
+ const fnRefs = refs.filter((r) => r.refType === "function");
353
+ expect(fnRefs).to.have.length(1);
354
+ expect(fnRefs[0].name).to.equal("get_posts");
355
+ });
356
+
357
+ it("should track table relation references from column definitions", () => {
358
+ const parser = xanoscriptParser(`table beer {
359
+ schema {
360
+ int brewery_id {
361
+ table = "brewery"
362
+ }
363
+ }
364
+ }`);
365
+ const refs = parser.__symbolTable.references;
366
+ const tableRefs = refs.filter((r) => r.refType === "table");
367
+ expect(tableRefs).to.have.length(1);
368
+ expect(tableRefs[0].name).to.equal("brewery");
369
+ });
370
+
371
+ it("should track function.run with empty name and not leak args to previous reference", () => {
372
+ const parser = xanoscriptParser(`function "caller" {
373
+ stack {
374
+ db.edit claim {
375
+ field_name = "id"
376
+ field_value = 1
377
+ data = {status: "active"}
378
+ } as $updated_claim
379
+
380
+ function.run "" {
381
+ input = {
382
+ claim_id: 1
383
+ to_status: "assessment"
384
+ }
385
+ } as $transition_result
386
+ }
387
+ }`);
388
+ const refs = parser.__symbolTable.references;
389
+ const tableRefs = refs.filter((r) => r.refType === "table");
390
+ const fnRefs = refs.filter((r) => r.refType === "function");
391
+
392
+ // The table ref should NOT have args from the function.run
393
+ expect(tableRefs).to.have.length(1);
394
+ expect(tableRefs[0].name).to.equal("claim");
395
+ expect(tableRefs[0].args).to.be.undefined;
396
+
397
+ // The function ref should exist (even with empty name) and carry the args
398
+ expect(fnRefs).to.have.length(1);
399
+ expect(fnRefs[0].name).to.equal("");
400
+ expect(fnRefs[0].args).to.have.property("claim_id");
401
+ expect(fnRefs[0].args).to.have.property("to_status");
402
+ });
403
+
404
+ it("should track security.create_auth_token table reference", () => {
405
+ const parser = xanoscriptParser(`function "caller" {
406
+ stack {
407
+ security.create_auth_token {
408
+ table = "users"
409
+ extras = {}
410
+ expiration = 86400
411
+ id = ""
412
+ } as $token
413
+ }
414
+ }`);
415
+ const refs = parser.__symbolTable.references;
416
+ const tableRefs = refs.filter((r) => r.refType === "table");
417
+ expect(tableRefs).to.have.length(1);
418
+ expect(tableRefs[0].name).to.equal("users");
419
+ });
420
+ });
@@ -0,0 +1,149 @@
1
+ import { getObjectInfoFromContent } from "../utils.js";
2
+
3
+ /**
4
+ * @typedef {Object} IndexEntry
5
+ * @property {string} uri - Document URI
6
+ * @property {string} type - Object type (function, table, query, task, etc.)
7
+ * @property {string} name - Object name
8
+ * @property {Object} [inputs] - Input parameters/columns from __symbolTable.input
9
+ */
10
+
11
+ /**
12
+ * In-memory index of all XanoScript objects in the workspace.
13
+ * One entry per .xs file, keyed by (type, name).
14
+ */
15
+ export class WorkspaceIndex {
16
+ constructor() {
17
+ /** @type {Map<string, Map<string, IndexEntry>>} type -> name -> entry */
18
+ this.index = new Map();
19
+ /** @type {Map<string, IndexEntry>} uri -> entry (reverse lookup) */
20
+ this.byUri = new Map();
21
+ }
22
+
23
+ /**
24
+ * @private
25
+ * Insert an entry into the index maps.
26
+ */
27
+ _setEntry(entry) {
28
+ const { type, name, uri } = entry;
29
+ if (!this.index.has(type)) {
30
+ this.index.set(type, new Map());
31
+ }
32
+
33
+ const existing = this.index.get(type).get(name);
34
+ if (existing && existing.uri !== uri) {
35
+ this.byUri.delete(existing.uri);
36
+ }
37
+
38
+ this.index.get(type).set(name, entry);
39
+ this.byUri.set(uri, entry);
40
+ }
41
+
42
+ /**
43
+ * Lightweight index: extract type+name via regex only (no parsing).
44
+ * Used for initial workspace scan to avoid heavy memory usage.
45
+ * Inputs will be populated later when the file is opened or parsed.
46
+ * @param {string} uri - Document URI
47
+ * @param {string} content - File content
48
+ * @returns {boolean} true if successfully indexed
49
+ */
50
+ addFile(uri, content) {
51
+ this.removeFile(uri);
52
+
53
+ const info = getObjectInfoFromContent(content);
54
+ if (!info) return false;
55
+
56
+ const entry = { uri, type: info.type, name: info.name, inputs: {} };
57
+ this._setEntry(entry);
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Add or update a file in the index using pre-parsed data.
63
+ * Avoids double-parsing when the caller already has the parse result.
64
+ * @param {string} uri - Document URI
65
+ * @param {string} content - File content (for name extraction)
66
+ * @param {Object} symbolTable - Parser's __symbolTable
67
+ * @returns {boolean} true if successfully indexed
68
+ */
69
+ addParsed(uri, content, symbolTable) {
70
+ this.removeFile(uri);
71
+
72
+ const info = getObjectInfoFromContent(content);
73
+ if (!info) return false;
74
+
75
+ const inputs = symbolTable?.input ? { ...symbolTable.input } : {};
76
+ const entry = { uri, type: info.type, name: info.name, inputs };
77
+ this._setEntry(entry);
78
+ return true;
79
+ }
80
+
81
+ /**
82
+ * Remove a file from the index.
83
+ * @param {string} uri - Document URI
84
+ */
85
+ removeFile(uri) {
86
+ const existing = this.byUri.get(uri);
87
+ if (existing) {
88
+ const typeMap = this.index.get(existing.type);
89
+ if (typeMap) {
90
+ typeMap.delete(existing.name);
91
+ if (typeMap.size === 0) {
92
+ this.index.delete(existing.type);
93
+ }
94
+ }
95
+ this.byUri.delete(uri);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get an index entry by type and name.
101
+ * @param {string} type
102
+ * @param {string} name
103
+ * @returns {IndexEntry | undefined}
104
+ */
105
+ get(type, name) {
106
+ return this.index.get(type)?.get(name);
107
+ }
108
+
109
+ /**
110
+ * Check if an entry exists.
111
+ * @param {string} type
112
+ * @param {string} name
113
+ * @returns {boolean}
114
+ */
115
+ has(type, name) {
116
+ return this.index.get(type)?.has(name) ?? false;
117
+ }
118
+
119
+ /**
120
+ * Get all entries for a given type.
121
+ * @param {string} type
122
+ * @returns {IndexEntry[]}
123
+ */
124
+ getByType(type) {
125
+ const typeMap = this.index.get(type);
126
+ return typeMap ? [...typeMap.values()] : [];
127
+ }
128
+
129
+ /**
130
+ * Get all names for a given type.
131
+ * @param {string} type
132
+ * @returns {string[]}
133
+ */
134
+ getAllNames(type) {
135
+ const typeMap = this.index.get(type);
136
+ return typeMap ? [...typeMap.keys()] : [];
137
+ }
138
+
139
+ /**
140
+ * Get entry by URI (reverse lookup).
141
+ * @param {string} uri
142
+ * @returns {IndexEntry | undefined}
143
+ */
144
+ getByUri(uri) {
145
+ return this.byUri.get(uri);
146
+ }
147
+ }
148
+
149
+ export const workspaceIndex = new WorkspaceIndex();