@xano/xanoscript-language-server 11.7.0 → 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.
@@ -10,7 +10,9 @@
10
10
  "Bash(grep:*)",
11
11
  "Bash(node --input-type=module:*)",
12
12
  "Bash(find:*)",
13
- "Read(//tmp/**)"
13
+ "Read(//tmp/**)",
14
+ "Bash(git stash:*)",
15
+ "Bash(node:*)"
14
16
  ]
15
17
  }
16
18
  }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.7.0",
3
+ "version": "11.7.1",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -100,10 +100,14 @@ describe("function_parser", () => {
100
100
  }
101
101
 
102
102
  stack {
103
- api.call "users/{user_id}" verb=GET {
104
- api_group = "account"
105
- input = { user_id: 123 }
106
- } as $user
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
+ }
107
111
  }
108
112
 
109
113
  response = null
@@ -111,4 +115,3 @@ describe("function_parser", () => {
111
115
  expect(parser.errors).to.not.be.empty;
112
116
  });
113
117
  });
114
-
@@ -20,15 +20,19 @@ export function apiCallFn($) {
20
20
  const fnToken = $.CONSUME(CallToken); // "call"
21
21
 
22
22
  // Validate that api.call is only used in allowed contexts
23
- const validContexts = ["workflowTestDeclaration", "toolDeclaration", "testClause"];
23
+ const validContexts = [
24
+ "workflowTestDeclaration",
25
+ "toolDeclaration",
26
+ "testClause",
27
+ ];
24
28
  const isInValidContext = validContexts.some((context) =>
25
- $.sectionStack.includes(context)
29
+ $.sectionStack.includes(context),
26
30
  );
27
31
 
28
32
  if (!isInValidContext) {
29
33
  $.addInvalidValueError(
30
34
  fnToken,
31
- "api.call can only be used within workflow_test, tool, or test contexts"
35
+ "api.call can only be used within workflow_test, tool, or test contexts",
32
36
  );
33
37
  }
34
38
 
@@ -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
 
@@ -20,6 +20,13 @@ describe("unitExpectWithArgumentsFn", () => {
20
20
  expect(parser.errors).to.be.empty;
21
21
  });
22
22
 
23
+ it("unitExpectWithArgumentsFn to_equal accepts a $response subpath with $ prefix", () => {
24
+ const parser = parse(`to_equal ($response.$xyz) {
25
+ value = "foo"
26
+ }`);
27
+ expect(parser.errors).to.be.empty;
28
+ });
29
+
23
30
  it("unitExpectWithArgumentsFn does not accept a non response variable", () => {
24
31
  const parser = parse(`to_equal ($foo) {
25
32
  value = 12
@@ -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, {
@@ -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
 
@@ -133,7 +133,7 @@ describe("schemaParseAttributeFn", () => {
133
133
  expect(parser.errors).to.be.empty;
134
134
 
135
135
  parse(
136
- `value = { something = 123.2, other_thing = 44 }`
136
+ `value = { something = 123.2, other_thing = 44 }`,
137
137
  ).schemaParseAttributeFn_flat();
138
138
  expect(parser.errors).to.be.empty;
139
139
  });
@@ -12,13 +12,13 @@ export function workflowExpectFn($) {
12
12
  // Validate that expect statements are only used in allowed contexts
13
13
  const validContexts = ["workflowTestDeclaration", "testClause"];
14
14
  const isInValidContext = validContexts.some((context) =>
15
- $.sectionStack.includes(context)
15
+ $.sectionStack.includes(context),
16
16
  );
17
17
 
18
18
  if (!isInValidContext) {
19
19
  $.addInvalidValueError(
20
20
  expectToken,
21
- "expect statements can only be used within workflow_test or test contexts"
21
+ "expect statements can only be used within workflow_test or test contexts",
22
22
  );
23
23
  }
24
24
 
@@ -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
  },
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":
@@ -45,9 +45,7 @@ export function workflowTestDeclaration($) {
45
45
  ALT: () => {
46
46
  hasStack = true;
47
47
  $.SUBRULE($.stackClause, {
48
- ARGS: [
49
- { 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
  }`);
@@ -95,6 +97,10 @@ describe("workflow_test_parser", () => {
95
97
  value = 42
96
98
  }
97
99
 
100
+ function.run woot {
101
+ input = {scope: 12}
102
+ } as $my_val
103
+
98
104
  group {
99
105
  stack {
100
106
  function.call "add" {
@@ -113,6 +119,11 @@ describe("workflow_test_parser", () => {
113
119
  input = {a: 20, b: 22}
114
120
  } as $sum
115
121
 
122
+ api.call "users/{user_id}" verb=GET {
123
+ api_group = "account"
124
+ input = { user_id: 123 }
125
+ } as $user
126
+
116
127
  expect.to_equal ($a) {
117
128
  value = 42
118
129
  }