@xano/xanoscript-language-server 11.6.5 → 11.7.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 (42) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/lexer/tokens.js +1 -1
  3. package/onCompletion/contentAssist.js +27 -23
  4. package/onHover/functions.md +9 -3
  5. package/package.json +1 -1
  6. package/parser/clauses/inputClause.js +22 -2
  7. package/parser/clauses/inputClause.spec.js +17 -0
  8. package/parser/clauses/nakedStackFn.js +1 -5
  9. package/parser/clauses/testClause.spec.js +0 -9
  10. package/parser/function_parser.js +4 -1
  11. package/parser/function_parser.spec.js +21 -0
  12. package/parser/functions/api/apiCallFn.js +18 -0
  13. package/parser/functions/api/apiCallFn.spec.js +2 -0
  14. package/parser/functions/api/apiRequestFn.js +1 -1
  15. package/parser/functions/api/apiRequestFn.spec.js +1 -1
  16. package/parser/functions/apiFn.js +2 -5
  17. package/parser/functions/controlFn.js +1 -0
  18. package/parser/functions/controls/conditionalFn.js +22 -7
  19. package/parser/functions/controls/conditionalFn.spec.js +21 -0
  20. package/parser/functions/controls/preconditionFn.spec.js +11 -1
  21. package/parser/functions/expect/unitExpectToBeWithinFn.spec.js +4 -2
  22. package/parser/functions/expect/unitExpectWithArgumentsFn.js +8 -2
  23. package/parser/functions/expect/unitExpectWithArgumentsFn.spec.js +15 -1
  24. package/parser/functions/expect/workflowExpectToBeWithinFn.js +2 -0
  25. package/parser/functions/expect/workflowExpectWithArgumentsFn.js +2 -0
  26. package/parser/functions/expect/workflowExpectWithArgumentsFn.spec.js +7 -0
  27. package/parser/functions/expect/workflowExpectWithoutArgumentsFn.js +2 -0
  28. package/parser/functions/schema/schemaFn.js +1 -2
  29. package/parser/functions/schema/schemaParseAttributeFn.spec.js +11 -9
  30. package/parser/functions/schema/schemaParseObjectFn.js +35 -10
  31. package/parser/functions/varFn.spec.js +15 -0
  32. package/parser/functions/workflowExpectFn.js +15 -1
  33. package/parser/functions/workflowExpectFn.spec.js +33 -0
  34. package/parser/generic/chainedIdentifier.js +2 -1
  35. package/parser/generic/expressionFn.js +2 -5
  36. package/parser/generic/functionAttrReq.js +1 -0
  37. package/parser/middleware_parser.js +2 -3
  38. package/parser/middleware_parser.spec.js +29 -2
  39. package/parser/parser.js +1 -0
  40. package/parser/query_parser.spec.js +21 -0
  41. package/parser/workflow_test_parser.js +1 -3
  42. package/parser/workflow_test_parser.spec.js +68 -2
@@ -7,7 +7,12 @@
7
7
  "Bash(git checkout:*)",
8
8
  "Bash(ls:*)",
9
9
  "Bash(npm run lint:*)",
10
- "Bash(grep:*)"
10
+ "Bash(grep:*)",
11
+ "Bash(node --input-type=module:*)",
12
+ "Bash(find:*)",
13
+ "Read(//tmp/**)",
14
+ "Bash(git stash:*)",
15
+ "Bash(node:*)"
11
16
  ]
12
17
  }
13
18
  }
