@xano/xanoscript-language-server 11.11.0 → 11.12.1

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 (62) hide show
  1. package/.claude/memory/feedback_no_git.md +11 -0
  2. package/.claude/settings.local.json +13 -28
  3. package/lexer/cast.js +3 -3
  4. package/lexer/control.js +0 -2
  5. package/lexer/literal.js +2 -2
  6. package/lexer/security.js +2 -2
  7. package/package.json +1 -1
  8. package/parser/addon_parser.js +2 -1
  9. package/parser/agent_parser.js +2 -1
  10. package/parser/agent_trigger_parser.js +2 -1
  11. package/parser/api_group_parser.js +2 -1
  12. package/parser/attributes/columnDefaultValueAttribute.js +10 -3
  13. package/parser/attributes/inputFilterFn.js +11 -4
  14. package/parser/base_parser.js +11 -0
  15. package/parser/branch_parser.js +2 -1
  16. package/parser/clauses/flexibleStringClause.js +2 -2
  17. package/parser/clauses/nakedStackFn.js +43 -36
  18. package/parser/clauses/nakedStackFn.spec.js +10 -0
  19. package/parser/clauses/responseClause.spec.js +5 -0
  20. package/parser/definitions/columnDefinition.js +7 -2
  21. package/parser/function_parser.js +1 -0
  22. package/parser/function_parser.spec.js +14 -0
  23. package/parser/functions/array/arrayNoValueAs.js +1 -1
  24. package/parser/functions/array/arrayPopFn.js +1 -3
  25. package/parser/functions/controls/forEachFn.spec.js +8 -0
  26. package/parser/functions/controls/preconditionFn.js +3 -0
  27. package/parser/functions/db/dbExternalDirectQueryFn.js +1 -0
  28. package/parser/functions/db/dbQueryFn.spec.js +31 -0
  29. package/parser/functions/schema/schemaParseConstantFn.js +11 -10
  30. package/parser/functions/varFn.js +14 -2
  31. package/parser/functions/varFn.spec.js +20 -12
  32. package/parser/generic/asVariable.spec.js +12 -8
  33. package/parser/generic/assignableVariableAs.js +50 -11
  34. package/parser/generic/assignableVariableAs.spec.js +29 -7
  35. package/parser/generic/assignableVariableProperty.js +22 -10
  36. package/parser/generic/assignableVariableProperty.spec.js +21 -12
  37. package/parser/generic/expressionFn.js +9 -2
  38. package/parser/generic/expressionFn.spec.js +5 -0
  39. package/parser/generic/filterNames.js +9 -0
  40. package/parser/mcp_server_parser.js +2 -1
  41. package/parser/mcp_server_trigger_parser.js +2 -1
  42. package/parser/middleware_parser.js +2 -1
  43. package/parser/multidoc.js +38 -17
  44. package/parser/multidoc.spec.js +7 -1
  45. package/parser/query_parser.js +12 -3
  46. package/parser/query_parser.spec.js +14 -3
  47. package/parser/realtime_channel_parser.js +2 -1
  48. package/parser/realtime_trigger_parser.js +2 -1
  49. package/parser/run_parser.js +4 -2
  50. package/parser/table_parser.js +2 -1
  51. package/parser/table_trigger_parser.js +2 -1
  52. package/parser/task_parser.js +2 -1
  53. package/parser/task_parser.spec.js +11 -0
  54. package/parser/tests/function/function_parser.spec.js +4 -4
  55. package/parser/tests/multidoc/multidoc_parser.spec.js +55 -0
  56. package/parser/tests/multidoc/valid_sources/function_stack.xs +130603 -0
  57. package/parser/tests/query/query_parser.spec.js +3 -3
  58. package/parser/tool_parser.js +14 -4
  59. package/parser/tool_parser.spec.js +19 -6
  60. package/parser/workflow_test_parser.js +2 -1
  61. package/parser/workspace_parser.js +2 -1
  62. package/parser/workspace_trigger_parser.js +2 -1
@@ -2,9 +2,14 @@ import { DotToken } from "../../lexer/tokens.js";
2
2
  import { UpdateToken, VarToken } from "../../lexer/var.js";
