@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,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
+ });
@@ -1,4 +1,7 @@
1
1
  import { documentCache } from "../cache/documentCache.js";
2
+ import { validateVariables } from "../parser/variableValidator.js";
3
+ import { crossFileValidate } from "../workspace/crossFileValidator.js";
4
+ import { workspaceIndex } from "../workspace/workspaceIndex.js";
2
5
 
3
6
  // Diagnostic severity constants
4
7
  const SEVERITY = {
@@ -56,7 +59,7 @@ function createDiagnostics(parser, document) {
56
59
  * @param {import('vscode-languageserver').Connection} connection
57
60
  * @returns
58
61
  */
59
- export function onDidChangeContent(params, connection) {
62
+ export function onDidChangeContent(params, connection, features = {}) {
60
63
  const document = params.document;
61
64
 
62
65
  if (!document) {
@@ -71,16 +74,14 @@ export function onDidChangeContent(params, connection) {
71
74
 
72
75
  try {
73
76
  // Parse the XanoScript file using cache
74
- const { parser, scheme } = documentCache.getOrParse(
77
+ const { lexResult, parser, scheme } = documentCache.getOrParse(
75
78
  document.uri,
76
79
  document.version,
77
80
  text,
78
81
  );
79
82
 
80
- if (parser.errors.length === 0) {
81
- // If parsing succeeds with no errors, send an empty diagnostics array
82
- connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
83
- }
83
+ // Update workspace index with already-parsed data (no re-parse needed)
84
+ workspaceIndex.addParsed(document.uri, text, parser.__symbolTable);
84
85
 
85
86
  for (const error of parser.errors) {
86
87
  console.error(
@@ -91,6 +92,52 @@ export function onDidChangeContent(params, connection) {
91
92
  // Create diagnostics in a single pass
92
93
  const diagnostics = createDiagnostics(parser, document);
93
94
 
95
+ // Run cross-file validation and append as warnings
96
+ if (features.crossFileValidation !== false && parser.__symbolTable?.references) {
97
+ const crossFileWarnings = crossFileValidate(
98
+ parser.__symbolTable.references,
99
+ workspaceIndex,
100
+ );
101
+ for (const warning of crossFileWarnings) {
102
+ diagnostics.push({
103
+ severity: SEVERITY.WARNING,
104
+ range: {
105
+ start: document.positionAt(warning.startOffset),
106
+ end: document.positionAt(warning.endOffset + 1),
107
+ },
108
+ message: warning.message,
109
+ });
110
+ }
111
+ }
112
+
113
+ // Run variable validation and append warnings/hints
114
+ if (features.variableValidation !== false && parser.__symbolTable?.varDeclarations) {
115
+ const varResult = validateVariables(
116
+ parser.__symbolTable,
117
+ lexResult.tokens,
118
+ );
119
+ for (const warning of varResult.warnings) {
120
+ diagnostics.push({
121
+ severity: SEVERITY.WARNING,
122
+ range: {
123
+ start: document.positionAt(warning.startOffset),
124
+ end: document.positionAt(warning.endOffset + 1),
125
+ },
126
+ message: warning.message,
127
+ });
128
+ }
129
+ for (const hint of varResult.hints) {
130
+ diagnostics.push({
131
+ severity: SEVERITY.HINT,
132
+ range: {
133
+ start: document.positionAt(hint.startOffset),
134
+ end: document.positionAt(hint.endOffset + 1),
135
+ },
136
+ message: hint.message,
137
+ });
138
+ }
139
+ }
140
+
94
141
  console.log(
95
142
  `onDidChangeContent(): sending diagnostic (${parser.errors.length} errors) for scheme:`,
96
143
  scheme,
@@ -1538,6 +1538,34 @@ security.jwe_encode {
1538
1538
 
1539
1539
  Encodes a payload as a JSON Web Encryption (JWE) token with `headers`, `claims`, and a `key`. The `key_algorithm` (e.g., `A256KW`) and `content_algorithm` (e.g., `A256GCM`) secure the token, and `ttl` sets its validity (0 for no expiration). The token is stored in the variable defined by `as`, here `$encrypted_token`.
1540
1540
 
1541
+ # security.jwe_encode_legacy
1542
+
1543
+ ```xs
1544
+ security.jwe_encode_legacy {
1545
+ payload = $jwt_payload
1546
+ audience = "LegacyApp"
1547
+ key = $env.magic_jwt_secret
1548
+ key_algorithm = "A256KW"
1549
+ content_algorithm = "A256CBC-HS512"
1550
+ } as $jwt
1551
+ ```
1552
+
1553
+ Encodes a payload as a JSON Web Encryption (JWE) token using legacy algorithms. The `payload` is the data to encrypt, `audience` specifies the intended recipient, and `key` is used for encryption. The `key_algorithm` (e.g., `A256KW`) and `content_algorithm` (e.g., `A256CBC-HS512`) secure the token. The resulting JWE token is stored in the variable defined by `as`, here `$jwt`.
1554
+
1555
+ # security.jwe_decode_legacy
1556
+
1557
+ ```xs
1558
+ security.jwe_decode_legacy {
1559
+ token = $input.magic_token
1560
+ key = $env.magic_jwt_secret
1561
+ audience = "LegacyApp"
1562
+ key_algorithm = "A256KW"
1563
+ content_algorithm = "A256CBC-HS512"
1564
+ } as $decoded_magic_token
1565
+ ```
1566
+
1567
+ Decodes a JSON Web Encryption (JWE) token that was encoded using legacy algorithms. The `token` is the JWE to decode, `key` is used for decryption, and `audience` specifies the expected recipient. The `key_algorithm` (e.g., `A256KW`) and `content_algorithm` (e.g., `A256CBC-HS512`) are used to properly decode the token. The decoded payload is stored in the variable defined by `as`, here `$decoded_magic_token`.
1568
+
1541
1569
  # security.create_secret_key
1542
1570
 
1543
1571
  ```xs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.8.4",
3
+ "version": "11.9.0",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -42,10 +42,16 @@ export class XanoBaseParser extends CstParser {
42
42
  * Add a variable to the registry
43
43
  * @param {string} name the name of the variable
44
44
  * @param {string} type the type of the variable (int, decimal, text, bool, etc)
45
- * @param {boolean} nullable whether the variable is nullable
45
+ * @param {import('chevrotain').IToken} [token] the token for position tracking
46
46
  */
47
- addVariable(name, type, value = null) {
48
- this.__symbolTable.var[name] = { type, value };
47
+ addVariable(name, type, token = null) {
48
+ this.__symbolTable.var[name] = { type, value: null };
49
+ this.__symbolTable.varDeclarations.push({
50
+ name,
51
+ type,
52
+ startOffset: token?.startOffset ?? null,
53
+ endOffset: token?.endOffset ?? null,
54
+ });
49
55
  }
50
56
 
51
57
  /**
@@ -58,6 +64,56 @@ export class XanoBaseParser extends CstParser {
58
64
  this.__symbolTable.input[name] = { type, iterable, nullable, optional };
59
65
  }
60
66
 
67
+ /**
68
+ * Record a cross-file reference for validation.
69
+ * @param {string} refType - The type of object referenced ("function", "table", "query", "task")
70
+ * @param {string} name - The referenced object name
71
+ * @param {import('chevrotain').IToken} token - The token containing the name (for diagnostics positioning)
72
+ */
73
+ addReference(refType, name, token) {
74
+ if (name == null || !token) return;
75
+ this.__symbolTable.references.push({
76
+ refType,
77
+ name,
78
+ startOffset: token.startOffset,
79
+ endOffset: token.endOffset,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Attach argument keys to the most recent reference (for input validation).
85
+ * @param {Object} args - Map of argument names to { value, startOffset, endOffset }
86
+ */
87
+ setReferenceArgs(args) {
88
+ const refs = this.__symbolTable.references;
89
+ if (refs.length === 0) return;
90
+ refs[refs.length - 1].args = args;
91
+ }
92
+
93
+ /**
94
+ * Attach field_name to the most recent reference (for column validation).
95
+ * @param {string} fieldName - The field name value
96
+ * @param {number} startOffset
97
+ * @param {number} endOffset
98
+ */
99
+ setReferenceFieldName(fieldName, startOffset, endOffset) {
100
+ const refs = this.__symbolTable.references;
101
+ if (refs.length === 0) return;
102
+ refs[refs.length - 1].fieldName = fieldName;
103
+ refs[refs.length - 1].fieldNameStartOffset = startOffset;
104
+ refs[refs.length - 1].fieldNameEndOffset = endOffset;
105
+ }
106
+
107
+ /**
108
+ * Attach data keys to the most recent reference (for column validation).
109
+ * @param {Array<{name: string, startOffset: number, endOffset: number}>} dataKeys
110
+ */
111
+ setReferenceDataKeys(dataKeys) {
112
+ const refs = this.__symbolTable.references;
113
+ if (refs.length === 0) return;
114
+ refs[refs.length - 1].dataKeys = dataKeys;
115
+ }
116
+
61
117
  /**
62
118
  * Add a warning message to the parser
63
119
  * @param {*} message
@@ -244,6 +300,8 @@ export class XanoBaseParser extends CstParser {
244
300
  var: {},
245
301
  auth: {},
246
302
  env: {},
303
+ references: [],
304
+ varDeclarations: [],
247
305
  };
248
306
  }
249
307
  }
@@ -1,5 +1,6 @@
1
1
  import { EqualToken } from "../../lexer/control.js";
2
2
  import { MiddlewareToken } from "../../lexer/tokens.js";
3
+ import { getVarName } from "../generic/utils.js";
3
4
 
4
5
  /**
5
6
  * response = $entries
@@ -12,6 +13,7 @@ export function middlewareClause($) {
12
13
 
13
14
  const parent = $.CONSUME(MiddlewareToken); // "middleware"
14
15
  $.CONSUME(EqualToken); // "="
16
+ const captured = {};
15
17
  $.SUBRULE($.schemaParseObjectFn, {
16
18
  ARGS: [
17
19
  parent,
@@ -31,9 +33,23 @@ export function middlewareClause($) {
31
33
  },
32
34
  ],
33
35
  },
36
+ captured,
34
37
  ],
35
38
  });
36
39
 
40
+ $.ACTION(() => {
41
+ for (const phase of ["pre", "post"]) {
42
+ const items = captured[phase]?.value;
43
+ if (!Array.isArray(items)) continue;
44
+ for (const item of items) {
45
+ const nameEntry = item.name;
46
+ if (!nameEntry?.value) continue;
47
+ const token = nameEntry.value;
48
+ $.addReference("middleware", getVarName(token), token);
49
+ }
50
+ }
51
+ });
52
+
37
53
  $.sectionStack.pop();
38
54
  };
39
55
  }
@@ -113,6 +113,11 @@ export function columnDefinition($) {
113
113
  ],
114
114
  });
115
115
 
116
+ // Track table reference from FK column definition
117
+ if (captured.table?.value) {
118
+ $.addReference("table", getVarName(captured.table.value), captured.table.value);
119
+ }
120
+
116
121
  if (inputTypeToken.image == "vector" && !captured.size) {
117
122
  $.addWarning(
118
123
  'Column named "vector" should have a size attribute defining its length.',
@@ -10,6 +10,7 @@ import {
10
10
  VerbToken,
11
11
  } from "../../../lexer/query.js";
12
12
  import { Identifier } from "../../../lexer/tokens.js";
13
+ import { getVarName } from "../../generic/utils.js";
13
14
 
14
15
  /**
15
16
  * @param {import('../../base_parser.js').XanoBaseParser} $
@@ -36,10 +37,11 @@ export function apiCallFn($) {
36
37
  );
37
38
  }
38
39
 
39
- $.OR([
40
- { ALT: () => $.CONSUME(StringLiteral) }, // "foo/bar"
41
- { ALT: () => $.CONSUME(Identifier) }, // foo
40
+ const nameToken = $.OR([
41
+ { ALT: () => $.CONSUME(StringLiteral) },
42
+ { ALT: () => $.CONSUME(Identifier) },
42
43
  ]);
44
+ $.addReference("query", getVarName(nameToken), nameToken);
43
45
  $.CONSUME(VerbToken);
44
46
  $.CONSUME(EqualToken);
45
47
  $.OR1([
@@ -1,6 +1,7 @@
1
1
  import { CallToken, FunctionToken } from "../../../lexer/function.js";
2
2
  import { StringLiteral } from "../../../lexer/literal.js";
3
3
  import { DotToken, Identifier } from "../../../lexer/tokens.js";
4
+ import { getVarName } from "../../generic/utils.js";
4
5
 
5
6
  /**
6
7
  *
@@ -17,10 +18,11 @@ export function functionCallFn($) {
17
18
  $.CONSUME(DotToken); // "."
18
19
  const fnToken = $.CONSUME(CallToken); // "call"
19
20
 
20
- $.OR([
21
- { ALT: () => $.CONSUME(Identifier) }, // user
22
- { ALT: () => $.CONSUME1(StringLiteral) }, // "user/ auth"
21
+ const nameToken = $.OR([
22
+ { ALT: () => $.CONSUME(Identifier) },
23
+ { ALT: () => $.CONSUME1(StringLiteral) },
23
24
  ]);
25
+ $.addReference("function", getVarName(nameToken), nameToken);
24
26
 
25
27
  $.SUBRULE($.functionAttrReq, {
26
28
  ARGS: [