package/lexer/tokens.js CHANGED
@@ -153,7 +153,7 @@ export const NullToken = createTokenByName("null", {
153
153
  export const RegExpToken = createToken({
154
154
  name: "RegExpToken",
155
155
  label: "regexp",
156
- pattern: /\/(?:[^/\n\\]|\\.)+\//,
156
+ pattern: /\/(?:[^/\n\\ ]|\\.)(?:[^/\n\\]|\\.)*\//,
157
157
  });
158
158
 
159
159
  export const WhiteSpace = createToken({
@@ -127,6 +127,11 @@ const filterSuggestions = Object.freeze(
127
127
  })
128
128
  );
129
129
 
130
+ // Maximum number of tokens to pass to computeContentAssist.
131
+ // Chevrotain's content assist has exponential complexity with complex grammars;
132
+ // beyond this threshold it can consume gigabytes of memory and OOM the process.
133
+ const MAX_CONTENT_ASSIST_TOKENS = 40;
134
+
130
135
  export function getContentAssistSuggestions(text, scheme) {
131
136
  try {
132
137
  const lexResult = lexDocument(text);
@@ -138,32 +143,31 @@ export function getContentAssistSuggestions(text, scheme) {
138
143
  return filterSuggestions;
139
144
  }
140
145
 
141
- let syntacticSuggestions;
142
- parser.reset();
143
- if (scheme === "db") {
144
- syntacticSuggestions = parser.computeContentAssist(
145
- "tableDeclaration",
146
- partialTokenVector
147
- );
148
- } else if (scheme === "api") {
149
- syntacticSuggestions = parser.computeContentAssist(
150
- "queryDeclaration",
151
- partialTokenVector
152
- );
153
- } else if (scheme === "function") {
154
- syntacticSuggestions = parser.computeContentAssist(
155
- "functionDeclaration",
156
- partialTokenVector
157
- );
158
- } else if (scheme === "task") {
159
- syntacticSuggestions = parser.computeContentAssist(
160
- "taskDeclaration",
161
- partialTokenVector
162
- );
163
- } else {
146
+ // Skip computeContentAssist for large token vectors to prevent OOM.
147
+ // Chevrotain explores all possible parse paths which grows exponentially
148
+ // with deeply nested grammars (e.g., arrays inside function blocks).
149
+ if (partialTokenVector.length > MAX_CONTENT_ASSIST_TOKENS) {
150
+ return [];
151
+ }
152
+
153
+ const ruleNameByScheme = {
154
+ db: "tableDeclaration",
155
+ api: "queryDeclaration",
156
+ function: "functionDeclaration",
157
+ task: "taskDeclaration",
158
+ };
159
+
160
+ const ruleName = ruleNameByScheme[scheme];
161
+ if (!ruleName) {
164
162
  return [];
165
163
  }
166
164
 
165
+ parser.reset();
166
+ const syntacticSuggestions = parser.computeContentAssist(
167
+ ruleName,
168
+ partialTokenVector
169
+ );
170
+
167
171
  // The suggestions also include the context, we are only interested
168
172
  // in the TokenTypes in this example.
169
173
  const tokenTypesSuggestions = syntacticSuggestions
@@ -398,12 +398,14 @@ api.call "account/{account_id}" verb=POST {
398
398
  } as $account_update_response
399
399
  ```
400
400
 
401
- Calls an internal Xano API endpoint, it allows you to invoke internal APIs within your end-to-end workflows tests.
401
+ Calls an internal Xano API endpoint from within a `workflow_test` or `test` clause. This allows you to invoke internal APIs as part of your end-to-end workflow tests or unit tests within queries, functions, and tasks.
402
+
403
+ **Note:** `api.call` is only available within `workflow_test`, `test` (in queries/functions/tasks/middleware), and `tool` contexts, including within control structures like `group`, `conditional`, loops, etc.
402
404
 
403
405
  - `api.call` signature is similar to the `query` definition it's invoking (e.g., `query <URL> verb=<METHOD> {...}`).
404
406
  - `api_group`: The API group to which the endpoint belongs.
405
- - `headers`: Custom headers to include in the request.
406
- - `input`: The request payload.
407
+ - `headers`: Optional custom headers to include in the request.
408
+ - `input`: The request payload containing parameters for the API call.
407
409
 
408
410
  # tool.call
409
411
 
@@ -2387,6 +2389,8 @@ test "value should always be greater than 0" {
2387
2389
 
2388
2390
  Defines a unit-test with a descriptive name (e.g., `"value should always be greater than 0"`). Each test can contain many `expect` blocks asserting conditions on the `$response` variable. The test will pass if all expectations are met. An optional `input` parameter can be passed to the stack being tested as its calling `input`.
2389
2391
 
2392
+ **Note:** `expect` statements are available within `test` clauses and `workflow_test` declarations. They cannot be used in regular queries or functions.
2393
+
2390
2394
  # expect.to_equal
2391
2395
 
2392
2396
  ```xs
@@ -2397,6 +2401,8 @@ expect.to_equal ($response.some_property) {
2397
2401
 
2398
2402
  Checks if the value of `$response.some_property` equals the specified `value`. Useful for basic equality checks in test assertions.
2399
2403
 
2404
+ **Note:** `expect` statements are only available within `workflow_test` declarations and `test` clauses. They cannot be used in regular queries or functions.
2405
+
2400
2406
  # expect.to_start_with
2401
2407
 
2402
2408
  ```xs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.6.5",
3
+ "version": "11.7.1",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -1,3 +1,4 @@
1
+ import { MismatchedTokenException } from "chevrotain";
1
2
  import { LCurly, RCurly } from "../../lexer/control.js";
2
3
  import { InputToken, NewlineToken } from "../../lexer/tokens.js";
3
4
 
@@ -7,12 +8,19 @@ import { InputToken, NewlineToken } from "../../lexer/tokens.js";
7
8
  export function inputClause($) {
8
9
  return () => {
9
10
  $.sectionStack.push("inputClause");
11
+
12
+ let hasStatement = false;
13
+ let hasNewLine = false;
14
+
10
15
  $.CONSUME(InputToken); // "input"
11
16
  // unlike schema, input can be mentioned without any fields
12
17
  $.OPTION(() => {
13
18
  $.CONSUME(LCurly); // "{"
14
19
  $.MANY(() => {
15
- $.AT_LEAST_ONE1(() => $.CONSUME1(NewlineToken)); // at least one new line
20
+ $.MANY1(() => {
21
+ $.CONSUME1(NewlineToken);
22
+ hasNewLine = true;
23
+ }); // at least one new line
16
24
  $.SUBRULE($.optionalCommentBlockFn);
17
25
 
18
26
  $.OR([
@@ -26,8 +34,20 @@ export function inputClause($) {
26
34
  ALT: () => $.SUBRULE($.dbLinkColumnDefinition),
27
35
  },
28
36
  ]);
37
+
38
+ if (hasStatement && !hasNewLine) {
39
+ $.SAVE_ERROR(
40
+ new MismatchedTokenException(
41
+ "Each statement in the input clause must be on a new line",
42
+ $.LA(0),
43
+ ),
44
+ );
45
+ }
46
+
47
+ hasNewLine = false;
48
+ hasStatement = true;
29
49
  });
30
- $.AT_LEAST_ONE2(() => $.CONSUME2(NewlineToken)); // at least one new line
50
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // can have new lines at the end of the input clause
31
51
  $.CONSUME(RCurly); // "}"
32
52
  });
33
53
  $.sectionStack.pop();
@@ -12,6 +12,23 @@ function parse(inputText) {
12
12
  }
13
13
 
14
14
  describe("inputClause", () => {
15
+ it("inputClause accepts empty input", () => {
16
+ const parser = parse(`input {}`);
17
+ expect(parser.errors).to.be.empty;
18
+ });
19
+
20
+ it("inputClause accepts a single input as one liner", () => {
21
+ const parser = parse(`input { email email_input? filters=trim|lower }`);
22
+ expect(parser.errors).to.be.empty;
23
+ });
24
+
25
+ it("inputClause cannot have 2 statement on the same line", () => {
26
+ const parser = parse(
27
+ `input { email email_input? filters=trim|lower text name }`,
28
+ );
29
+ expect(parser.errors).to.not.be.empty;
30
+ });
31
+
15
32
  it("inputClause accepts filters and field definition", () => {
16
33
  const parser = parse(`input {
17
34
  email email_input? filters=trim|lower
@@ -4,7 +4,6 @@ import { LCurly, RCurly } from "../../lexer/control.js";
4
4
  import { NewlineToken } from "../../lexer/tokens.js";
5
5
 
6
6
  export const DEFAULT_OPTIONS = {
7
- allowExpectStatements: false,
8
7
  allowCallStatements: false,
9
8
  };
10
9
 
@@ -56,10 +55,7 @@ export function nakedStackFn($) {
56
55
  GATE: () => options.allowCallStatements,
57
56
  ALT: () => $.SUBRULE($.middlewareCallFn),
58
57
  },
59
- {
60
- GATE: () => options.allowExpectStatements,
61
- ALT: () => $.SUBRULE($.workflowExpectFn),
62
- },
58
+ { ALT: () => $.SUBRULE($.workflowExpectFn) },
63
59
  ]);
64
60
  });
65
61
  $.AT_LEAST_ONE2(() => $.CONSUME2(NewlineToken));
@@ -20,15 +20,6 @@ describe("testClause", () => {
20
20
  expect(parser.errors).to.be.empty;
21
21
  });
22
22
 
23
- it("testClause accepts a comment", () => {
24
- const parser = parse(`test "should add numbers" {
25
- expect.to_equal ($response) {
26
- value = 5
27
- }
28
- }`);
29
- expect(parser.errors).to.be.empty;
30
- });
31
-
32
23
  it("testClause requires accepts an optional datasource", () => {
33
24
  const parser = parse(`test "should add numbers" {
34
25
  datasource = "live"
@@ -29,7 +29,10 @@ export function functionDeclaration($) {
29
29
  ]);
30
30
 
31
31
  if (nameToken?.image && getVarName(nameToken).startsWith("/")) {
32
- $.addInvalidValueError(nameToken, "function name must not start with '/'");
32
+ $.addInvalidValueError(
33
+ nameToken,
34
+ "function name must not start with '/'",
35
+ );
33
36
  }
34
37
 
35
38
  $.CONSUME(LCurly); // "{"
@@ -93,4 +93,25 @@ describe("function_parser", () => {
93
93
  }`);
94
94
  expect(parser.errors).to.not.be.empty;
95
95
  });
96
+
97
+ it("should not allow api.call in a function stack", () => {
98
+ const parser = xanoscriptParser(`function foo {
99
+ input {
100
+ }
101
+
102
+ stack {
103
+ group {
104
+ stack {
105
+ api.call "users/{user_id}" verb=GET {
106
+ api_group = "account"
107
+ input = { user_id: 123 }
108
+ } as $user
109
+ }
110
+ }
111
+ }
112
+
113
+ response = null
114
+ }`);
115
+ expect(parser.errors).to.not.be.empty;
116
+ });
96
117
  });
@@ -18,6 +18,24 @@ export function apiCallFn($) {
18
18
  return () => {
19
19
  $.sectionStack.push("apiCallFn");
20
20
  const fnToken = $.CONSUME(CallToken); // "call"
21
+
22
+ // Validate that api.call is only used in allowed contexts
23
+ const validContexts = [
24
+ "workflowTestDeclaration",
25
+ "toolDeclaration",
26
+ "testClause",
27
+ ];
28
+ const isInValidContext = validContexts.some((context) =>
29
+ $.sectionStack.includes(context),
30
+ );
31
+
32
+ if (!isInValidContext) {
33
+ $.addInvalidValueError(
34
+ fnToken,
35
+ "api.call can only be used within workflow_test, tool, or test contexts",
36
+ );
37
+ }
38
+
21
39
  $.OR([
22
40
  { ALT: () => $.CONSUME(StringLiteral) }, // "foo/bar"
23
41
  { ALT: () => $.CONSUME(Identifier) }, // foo
@@ -7,6 +7,8 @@ function parse(inputText) {
7
7
  parser.reset();
8
8
  const lexResult = lexDocument(inputText);
9
9
  parser.input = lexResult.tokens;
10
+ // Simulate being inside a test context for api.call validation
11
+ parser.sectionStack.push("testClause");
10
12
  parser.apiCallFn();
11
13
  return parser;
12
14
  }
@@ -29,7 +29,7 @@ export function apiRequestFn($) {
29
29
  "disabled?": "[boolean]",
30
30
  "params?": "[expression]",
31
31
  "headers?": "[expression]",
32
- "timeout?": "[number]",
32
+ "timeout?": "[expression]",
33
33
  "follow_location?": "[expression]",
34
34
  "ca_certificate?": "[string]",
35
35
  "certificate?": "[string]",
@@ -53,7 +53,7 @@ describe("apiRequestFn", () => {
53
53
  method = "POST"
54
54
  params = {}|set:"foo":"bar"
55
55
  headers = []|push:"Set-Cookie: sessionId=e8bb43229de9; Domain=foo.example.com"
56
- timeout = 25
56
+ timeout = $input.timeout
57
57
  ca_certificate = "what is this"
58
58
  certificate = """
59
59
  -----BEGIN CERTIFICATE-----
@@ -5,15 +5,12 @@ import { DotToken } from "../../lexer/tokens.js";
5
5
  * @param {import('../base_parser.js').XanoBaseParser} $
6
6
  */
7
7
  export function apiFn($) {
8
- return (options = {}) => {
8
+ return () => {
9
9
  $.sectionStack.push("api");
10
10
  $.CONSUME(ApiToken); // "api"
11
11
  $.CONSUME(DotToken); // "."
12
12
  $.OR([
13
- {
14
- GATE: () => options.allowCallStatements,
15
- ALT: () => $.SUBRULE($.apiCallFn), // "api.call"
16
- },
13
+ { ALT: () => $.SUBRULE($.apiCallFn) }, // "api.call"
17
14
  { ALT: () => $.SUBRULE($.apiLambdaFn) }, // "api.lambda"
18
15
  { ALT: () => $.SUBRULE($.apiRequestFn) }, // "api.request"
19
16
  { ALT: () => $.SUBRULE($.apiRealtimeEventFn) }, // "api.realtime_event"
@@ -6,6 +6,7 @@ import { BreakToken, ContinueToken } from "../../lexer/control.js";
6
6
  */
7
7
  export function controlFn($) {
8
8
  return (options = {}) => {
9
+ $.sectionStack.push("controlFn");
9
10
  $.OR([
10
11
  { ALT: () => $.CONSUME(BreakToken) }, // "break"
11
12
  { ALT: () => $.CONSUME(ContinueToken) }, // "continue"
@@ -10,6 +10,7 @@ export function conditionalFn($) {
10
10
  let hasIfStatement = false;
11
11
  let hasDescription = false;
12
12
  let hasDisabled = false;
13
+ let hasElseStatement = false;
13
14
 
14
15
  $.sectionStack.push("conditionalFn");
15
16
  const parent = $.CONSUME(ConditionalToken); // "conditional"
@@ -37,13 +38,27 @@ export function conditionalFn($) {
37
38
  ALT: () => {
38
39
  hasIfStatement = true;
39
40
  $.SUBRULE($.conditionalIfFn);
40
- $.MANY1(() => {
41
- $.AT_LEAST_ONE1(() => $.CONSUME3(NewlineToken));
42
- $.SUBRULE($.conditionalElifFn);
43
- });
44
- $.OPTION(() => {
45
- $.AT_LEAST_ONE2(() => $.CONSUME4(NewlineToken));
46
- $.SUBRULE($.conditionalElseFn);
41
+
42
+ // as soon as we hit an else statement, we know there can be no more elif statements, so we only check for the absence of an else statement
43
+ $.MANY1({
44
+ GATE: () => !hasElseStatement,
45
+ DEF: () => {
46
+ $.AT_LEAST_ONE1(() => $.CONSUME3(NewlineToken));
47
+ $.SUBRULE($.optionalCommentBlockFn);
48
+ $.OR1([
49
+ {
50
+ ALT: () => {
51
+ hasElseStatement = true;
52
+ $.SUBRULE($.conditionalElseFn);
53
+ },
54
+ },
55
+ {
56
+ ALT: () => {
57
+ $.SUBRULE($.conditionalElifFn);
58
+ },
59
+ },
60
+ ]);
61
+ },
47
62
  });
48
63
  },
49
64
  },
@@ -108,6 +108,27 @@ describe("conditionalFn", () => {
108
108
  expect(parser.errors).to.be.empty;
109
109
  });
110
110
 
111
+ it("conditionalFn accepts a comment above the else", () => {
112
+ const parser = parse(`conditional {
113
+ // If destination address is a system account.
114
+ if ($some_condition) {
115
+ // Getting the final destination
116
+ var $destination {
117
+ value = "https://example.com/default_destination/x"
118
+ }
119
+ }
120
+
121
+ // Set the final destination URL
122
+ else {
123
+ // Getting the final destination
124
+ var $destination {
125
+ value = "https://example.com/default_destination"
126
+ }
127
+ }
128
+ }`);
129
+ expect(parser.errors).to.be.empty;
130
+ });
131
+
111
132
  it("conditionalFn accepts a description", () => {
112
133
  const parser = parse(`conditional {
113
134
  description = "foo"
@@ -28,9 +28,19 @@ describe("preconditionFn", () => {
28
28
  expect(parser.errors).to.be.empty;
29
29
  });
30
30
 
31
+ it("should accept a complex precondition expression", () => {
32
+ const parser =
33
+ parse(`precondition (($a_rate >= ($rate_token.x_rate * (1 - ($env.x_rate_all / 100)))) && ($a_rate <= ($rate_token.x_rate * (1 + ($env.x_rate_all / 100))))) {
34
+ error_type = "badrequest"
35
+ error = "ohoh"
36
+ payload = "The rate is off"
37
+ }`);
38
+ expect(parser.errors).to.be.empty;
39
+ });
40
+
31
41
  it("preconditionFn accept an escaped string", () => {
32
42
  const parser = parse(
33
- `precondition ($escaped == '\\"hello this is chris\\\\\\' test\\"')`
43
+ `precondition ($escaped == '\\"hello this is chris\\\\\\' test\\"')`,
34
44
  );
35
45
  expect(parser.errors).to.be.empty;
36
46
  });
@@ -29,8 +29,10 @@ describe("unitExpectToBeWithinFn", () => {
29
29
  });
30
30
 
31
31
  it("unitExpectToBeWithinFn does not accepts filters on value argument", () => {
32
- const parser =
33
- parse(`to_be_within (($response|get:foo:"bar")|first|concat:bar) {
32
+ const parser = parse(`to_be_within (($response|get:foo:"bar")
33
+ |first
34
+ |concat:bar
35
+ ) {
34
36
  min = 11
35
37
  max = 13
36
38
  }`);
@@ -10,7 +10,10 @@ import {
10
10
  ToStartWithToken,
11
11
  } from "../../../lexer/expect.js";
12
12
  import { DotToken, Identifier } from "../../../lexer/tokens.js";
13
- import { ResponseVariable } from "../../../lexer/variables.js";
13
+ import {
14
+ ResponseVariable,
15
+ ShortFormVariable,
16
+ } from "../../../lexer/variables.js";
14
17
 
15
18
  /**
16
19
  * @param {import('../../base_parser.js').XanoBaseParser} $
@@ -32,7 +35,10 @@ export function unitExpectWithArgumentsFn($) {
32
35
  $.CONSUME(ResponseVariable); // "$response"
33
36
  $.MANY(() => {
34
37
  $.CONSUME(DotToken); // "."
35
- $.CONSUME(Identifier); // "x", "users", etc.
38
+ $.OR1([
39
+ { ALT: () => $.CONSUME(Identifier) }, // "x", "users", etc.
40
+ { ALT: () => $.CONSUME(ShortFormVariable) }, // "$xyz"
41
+ ]);
36
42
  });
37
43
  $.CONSUME(RParent); // ")"
38
44
 
@@ -16,7 +16,14 @@ describe("unitExpectWithArgumentsFn", () => {
16
16
  it("unitExpectWithArgumentsFn to_equal accepts a $response variable", () => {
17
17
  const parser = parse(`to_equal ($response) {
18
18
  value = "foo"
19
- };`);
19
+ }`);
20
+ expect(parser.errors).to.be.empty;
21
+ });
22
+
23
+ it("unitExpectWithArgumentsFn to_equal accepts a $response subpath with $ prefix", () => {
24
+ const parser = parse(`to_equal ($response.$xyz) {
25
+ value = "foo"
26
+ }`);
20
27
  expect(parser.errors).to.be.empty;
21
28
  });
22
29
 
@@ -27,6 +34,13 @@ describe("unitExpectWithArgumentsFn", () => {
27
34
  expect(parser.errors).to.not.be.empty;
28
35
  });
29
36
 
37
+ it("unitExpectWithArgumentsFn accepts an expression", () => {
38
+ const parser = parse(`to_equal ($response) {
39
+ value = \`12092834098240928304982039840923908402384.09823904823098402834\`
40
+ }`);
41
+ expect(parser.errors).to.be.empty;
42
+ });
43
+
30
44
  it("unitExpectWithArgumentsFn does not accept variables", () => {
31
45
  const parser = parse(`to_equal ($response) {
32
46
  value = $response.x
@@ -1,5 +1,6 @@
1
1
  import { LParent, RParent } from "../../../lexer/control.js";
2
2
  import { ToBeWithinToken } from "../../../lexer/expect.js";
3
+ import { NewlineToken } from "../../../lexer/tokens.js";
3
4
 
4
5
  /**
5
6
  * @param {import('../../base_parser.js').XanoBaseParser} $
@@ -13,6 +14,7 @@ export function workflowExpectToBeWithinFn($) {
13
14
  const fnToken = $.CONSUME(ToBeWithinToken); // "to_be_within"
14
15
  $.CONSUME(LParent); // "("
15
16
  $.SUBRULE($.expressionFn); // "$foo|get:bar:null"
17
+ $.MANY(() => $.CONSUME(NewlineToken)); // allow newlines between the expression and the attributes
16
18
  $.CONSUME(RParent); // ")"
17
19
  $.SUBRULE($.functionAttrReq, {
18
20
  ARGS: [fnToken, requiredAttrs, optionalAttrs],
@@ -9,6 +9,7 @@ import {
9
9
  ToNotEqualToken,
10
10
  ToStartWithToken,
11
11
  } from "../../../lexer/expect.js";
12
+ import { NewlineToken } from "../../../lexer/tokens.js";
12
13
 
13
14
  /**
14
15
  * @param {import('../../base_parser.js').XanoBaseParser} $
@@ -31,6 +32,7 @@ export function workflowExpectWithArgumentsFn($) {
31
32
  ]);
32
33
  $.CONSUME(LParent); // "("
33
34
  $.SUBRULE($.expressionFn); // "$foo|get:bar:null"
35
+ $.MANY(() => $.CONSUME(NewlineToken)); // allow newlines between the expression and the attributes
34
36
  $.CONSUME(RParent); // ")"
35
37
  $.SUBRULE($.functionAttrReq, {
36
38
  ARGS: [fnToken, requiredAttrs, optionalAttrs],
@@ -27,6 +27,13 @@ describe("workflowExpectWithArgumentsFn", () => {
27
27
  expect(parser.errors).to.be.empty;
28
28
  });
29
29
 
30
+ it("accept string a named attribute", () => {
31
+ const parser = parse(`to_equal ($attr.sub."some value") {
32
+ value = "enabled"
33
+ }`);
34
+ expect(parser.errors).to.be.empty;
35
+ });
36
+
30
37
  it("workflowExpectWithArgumentsFn accepts a filtered variable", () => {
31
38
  // foo is not in quotes
32
39
  let parser = parse(`to_equal ($x|get:foo:"bar") {
@@ -10,6 +10,7 @@ import {
10
10
  ToNotBeDefinedToken,
11
11
  ToNotBeNullToken,
12
12
  } from "../../../lexer/expect.js";
13
+ import { NewlineToken } from "../../../lexer/tokens.js";
13
14
 
14
15
  /**
15
16
  * @param {import('../../base_parser.js').XanoBaseParser} $
@@ -30,6 +31,7 @@ export function workflowExpectWithoutArgumentsFn($) {
30
31
  ]);
31
32
  $.CONSUME(LParent); // "("
32
33
  $.SUBRULE($.expressionFn); // "$foo|get:bar:null"
34
+ $.MANY(() => $.CONSUME(NewlineToken)); // allow newlines between the expression and the attributes
33
35
  $.CONSUME(RParent); // ")"
34
36
  $.SUBRULE($.functionAttrReq, {
35
37
  ARGS: [fnToken, [], ["description", "disabled"]],
@@ -64,7 +64,7 @@ export function schemaFn($) {
64
64
  schemaExpectOneOf(
65
65
  schema,
66
66
  ["string", "number", "boolean", "timestamp"],
67
- captured
67
+ captured,
68
68
  ),
69
69
  ALT: () => {
70
70
  $.SUBRULE($.schemaParseImmutableFn, {
@@ -130,7 +130,6 @@ export function schemaFn($) {
130
130
  $.SUBRULE($.nakedStackFn, {
131
131
  ARGS: [
132
132
  {
133
- allowExpectStatements: isTest,
134
133
  allowCallStatements: isTest,
135
134
  },
136
135
  ],
@@ -14,7 +14,7 @@ export function parserExtension() {
14
14
  const name = this.CONSUME(Identifier);
15
15
  this.CONSUME(EqualToken); // "="
16
16
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
17
- }
17
+ },
18
18
  );
19
19
 
20
20
  this.schemaParseAttributeFn_expression = this.RULE(
@@ -26,7 +26,7 @@ export function parserExtension() {
26
26
  };
27
27
  const name = this.CONSUME(Identifier);
28
28
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
29
- }
29
+ },
30
30
  );
31
31
 
32
32
  // this rule requires a foo field to be defined
@@ -36,7 +36,7 @@ export function parserExtension() {
36
36
  const schema = { "disabled?": "[boolean]", "description?": "[string]" };
37
37
  const name = this.CONSUME(Identifier);
38
38
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
39
- }
39
+ },
40
40
  );
41
41
 
42
42
  this.schemaParseAttributeFn_deep = this.RULE(
@@ -52,7 +52,7 @@ export function parserExtension() {
52
52
  const name = this.CONSUME(Identifier);
53
53
  this.CONSUME(EqualToken); // "="
54
54
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
55
- }
55
+ },
56
56
  );
57
57
 
58
58
  this.schemaParseAttributeFn_stack = this.RULE(
@@ -65,7 +65,7 @@ export function parserExtension() {
65
65
  };
66
66
  const name = this.CONSUME(Identifier);
67
67
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
68
- }
68
+ },
69
69
  );
70
70
 
71
71
  this.schemaParseAttributeFn_optional = this.RULE(
@@ -80,7 +80,7 @@ export function parserExtension() {
80
80
  const name = this.CONSUME(Identifier);
81
81
  this.CONSUME(EqualToken); // "="
82
82
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
83
- }
83
+ },
84
84
  );
85
85
 
86
86
  this.schemaParseAttributeFn_multiple = this.RULE(
@@ -93,7 +93,7 @@ export function parserExtension() {
93
93
  const name = this.CONSUME(Identifier);
94
94
  this.CONSUME(EqualToken); // "="
95
95
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
96
- }
96
+ },
97
97
  );
98
98
 
99
99
  this.schemaParseAttributeFn_disabled = this.RULE(
@@ -108,7 +108,7 @@ export function parserExtension() {
108
108
  const name = this.CONSUME(Identifier);
109
109
  this.CONSUME(EqualToken); // "="
110
110
  this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
111
- }
111
+ },
112
112
  );
113
113
  }
114
114
 
@@ -116,6 +116,8 @@ function parse(inputText) {
116
116
  parser.reset();
117
117
  const lexResult = lexDocument(inputText);
118
118
  parser.input = lexResult.tokens;
119
+ // Simulate being inside a test context for api.call validation
120
+ parser.sectionStack.push("testClause");
119
121
  return parser;
120
122
  }
121
123
 
@@ -131,7 +133,7 @@ describe("schemaParseAttributeFn", () => {
131
133
  expect(parser.errors).to.be.empty;
132
134
 
133
135
  parse(
134
- `value = { something = 123.2, other_thing = 44 }`
136
+ `value = { something = 123.2, other_thing = 44 }`,
135
137
  ).schemaParseAttributeFn_flat();
136
138
  expect(parser.errors).to.be.empty;
137
139
  });
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../../lexer/control.js";
9
9
  import { StringLiteral } from "../../../lexer/literal.js";
10
10
  import { DotToken, Identifier, NewlineToken } from "../../../lexer/tokens.js";
11
+ import { InputVariable, ShortFormVariable } from "../../../lexer/variables.js";
11
12
  import {
12
13
  canBeDisabledAttribute,
13
14
  isMultiAttribute,
@@ -40,7 +41,7 @@ export function schemaParseObjectFn($) {
40
41
  some(objectKeys, (k) => isSchemaGenericType(k))
41
42
  ) {
42
43
  throw new Error(
43
- "schemaParseObjectFn supports only one generic type when multiple keys are defined"
44
+ "schemaParseObjectFn supports only one generic type when multiple keys are defined",
44
45
  );
45
46
  }
46
47
  });
@@ -55,7 +56,7 @@ export function schemaParseObjectFn($) {
55
56
  if (needSeparator) {
56
57
  $.addInvalidValueError(
57
58
  lastToken,
58
- "Expected a comma, a new line or closing bracket"
59
+ "Expected a comma, a new line or closing bracket",
59
60
  );
60
61
  }
61
62
 
@@ -82,25 +83,49 @@ export function schemaParseObjectFn($) {
82
83
  return objectKeyToken.image.slice(1, -1);
83
84
  },
84
85
  },
86
+ {
87
+ // dynamic key using $input.property
88
+ ALT: () => {
89
+ objectKeyToken = $.CONSUME(InputVariable);
90
+ const tokens = [objectKeyToken];
91
+ $.MANY4(() => {
92
+ tokens.push($.CONSUME1(DotToken));
93
+ tokens.push($.CONSUME2(Identifier));
94
+ });
95
+ return tokens.map((t) => t.image).join(".");
96
+ },
97
+ },
98
+ {
99
+ // dynamic key using $variable or $variable.property
100
+ ALT: () => {
101
+ objectKeyToken = $.CONSUME(ShortFormVariable);
102
+ const tokens = [objectKeyToken];
103
+ $.MANY5(() => {
104
+ tokens.push($.CONSUME2(DotToken));
105
+ tokens.push($.CONSUME3(Identifier));
106
+ });
107
+ return tokens.map((t) => t.image).join(".");
108
+ },
109
+ },
85
110
  ]);
86
111
 
87
112
  const subSchemaKey = valueMatchRequirements(keyValue, objectKeys);
88
113
  if (!subSchemaKey) {
89
114
  $.addInvalidValueError(
90
115
  objectKeyToken,
91
- `The key '${keyValue}' is not valid in this context`
116
+ `The key '${keyValue}' is not valid in this context`,
92
117
  );
93
118
  } else if (disabledToken && !canBeDisabledAttribute(subSchemaKey)) {
94
119
  $.addInvalidValueError(
95
120
  disabledToken,
96
- `The key '${keyValue}' cannot be disabled`
121
+ `The key '${keyValue}' cannot be disabled`,
97
122
  );
98
123
  }
99
124
 
100
125
  if (keyNames.includes(keyValue) && !isMultiAttribute(subSchemaKey)) {
101
126
  $.addInvalidValueError(
102
127
  objectKeyToken,
103
- `Duplicate key '${keyValue}' found`
128
+ `Duplicate key '${keyValue}' found`,
104
129
  );
105
130
  } else {
106
131
  keyNames.push(keyValue);
@@ -133,25 +158,25 @@ export function schemaParseObjectFn($) {
133
158
 
134
159
  if (missingKeys.length) {
135
160
  const missingKeyNames = missingKeys.filter(
136
- (k) => !isOptionalAttribute(k)
161
+ (k) => !isOptionalAttribute(k),
137
162
  );
138
163
  if (missingKeyNames.length > 1) {
139
164
  $.addMissingError(
140
165
  lastToken || token,
141
166
  `{} is missing the following required entries: ${missingKeyNames.join(
142
- ", "
143
- )}`
167
+ ", ",
168
+ )}`,
144
169
  );
145
170
  } else if (missingKeyNames.length === 1) {
146
171
  if (isSchemaGenericType(missingKeyNames[0])) {
147
172
  $.addMissingError(
148
173
  lastToken || token,
149
- `{} requires a least one entry`
174
+ `{} requires a least one entry`,
150
175
  );
151
176
  } else {
152
177
  $.addMissingError(
153
178
  lastToken || token,
154
- `{} is missing required key '${missingKeyNames[0]}'`
179
+ `{} is missing required key '${missingKeyNames[0]}'`,
155
180
  );
156
181
  }
157
182
  }
@@ -142,6 +142,14 @@ describe("varFn", () => {
142
142
  expect(parser.errors).to.be.empty;
143
143
  });
144
144
 
145
+ it("var.update should allow $input reference in its filters", () => {
146
+ const parser = parse(`var.update $object_for_update {
147
+ value = $object_for_update
148
+ |merge_recursive:{$input.key:$input.value}
149
+ }`);
150
+ expect(parser.errors).to.be.empty;
151
+ });
152
+
145
153
  it("should allow accessing a property from an object defined in place", () => {
146
154
  let parser = parse(`var $config {
147
155
  value = {
@@ -267,6 +275,13 @@ describe("varFn", () => {
267
275
  expect(parser.errors).to.not.be.empty;
268
276
  });
269
277
 
278
+ it("accepts a backtick expression as value", () => {
279
+ const parser = parse(`var $current_time {
280
+ value = \`12316546543321321.3213521654654\`
281
+ }`);
282
+ expect(parser.errors).to.be.empty;
283
+ });
284
+
270
285
  // All blacklisted names should be rejected in long form via var.update
271
286
  const allBlacklisted = [
272
287
  "auth",
@@ -7,7 +7,21 @@ import { DotToken } from "../../lexer/tokens.js";
7
7
  export function workflowExpectFn($) {
8
8
  return () => {
9
9
  $.sectionStack.push("workflowExpect");
10
- $.CONSUME(ExpectToken); // "expect"
10
+ const expectToken = $.CONSUME(ExpectToken); // "expect"
11
+
12
+ // Validate that expect statements are only used in allowed contexts
13
+ const validContexts = ["workflowTestDeclaration", "testClause"];
14
+ const isInValidContext = validContexts.some((context) =>
15
+ $.sectionStack.includes(context),
16
+ );
17
+
18
+ if (!isInValidContext) {
19
+ $.addInvalidValueError(
20
+ expectToken,
21
+ "expect statements can only be used within workflow_test or test contexts",
22
+ );
23
+ }
24
+
11
25
  $.CONSUME(DotToken); // "."
12
26
  $.OR([
13
27
  { ALT: () => $.SUBRULE($.workflowExpectWithArgumentsFn) }, // "expect.to_equal, expect.to_start_with, etc."
@@ -0,0 +1,33 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { lexDocument } from "../../lexer/lexer.js";
4
+ import { parser } from "../test_parser.js";
5
+
6
+ function parse(inputText) {
7
+ parser.reset();
8
+ const lexResult = lexDocument(inputText);
9
+ parser.input = lexResult.tokens;
10
+ parser.sectionStack.push("workflowTestDeclaration");
11
+ parser.workflowExpectFn();
12
+ return parser;
13
+ }
14
+
15
+ describe("workflowExpectFn", () => {
16
+ it("workflowExpectFn accepts a string literal as value", () => {
17
+ const parser = parse(`expect.to_equal ($response.email) {
18
+ value = "email@example.com"
19
+ }`);
20
+ expect(parser.errors).to.be.empty;
21
+ });
22
+
23
+ it("workflowExpectFn accepts filtered values", () => {
24
+ const parser = parse(`expect.to_equal ($total
25
+ |subtract:$tax
26
+ |subtract:$cost
27
+ ) {
28
+ value = $som_value
29
+ }`);
30
+ console.log(parser.errors);
31
+ expect(parser.errors).to.be.empty;
32
+ });
33
+ });
@@ -1,4 +1,4 @@
1
- import { IntegerLiteral } from "../../lexer/literal.js";
1
+ import { IntegerLiteral, StringLiteral } from "../../lexer/literal.js";
2
2
  import { DotToken, Identifier } from "../../lexer/tokens.js";
3
3
  import { ShortFormVariable } from "../../lexer/variables.js";
4
4
 
@@ -38,6 +38,7 @@ export function singleChainedIdentifier($) {
38
38
  }, // e.g., .bar
39
39
  { ALT: () => $.CONSUME(Identifier) }, // e.g., .bar
40
40
  { ALT: () => $.CONSUME2(IntegerLiteral) }, // e.g., .3
41
+ { ALT: () => $.CONSUME(StringLiteral) }, // e.g., ."some value"
41
42
  { ALT: () => $.SUBRULE($.bracketAccessor) }, // e.g., .["key"]
42
43
  ]);
43
44
  },
@@ -235,14 +235,14 @@ export function expressionFn($) {
235
235
  if (hasFilters && hasTests) {
236
236
  $.addInvalidValueError(
237
237
  token,
238
- "An expression should be wrapped in parentheses when combining filters and tests"
238
+ "An expression should be wrapped in parentheses when combining filters and tests",
239
239
  );
240
240
  }
241
241
 
242
242
  if (options.allowQueryExpression && has_ternary) {
243
243
  $.addInvalidValueError(
244
244
  testToken,
245
- "Ternary expressions are not allowed in a where clause"
245
+ "Ternary expressions are not allowed in a where clause",
246
246
  );
247
247
  }
248
248
  };
@@ -373,15 +373,12 @@ export function simpleExpressionFn($) {
373
373
 
374
374
  // Group 2: Complex literals (distinct tokens)
375
375
  {
376
- GATE: () => options.allowExpression,
377
376
  ALT: () => $.CONSUME(MultiLineStringToken),
378
377
  },
379
378
  {
380
- GATE: () => options.allowExpression,
381
379
  ALT: () => $.CONSUME(MultiLineExpressionToken),
382
380
  },
383
381
  {
384
- GATE: () => options.allowExpression,
385
382
  ALT: () => $.CONSUME(ExpressionLiteral),
386
383
  },
387
384
 
@@ -24,6 +24,7 @@ export function functionAttrReq($) {
24
24
  * @param {string[]} options.allowQueryExpression special attriutes that will allow query expressions
25
25
  **/
26
26
  return (parent, required, optional, options = {}) => {
27
+ $.sectionStack.push("functionAttrReq");
27
28
  // setup basic defaults
28
29
  required = required || [];
29
30
  optional = optional || [];
@@ -1,3 +1,4 @@
1
+ import { CommentToken } from "../lexer/comment.js";
1
2
  import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
2
3
  import { StringLiteral } from "../lexer/literal.js";
3
4
  import {
@@ -34,9 +35,7 @@ export function middlewareDeclaration($) {
34
35
  $.MANY(() => {
35
36
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
36
37
  $.OR2([
37
- {
38
- ALT: () => $.SUBRULE($.commentBlockFn),
39
- },
38
+ { ALT: () => $.CONSUME(CommentToken) },
40
39
  {
41
40
  GATE: () => !hasDescription,
42
41
  ALT: () => {
@@ -60,6 +60,33 @@ describe("middleware_parser", () => {
60
60
  expect(parser.errors).to.not.be.empty;
61
61
  });
62
62
 
63
+ it("middleware should accept a test", () => {
64
+ const parser = xanoscriptParser(`middleware foo {
65
+ input {
66
+ }
67
+
68
+ stack {
69
+ }
70
+
71
+ response = null
72
+ response_strategy = "replace"
73
+ exception_policy = "rethrow"
74
+
75
+ // test cases
76
+ test "testset " {
77
+ input = {
78
+ adoption_application_id: ""
79
+ user_id : ""
80
+ completed_only : ""
81
+ scheduled_only : ""
82
+ page : ""
83
+ per_page : ""
84
+ }
85
+ }
86
+ }`);
87
+ expect(parser.errors).to.be.empty;
88
+ });
89
+
63
90
  describe("ResponseStrategy", () => {
64
91
  it("should parse middleware with response_strategy = 'merge'", () => {
65
92
  const parser = xanoscriptParser(`middleware test_merge {
@@ -109,7 +136,7 @@ describe("middleware_parser", () => {
109
136
  expect(parser.errors).to.not.be.empty;
110
137
  expect(parser.errors[0].message).to.include('Invalid value "invalid"');
111
138
  expect(parser.errors[0].message).to.include(
112
- 'Must be one of: "merge", "replace"'
139
+ 'Must be one of: "merge", "replace"',
113
140
  );
114
141
  });
115
142
  });
@@ -179,7 +206,7 @@ describe("middleware_parser", () => {
179
206
  expect(parser.errors).to.not.be.empty;
180
207
  expect(parser.errors[0].message).to.include('Invalid value "invalid"');
181
208
  expect(parser.errors[0].message).to.include(
182
- 'Must be one of: "silent", "rethrow", "critical"'
209
+ 'Must be one of: "silent", "rethrow", "critical"',
183
210
  );
184
211
  });
185
212
  });
package/parser/parser.js CHANGED
@@ -15,6 +15,7 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
15
15
  scheme = getSchemeFromContent(text);
16
16
  }
17
17
  const lexResult = preTokenized || lexDocument(text);
18
+ parser.reset();
18
19
  parser.input = lexResult.tokens;
19
20
  switch (scheme.toLowerCase()) {
20
21
  case "addon":
@@ -151,4 +151,25 @@ describe("query_parser", () => {
151
151
  }`);
152
152
  expect(parser.errors).to.be.empty;
153
153
  });
154
+
155
+ it("should not allow api.call within a group in a query stack", () => {
156
+ const parser = xanoscriptParser(`query foo verb=GET {
157
+ input {
158
+ }
159
+
160
+ stack {
161
+ group {
162
+ stack {
163
+ api.call bar verb=POST {
164
+ api_group = "internal"
165
+ input = { x: 1 }
166
+ } as $result
167
+ }
168
+ }
169
+ }
170
+
171
+ response = null
172
+ }`);
173
+ expect(parser.errors).to.not.be.empty;
174
+ });
154
175
  });
@@ -45,9 +45,7 @@ export function workflowTestDeclaration($) {
45
45
  ALT: () => {
46
46
  hasStack = true;
47
47
  $.SUBRULE($.stackClause, {
48
- ARGS: [
49
- { allowExpectStatements: true, allowCallStatements: true },
50
- ],
48
+ ARGS: [{ allowCallStatements: true }],
51
49
  });
52
50
  },
53
51
  },
@@ -39,8 +39,10 @@ describe("workflow_test_parser", () => {
39
39
  input = {a: 5, b: 10}
40
40
  } as $sums
41
41
 
42
- expect.to_equal ($sum) {
43
- value = 15
42
+ expect.to_equal ($sum
43
+ |subtract:5
44
+ ) {
45
+ value = 10
44
46
  }
45
47
  }
46
48
  }`);
@@ -68,4 +70,68 @@ describe("workflow_test_parser", () => {
68
70
  }`);
69
71
  expect(parser.errors).to.be.empty;
70
72
  });
73
+
74
+ it("should parse a workflow test with api.call within a group", () => {
75
+ const parser = xanoscriptParser(`workflow_test foo {
76
+ stack {
77
+ group {
78
+ stack {
79
+ api.call "endpoint/{id}" verb=PATCH {
80
+ api_group = "external"
81
+ input = {
82
+ data: {action: "update"}
83
+ id: 123
84
+ }
85
+ } as $result
86
+ }
87
+ }
88
+ }
89
+ }`);
90
+ expect(parser.errors).to.be.empty;
91
+ });
92
+
93
+ it("should parse a workflow test with expect.to_equal within a group", () => {
94
+ const parser = xanoscriptParser(`workflow_test foo {
95
+ stack {
96
+ var $a {
97
+ value = 42
98
+ }
99
+
100
+ function.run woot {
101
+ input = {scope: 12}
102
+ } as $my_val
103
+
104
+ group {
105
+ stack {
106
+ function.call "add" {
107
+ input = {a: 20, b: 22}
108
+ } as $sum
109
+
110
+ expect.to_equal ($a) {
111
+ value = 42
112
+ }
113
+ }
114
+ }
115
+
116
+ group {
117
+ stack {
118
+ function.call "add" {
119
+ input = {a: 20, b: 22}
120
+ } as $sum
121
+
122
+ api.call "users/{user_id}" verb=GET {
123
+ api_group = "account"
124
+ input = { user_id: 123 }
125
+ } as $user
126
+
127
+ expect.to_equal ($a) {
128
+ value = 42
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }`);
134
+ console.log(parser.errors);
135
+ expect(parser.errors).to.be.empty;
136
+ });
71
137
  });