3
3
  import {
4
4
  ApiBaseUrlVariable,
5
+ AuthVariable,
5
6
  BranchVariable,
6
7
  DatasourceVariable,
8
+ EnvVariable,
9
+ ErrorVariable,
7
10
  HttpHeadersVariable,
11
+ InputVariable,
12
+ LongFormVariable,
8
13
  RemoteHostVariable,
9
14
  RemoteIpVariable,
10
15
  RemotePasswordVariable,
@@ -53,16 +58,23 @@ export function varFn($) {
53
58
  { ALT: () => $.CONSUME(RequestUriVariable) },
54
59
  { ALT: () => $.CONSUME(TenantVariable) },
55
60
  { ALT: () => $.CONSUME(WebflowVariable) },
61
+ // Not recommended but still valid
62
+ { ALT: () => $.CONSUME(LongFormVariable) },
63
+ { ALT: () => $.CONSUME(EnvVariable) },
64
+ { ALT: () => $.CONSUME(AuthVariable) },
65
+ { ALT: () => $.CONSUME(InputVariable) },
66
+ { ALT: () => $.CONSUME(ErrorVariable) },
56
67
  ]);
68
+
57
69
  if (variable.image) {
58
70
  // Check for blacklisted variable names
59
71
  const nameWithoutPrefix = variable.image.startsWith("$")
60
72
  ? variable.image.slice(1)
61
73
  : variable.image;
62
74
  if (isBlacklistedVariableName(nameWithoutPrefix)) {
63
- $.addInvalidValueError(
64
- variable,
75
+ $.addWarning(
65
76
  `'${variable.image}' is a reserved variable name and should not be used as a variable.`,
77
+ variable,
66
78
  );
67
79
  }
68
80
  // we can directly add the variable here but
@@ -225,16 +225,18 @@ describe("varFn", () => {
225
225
  const parser = parse(`var $${name} {
226
226
  value = "test"
227
227
  }`);
228
- expect(parser.errors).to.not.be.empty;
229
- expect(parser.errors[0].message).to.include("reserved");
228
+ expect(parser.errors).to.be.empty;
229
+ expect(parser.warnings).to.not.be.empty;
230
+ expect(parser.warnings[0].message).to.include("reserved");
230
231
  });
231
232
 
232
233
  it(`rejects var.update $${name} as variable name (short form)`, () => {
233
234
  const parser = parse(`var.update $${name} {
234
235
  value = "test"
235
236
  }`);
236
- expect(parser.errors).to.not.be.empty;
237
- expect(parser.errors[0].message).to.include("reserved");
237
+ expect(parser.errors).to.be.empty;
238
+ expect(parser.warnings).to.not.be.empty;
239
+ expect(parser.warnings[0].message).to.include("reserved");
238
240
  });
239
241
  }
240
242
 
@@ -249,30 +251,35 @@ describe("varFn", () => {
249
251
  ];
250
252
 
251
253
  for (const name of separateTokenBlacklisted) {
252
- it(`rejects var $${name} as variable name (separate token)`, () => {
254
+ it(`warns var $${name} as variable name (separate token)`, () => {
253
255
  const parser = parse(`var $${name} {
254
256
  value = "test"
255
257
  }`);
256
258
  if (parser.errors.length === 0) {
257
259
  console.log(parser.errors, name);
258
260
  }
259
- expect(parser.errors).to.not.be.empty;
261
+ expect(parser.errors).to.be.empty;
262
+ expect(parser.warnings).to.not.be.empty;
263
+ expect(parser.warnings[0].message).to.include("reserved");
260
264
  });
261
265
 
262
- it(`rejects var.update $${name} as variable name (separate token)`, () => {
266
+ it(`warns var.update $${name} as variable name (separate token)`, () => {
263
267
  const parser = parse(`var.update $${name} {
264
268
  value = "test"
265
269
  }`);
266
- expect(parser.errors).to.not.be.empty;
270
+ expect(parser.errors).to.be.empty;
271
+ expect(parser.warnings).to.not.be.empty;
272
+ expect(parser.warnings[0].message).to.include("reserved");
267
273
  });
268
274
  }
269
275
 
270
276
  // $var is the LongFormVariable prefix, parser expects a dot after it
271
- it("rejects var $var as variable name (long form prefix)", () => {
277
+ it("warns var $var as variable name (long form prefix)", () => {
272
278
  const parser = parse(`var $var {
273
279
  value = "test"
274
280
  }`);
275
- expect(parser.errors).to.not.be.empty;
281
+ expect(parser.warnings).to.not.be.empty;
282
+ expect(parser.errors).to.be.empty;
276
283
  });
277
284
 
278
285
  it("accepts a backtick expression as value", () => {
@@ -301,8 +308,9 @@ describe("varFn", () => {
301
308
  const parser = parse(`var.update $var.${name} {
302
309
  value = "test"
303
310
  }`);
304
- expect(parser.errors).to.not.be.empty;
305
- expect(parser.errors[0].message).to.include("reserved");
311
+ expect(parser.errors).to.be.empty;
312
+ expect(parser.warnings).to.not.be.empty;
313
+ expect(parser.warnings[0].message).to.include("reserved");
306
314
  });
307
315
  }
308
316
 
@@ -44,8 +44,9 @@ describe("asVariable", () => {
44
44
  for (const name of shortFormBlacklisted) {
45
45
  it(`rejects as $${name} (short form)`, () => {
46
46
  const parser = parse(`as $${name}`);
47
- expect(parser.errors).to.not.be.empty;
48
- expect(parser.errors[0].message).to.include("reserved");
47
+ expect(parser.errors).to.be.empty;
48
+ expect(parser.warnings).to.not.be.empty;
49
+ expect(parser.warnings[0].message).to.include("reserved");
49
50
  });
50
51
  }
51
52
 
@@ -62,14 +63,16 @@ describe("asVariable", () => {
62
63
  for (const name of separateTokenBlacklisted) {
63
64
  it(`rejects as $${name} (separate token)`, () => {
64
65
  const parser = parse(`as $${name}`);
65
- expect(parser.errors).to.not.be.empty;
66
+ expect(parser.warnings).to.not.be.empty;
66
67
  });
67
68
  }
68
69
 
69
70
  // $var is the LongFormVariable prefix, parser expects a dot after it
71
+ // FIXME: shoud be a warning, not an error, since it's still a valid variable name (albeit confusing)
70
72
  it("rejects as $var (long form prefix)", () => {
71
73
  const parser = parse("as $var");
72
- expect(parser.errors).to.not.be.empty;
74
+ expect(parser.errors).to.be.empty;
75
+ expect(parser.warnings).to.not.be.empty;
73
76
  });
74
77
 
75
78
  // All blacklisted names should be rejected in long form ($var.name)
@@ -89,25 +92,26 @@ describe("asVariable", () => {
89
92
  for (const name of allBlacklisted) {
90
93
  it(`rejects as $var.${name} (long form)`, () => {
91
94
  const parser = parse(`as $var.${name}`);
92
- expect(parser.errors).to.not.be.empty;
93
- expect(parser.errors[0].message).to.include("reserved");
95
+ expect(parser.warnings).to.not.be.empty;
96
+ expect(parser.warnings[0].message).to.include("reserved");
94
97
  });
95
98
  }
96
99
 
97
100
  // Ensure valid names still work
98
101
  it("accepts as $user", () => {
99
102
  const parser = parse("as $user");
100
- expect(parser.errors).to.be.empty;
103
+ expect(parser.warnings).to.be.empty;
101
104
  });
102
105
 
103
106
  it("accepts as $my_input", () => {
104
107
  const parser = parse("as $my_input");
105
108
  expect(parser.errors).to.be.empty;
109
+ expect(parser.warnings).to.be.empty;
106
110
  });
107
111
 
108
112
  it("accepts as $var.my_var", () => {
109
113
  const parser = parse("as $var.my_var");
110
- expect(parser.errors).to.be.empty;
114
+ expect(parser.warnings).to.be.empty;
111
115
  });
112
116
  });
113
117
  });
@@ -1,6 +1,12 @@
1
- import { MismatchedTokenException } from "chevrotain";
2
1
  import { DotToken, Identifier } from "../../lexer/tokens.js";
3
- import { LongFormVariable, ShortFormVariable } from "../../lexer/variables.js";
2
+ import {
3
+ AuthVariable,
4
+ EnvVariable,
5
+ ErrorVariable,
6
+ InputVariable,
7
+ LongFormVariable,
8
+ ShortFormVariable,
9
+ } from "../../lexer/variables.js";
4
10
  import { isBlacklistedVariableName } from "./assignableVariableProperty.js";
5
11
 
6
12
  /**
@@ -23,10 +29,45 @@ export function assignableVariableAs($) {
23
29
  // "$var.users"
24
30
  {
25
31
  ALT: () => {
26
- $.CONSUME(LongFormVariable);
27
- $.CONSUME(DotToken);
28
- const variable = $.CONSUME(Identifier);
29
- variableName = `$${variable.image}`;
32
+ const variable = $.CONSUME(LongFormVariable);
33
+ variableName = variable.image;
34
+ variableToken = variable;
35
+
36
+ // Handle optional property access, e.g., "$var.users" because "$var" is a valid
37
+ // variable name even if it's not recommended and confusing
38
+ $.OPTION(() => {
39
+ $.CONSUME(DotToken);
40
+ const variable = $.CONSUME(Identifier);
41
+ variableName = `$${variable.image}`;
42
+ variableToken = variable;
43
+ });
44
+ },
45
+ },
46
+ {
47
+ ALT: () => {
48
+ const variable = $.CONSUME(EnvVariable);
49
+ variableName = variable.image;
50
+ variableToken = variable;
51
+ },
52
+ },
53
+ {
54
+ ALT: () => {
55
+ const variable = $.CONSUME(AuthVariable);
56
+ variableName = variable.image;
57
+ variableToken = variable;
58
+ },
59
+ },
60
+ {
61
+ ALT: () => {
62
+ const variable = $.CONSUME(InputVariable);
63
+ variableName = variable.image;
64
+ variableToken = variable;
65
+ },
66
+ },
67
+ {
68
+ ALT: () => {
69
+ const variable = $.CONSUME(ErrorVariable);
70
+ variableName = variable.image;
30
71
  variableToken = variable;
31
72
  },
32
73
  },
@@ -38,11 +79,9 @@ export function assignableVariableAs($) {
38
79
  ? variableName.slice(1)
39
80
  : variableName;
40
81
  if (isBlacklistedVariableName(nameWithoutPrefix)) {
41
- $.SAVE_ERROR(
42
- new MismatchedTokenException(
43
- `'${variableName}' is a reserved variable name and cannot be used`,
44
- variableToken
45
- )
82
+ $.addWarning(
83
+ `'${variableToken.image}' is a reserved variable name and should not be used as a variable.`,
84
+ variableToken,
46
85
  );
47
86
  }
48
87
  $.addVariable(variableName, "unknown", variableToken);
@@ -25,6 +25,12 @@ describe("assignableVariableAs", () => {
25
25
  expect(parser.errors).to.not.be.empty;
26
26
  });
27
27
 
28
+ it("assignableVariableAs should warn on $var reserved variable names", () => {
29
+ const parser = parse("$var");
30
+ expect(parser.errors).to.be.empty;
31
+ expect(parser.warnings).to.not.be.empty;
32
+ });
33
+
28
34
  it("assignableVariableAs cannot be a short named variable with sub props", () => {
29
35
  const parser = parse("$users.x.y[1].z");
30
36
  expect(parser.errors).to.not.be.empty;
@@ -53,13 +59,28 @@ describe("assignableVariableAs", () => {
53
59
  // Blacklisted variable names - used in loop contexts (each as $variable)
54
60
  describe("blacklisted variable names", () => {
55
61
  // These are lexed as ShortFormVariable and need explicit blacklisting
56
- const shortFormBlacklisted = ["output", "db", "toolset"];
62
+ const shortFormBlacklisted = [
63
+ "output",
64
+ "db",
65
+ "toolset",
66
+ "auth",
67
+ "input",
68
+ "env",
69
+ "response",
70
+ "output",
71
+ "var",
72
+ "db",
73
+ "error",
74
+ "toolset",
75
+ "this",
76
+ ];
57
77
 
58
78
  for (const name of shortFormBlacklisted) {
59
- it(`rejects $${name} (short form)`, () => {
79
+ it(`warns on $${name} (short form)`, () => {
60
80
  const parser = parse(`$${name}`);
61
- expect(parser.errors).to.not.be.empty;
62
- expect(parser.errors[0].message).to.include("reserved");
81
+ expect(parser.errors).to.be.empty;
82
+ expect(parser.warnings).to.not.be.empty;
83
+ expect(parser.warnings[0].message).to.include("reserved");
63
84
  });
64
85
  }
65
86
 
@@ -78,10 +99,11 @@ describe("assignableVariableAs", () => {
78
99
  ];
79
100
 
80
101
  for (const name of allBlacklisted) {
81
- it(`rejects $var.${name} (long form)`, () => {
102
+ it(`warns on $var.${name} (long form)`, () => {
82
103
  const parser = parse(`$var.${name}`);
83
- expect(parser.errors).to.not.be.empty;
84
- expect(parser.errors[0].message).to.include("reserved");
104
+ expect(parser.errors).to.be.empty;
105
+ expect(parser.warnings).to.not.be.empty;
106
+ expect(parser.warnings[0].message).to.include("reserved");
85
107
  });
86
108
  }
87
109
 
@@ -1,11 +1,14 @@
1
- import { MismatchedTokenException } from "chevrotain";
2
1
  import { PipeToken } from "../../lexer/control.js";
3
2
  import { DotToken, Identifier, NewlineToken } from "../../lexer/tokens.js";
4
3
  import {
5
4
  ApiBaseUrlVariable,
5
+ AuthVariable,
6
6
  BranchVariable,
7
7
  DatasourceVariable,
8
+ EnvVariable,
9
+ ErrorVariable,
8
10
  HttpHeadersVariable,
11
+ InputVariable,
9
12
  LongFormVariable,
10
13
  RemoteHostVariable,
11
14
  RemoteIpVariable,
@@ -78,6 +81,11 @@ export function assignableVariableProperty($) {
78
81
  { ALT: () => $.CONSUME(RequestUriVariable) },
79
82
  { ALT: () => $.CONSUME(TenantVariable) },
80
83
  { ALT: () => $.CONSUME(WebflowVariable) },
84
+ // Not recommended but still valid
85
+ { ALT: () => $.CONSUME(EnvVariable) },
86
+ { ALT: () => $.CONSUME(AuthVariable) },
87
+ { ALT: () => $.CONSUME(InputVariable) },
88
+ { ALT: () => $.CONSUME(ErrorVariable) },
81
89
  ]);
82
90
  variableName = variable.image;
83
91
  variableToken = variable;
@@ -86,11 +94,17 @@ export function assignableVariableProperty($) {
86
94
  // "$var.users"
87
95
  {
88
96
  ALT: () => {
89
- $.CONSUME(LongFormVariable);
90
- $.CONSUME(DotToken);
91
- const variable = $.CONSUME(Identifier);
92
- variableName = `$${variable.image}`;
97
+ const variable = $.CONSUME(LongFormVariable);
98
+ variableName = variable.image;
93
99
  variableToken = variable;
100
+
101
+ // because we want to allow "$var" we need to optionally consume a dot and an identifier
102
+ $.OPTION(() => {
103
+ $.CONSUME(DotToken);
104
+ const variable = $.CONSUME(Identifier);
105
+ variableName = `$${variable.image}`;
106
+ variableToken = variable;
107
+ });
94
108
  },
95
109
  },
96
110
  ],
@@ -104,11 +118,9 @@ export function assignableVariableProperty($) {
104
118
  ? variableName.slice(1)
105
119
  : variableName;
106
120
  if (isBlacklistedVariableName(nameWithoutPrefix)) {
107
- $.SAVE_ERROR(
108
- new MismatchedTokenException(
109
- `'${variableName}' is a reserved variable name and cannot be used`,
110
- variableToken
111
- )
121
+ $.addWarning(
122
+ `'${variableName}' is a reserved variable name and should not be used`,
123
+ variableToken,
112
124
  );
113
125
  }
114
126
  }
@@ -30,17 +30,21 @@ describe("assignableVariableProperty", () => {
30
30
  expect(parser.errors).to.be.empty;
31
31
  });
32
32
 
33
- it("assignableVariableProperty cannot be an env variable", () => {
33
+ it("assignableVariableProperty should warn on $env variable", () => {
34
34
  const parser = parse("$env.my_api_key");
35
- expect(parser.errors).to.not.be.empty;
35
+ expect(parser.errors).to.be.empty;
36
+ expect(parser.warnings).to.not.be.empty;
37
+ expect(parser.warnings[0].message).to.include("reserved");
36
38
  });
37
39
 
38
- it("assignableVariableProperty cannot be an input variable", () => {
40
+ it("assignableVariableProperty should warn on $input variable", () => {
39
41
  const parser = parse("$input.remote_ip");
40
- expect(parser.errors).to.not.be.empty;
42
+ expect(parser.errors).to.be.empty;
43
+ expect(parser.warnings).to.not.be.empty;
44
+ expect(parser.warnings[0].message).to.include("reserved");
41
45
  });
42
46
 
43
- it("assignableVariableProperty cannot be a string", () => {
47
+ it("assignableVariableProperty cannot be an indentifier", () => {
44
48
  const parser = parse(`"someting"`);
45
49
  expect(parser.errors).to.not.be.empty;
46
50
  });
@@ -59,8 +63,9 @@ describe("assignableVariableProperty", () => {
59
63
  for (const name of shortFormBlacklisted) {
60
64
  it(`rejects $${name} as assignable variable (short form)`, () => {
61
65
  const parser = parse(`$${name}`);
62
- expect(parser.errors).to.not.be.empty;
63
- expect(parser.errors[0].message).to.include("reserved");
66
+ expect(parser.errors).to.be.empty;
67
+ expect(parser.warnings).to.not.be.empty;
68
+ expect(parser.warnings[0].message).to.include("reserved");
64
69
  });
65
70
  }
66
71
 
@@ -78,14 +83,17 @@ describe("assignableVariableProperty", () => {
78
83
  for (const name of separateTokenBlacklisted) {
79
84
  it(`rejects $${name} as assignable variable (separate token)`, () => {
80
85
  const parser = parse(`$${name}`);
81
- expect(parser.errors).to.not.be.empty;
86
+ expect(parser.errors).to.be.empty;
87
+ expect(parser.warnings).to.not.be.empty;
88
+ expect(parser.warnings[0].message).to.include("reserved");
82
89
  });
83
90
  }
84
91
 
85
92
  // $var is the LongFormVariable prefix, parser expects a dot after it
86
- it("rejects $var as assignable variable (long form prefix)", () => {
93
+ it("warns when using $var as assignable variable (long form prefix)", () => {
87
94
  const parser = parse("$var");
88
- expect(parser.errors).to.not.be.empty;
95
+ expect(parser.errors).to.be.empty;
96
+ expect(parser.warnings).to.not.be.empty;
89
97
  });
90
98
 
91
99
  // All blacklisted names should be rejected in long form ($var.name)
@@ -105,8 +113,9 @@ describe("assignableVariableProperty", () => {
105
113
  for (const name of allBlacklisted) {
106
114
  it(`rejects $var.${name} as assignable variable (long form)`, () => {
107
115
  const parser = parse(`$var.${name}`);
108
- expect(parser.errors).to.not.be.empty;
109
- expect(parser.errors[0].message).to.include("reserved");
116
+ expect(parser.errors).to.be.empty;
117
+ expect(parser.warnings).to.not.be.empty;
118
+ expect(parser.warnings[0].message).to.include("reserved");
110
119
  });
111
120
  }
112
121
 
@@ -366,8 +366,15 @@ export function simpleExpressionFn($) {
366
366
  // Group 1: Literals (fast path - most common, distinct tokens)
367
367
  { ALT: () => $.CONSUME(StringLiteral) },
368
368
  { ALT: () => $.CONSUME(SingleQuotedStringLiteral) },
369
- { ALT: () => $.CONSUME(IntegerLiteral) },
370
- { ALT: () => $.CONSUME(FloatLiteral) },
369
+ {
370
+ ALT: () => {
371
+ $.OPTION(() => $.CONSUME(Minus));
372
+ $.OR1([
373
+ { ALT: () => $.CONSUME(IntegerLiteral) },
374
+ { ALT: () => $.CONSUME(FloatLiteral) },
375
+ ]);
376
+ },
377
+ },
371
378
  { ALT: () => $.CONSUME(TrueToken) },
372
379
  { ALT: () => $.CONSUME(FalseToken) },
373
380
  { ALT: () => $.CONSUME(NullToken) },
@@ -570,6 +570,11 @@ describe("expressionFn", () => {
570
570
  expect(parser.errors).to.be.empty;
571
571
  });
572
572
 
573
+ it("expressionFn accept parenthesis for priority for math", () => {
574
+ const parser = parse(`(1+2+4)*5-2`);
575
+ expect(parser.errors).to.be.empty;
576
+ });
577
+
573
578
  it("expressionFn accept math with variable", () => {
574
579
  const parser = parse(`2 * $input.a + 2 / $input.b - 2`);
575
580
  expect(parser.errors).to.be.empty;
@@ -286,6 +286,15 @@ export const filterNames = [
286
286
  "sql_esc",
287
287
  "sql_alias",
288
288
  "is_uuid",
289
+ "count_distinct",
290
+ "median",
291
+ "to_list",
292
+ "to_list_asc",
293
+ "to_list_desc",
294
+ "to_distinct_list",
295
+ "to_distinct_list_asc",
296
+ "to_distinct_list_desc",
297
+ "lambda",
289
298
  ];
290
299
 
291
300
  // List of input filters allowed for each input type
@@ -24,10 +24,11 @@ export function mcpServerDeclaration($) {
24
24
  // Allow leading comments and newlines before the mcp_server declaration
25
25
  $.SUBRULE($.optionalCommentBlockFn);
26
26
  const parent = $.CONSUME(McpServerToken); // mcp_server
27
- $.OR([
27
+ const nameToken = $.OR([
28
28
  { ALT: () => $.CONSUME(StringLiteral) },
29
29
  { ALT: () => $.CONSUME(Identifier) },
30
30
  ]);
31
+ $.validateDeclarationName(nameToken, "mcp_server");
31
32
  $.CONSUME(LCurly); // "{"
32
33
  $.MANY(() => {
33
34
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
@@ -25,10 +25,11 @@ export function mcpServerTriggerDeclaration($) {
25
25
  $.SUBRULE($.optionalCommentBlockFn);
26
26
 
27
27
  const parent = $.CONSUME(McpServerTriggerToken); // mcp_server_trigger
28
- $.OR([
28
+ const nameToken = $.OR([
29
29
  { ALT: () => $.CONSUME(StringLiteral) },
30
30
  { ALT: () => $.CONSUME(Identifier) },
31
31
  ]);
32
+ $.validateDeclarationName(nameToken, "mcp_server_trigger");
32
33
  $.CONSUME(LCurly); // "{"
33
34
  $.MANY(() => {
34
35
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
@@ -28,10 +28,11 @@ export function middlewareDeclaration($) {
28
28
  $.SUBRULE($.optionalCommentBlockFn);
29
29
 
30
30
  const parent = $.CONSUME(MiddlewareToken);
31
- $.OR([
31
+ const nameToken = $.OR([
32
32
  { ALT: () => $.CONSUME(Identifier) },
33
33
  { ALT: () => $.CONSUME(StringLiteral) },
34
34
  ]);
35
+ $.validateDeclarationName(nameToken, "middleware");
35
36
  $.CONSUME(LCurly); // "{"
36
37
  $.MANY(() => {
37
38
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
@@ -6,6 +6,7 @@ import { validateVariables } from "./variableValidator.js";
6
6
 
7
7
  const SEPARATOR = "\n---\n";
8
8
  const SEPARATOR_LENGTH = 5;
9
+ const SEPARATOR_NEWLINES = 2; // number of \n characters in SEPARATOR
9
10
 
10
11
  /**
11
12
  * Find the segment that contains the given global offset.
@@ -38,6 +39,23 @@ export function findSegmentAtOffset(parserResult, offset) {
38
39
  return null;
39
40
  }
40
41
 
42
+ /**
43
+ * Snapshot a Chevrotain token (or minimal token-like object) into a plain object.
44
+ * @param {Object|null} token
45
+ * @returns {Object|null}
46
+ */
47
+ function snapshotToken(token) {
48
+ if (!token) return null;
49
+ return {
50
+ startOffset: token.startOffset,
51
+ endOffset: token.endOffset,
52
+ startLine: token.startLine,
53
+ startColumn: token.startColumn,
54
+ image: token.image,
55
+ tokenTypeName: token.tokenType?.name,
56
+ };
57
+ }
58
+
41
59
  /**
42
60
  * Snapshot parser state into a plain object.
43
61
  * Must be called immediately after xanoscriptParser() before parsing next segment,
@@ -49,28 +67,22 @@ function snapshotParser(parser) {
49
67
  return {
50
68
  errors: parser.errors.map((e) => ({
51
69
  message: e.message,
52
- token: e.token
53
- ? { startOffset: e.token.startOffset, endOffset: e.token.endOffset }
54
- : null,
70
+ token: snapshotToken(e.token),
71
+ previousToken: snapshotToken(e.previousToken),
55
72
  })),
56
73
  warnings: parser.warnings.map((w) => ({
57
74
  message: w.message,
58
- token: w.token
59
- ? { startOffset: w.token.startOffset, endOffset: w.token.endOffset }
60
- : null,
75
+ token: snapshotToken(w.token),
61
76
  })),
62
77
  informations: parser.informations.map((i) => ({
63
78
  message: i.message,
64
- token: i.token
65
- ? { startOffset: i.token.startOffset, endOffset: i.token.endOffset }
66
- : null,
79
+ token: snapshotToken(i.token),
67
80
  })),
68
81
  hints: parser.hints.map((h) => ({
69
82
  message: h.message,
70
- token: h.token
71
- ? { startOffset: h.token.startOffset, endOffset: h.token.endOffset }
72
- : null,
83
+ token: snapshotToken(h.token),
73
84
  })),
85
+ sectionStack: [...parser.sectionStack],
74
86
  __symbolTable: parser.__symbolTable
75
87
  ? {
76
88
  input: { ...parser.__symbolTable.input },
@@ -94,17 +106,24 @@ function snapshotParser(parser) {
94
106
  * Apply a global offset to all position-bearing fields in a parser snapshot.
95
107
  * @param {Object} snapshot - The parser state snapshot
96
108
  * @param {number} globalOffset - The character offset to add
109
+ * @param {number} lineOffset - The line number offset to add
97
110
  */
98
- function offsetResult(snapshot, globalOffset) {
111
+ function offsetResult(snapshot, globalOffset, lineOffset) {
99
112
  if (globalOffset === 0) return;
100
113
 
101
114
  const offsetToken = (token) => {
102
115
  if (!token) return;
103
116
  token.startOffset += globalOffset;
104
117
  token.endOffset += globalOffset;
118
+ if (token.startLine != null) {
119
+ token.startLine += lineOffset;
120
+ }
105
121
  };
106
122
 
107
- for (const err of snapshot.errors) offsetToken(err.token);
123
+ for (const err of snapshot.errors) {
124
+ offsetToken(err.token);
125
+ offsetToken(err.previousToken);
126
+ }
108
127
  for (const warn of snapshot.warnings) offsetToken(warn.token);
109
128
  for (const info of snapshot.informations) offsetToken(info.token);
110
129
  for (const hint of snapshot.hints) offsetToken(hint.token);
@@ -161,6 +180,7 @@ export function multidocParser(text) {
161
180
  const segments = text.split(SEPARATOR);
162
181
  const results = [];
163
182
  let globalOffset = 0;
183
+ let lineOffset = 0;
164
184
 
165
185
  for (const segmentText of segments) {
166
186
  const parser = xanoscriptParser(segmentText);
@@ -199,8 +219,9 @@ export function multidocParser(text) {
199
219
  : null;
200
220
  // Deep-copy the snapshot before offsetResult mutates it (needed for hover providers)
201
221
  const localSnapshot = JSON.parse(JSON.stringify(snapshot));
202
- results.push({ snapshot, localSnapshot, segmentText, segmentSymbolTable, globalOffset });
222
+ results.push({ snapshot, localSnapshot, segmentText, segmentSymbolTable, globalOffset, lineOffset });
203
223
  globalOffset += segmentText.length + SEPARATOR_LENGTH;
224
+ lineOffset += (segmentText.match(/\n/g) || []).length + SEPARATOR_NEWLINES;
204
225
  }
205
226
 
206
227
  // For now, merge without offsetting (next task)
@@ -221,8 +242,8 @@ export function multidocParser(text) {
221
242
  segmentCount: segments.length,
222
243
  };
223
244
 
224
- for (const { snapshot, globalOffset } of results) {
225
- offsetResult(snapshot, globalOffset);
245
+ for (const { snapshot, globalOffset, lineOffset } of results) {
246
+ offsetResult(snapshot, globalOffset, lineOffset);
226
247
  merged.errors.push(...snapshot.errors);
227
248
  merged.warnings.push(...snapshot.warnings);
228
249
  merged.informations.push(...snapshot.informations);