@xano/xanoscript-language-server 11.12.0 → 11.12.2

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/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/regexp.spec.js +84 -0
  7. package/lexer/security.js +2 -2
  8. package/lexer/tokens.js +22 -3
  9. package/package.json +1 -1
  10. package/parser/attributes/columnDefaultValueAttribute.js +10 -3
  11. package/parser/attributes/inputFilterFn.js +11 -4
  12. package/parser/clauses/flexibleStringClause.js +2 -2
  13. package/parser/clauses/nakedStackFn.js +43 -36
  14. package/parser/clauses/nakedStackFn.spec.js +10 -0
  15. package/parser/clauses/responseClause.spec.js +5 -0
  16. package/parser/definitions/columnDefinition.js +7 -2
  17. package/parser/functions/array/arrayNoValueAs.js +1 -1
  18. package/parser/functions/array/arrayPopFn.js +1 -3
  19. package/parser/functions/controls/forEachFn.spec.js +8 -0
  20. package/parser/functions/controls/functionRunFn.spec.js +10 -0
  21. package/parser/functions/controls/preconditionFn.js +3 -0
  22. package/parser/functions/db/dbExternalDirectQueryFn.js +1 -0
  23. package/parser/functions/db/dbQueryFn.spec.js +31 -0
  24. package/parser/functions/schema/schemaParseConstantFn.js +11 -10
  25. package/parser/functions/varFn.js +14 -2
  26. package/parser/functions/varFn.spec.js +20 -12
  27. package/parser/generic/asVariable.spec.js +12 -8
  28. package/parser/generic/assignableVariableAs.js +50 -11
  29. package/parser/generic/assignableVariableAs.spec.js +29 -7
  30. package/parser/generic/assignableVariableProperty.js +22 -10
  31. package/parser/generic/assignableVariableProperty.spec.js +21 -12
  32. package/parser/generic/expressionFn.js +9 -2
  33. package/parser/generic/expressionFn.spec.js +5 -0
  34. package/parser/generic/filterNames.js +9 -0
  35. package/parser/multidoc.js +38 -17
  36. package/parser/multidoc.spec.js +7 -1
  37. package/parser/query_parser.js +11 -3
  38. package/parser/query_parser.spec.js +0 -3
  39. package/parser/tests/function/function_parser.spec.js +4 -4
  40. package/parser/tests/query/query_parser.spec.js +3 -3
  41. package/parser/tool_parser.js +12 -3
  42. package/parser/tool_parser.spec.js +19 -6
@@ -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);
@@ -299,6 +299,10 @@ function bar {
299
299
  var.update $x {
300
300
  value = 2
301
301
  }
302
+
303
+ var $y {
304
+ value = 2
305
+ }
302
306
  }
303
307
 
304
308
  response = null
@@ -691,7 +695,9 @@ function bar {
691
695
  // The error must be within seg3's global range
692
696
  const error = result.errors[0];
693
697
  expect(error.token.startOffset).to.be.at.least(expectedSeg3Start);
694
- expect(error.token.startOffset).to.be.lessThan(expectedSeg3Start + seg3.length);
698
+ expect(error.token.startOffset).to.be.lessThan(
699
+ expectedSeg3Start + seg3.length,
700
+ );
695
701
  });
696
702
 
697
703
  it("should isolate errors — bad seg1 should not prevent seg2 from parsing", () => {
@@ -30,6 +30,7 @@ export function queryDeclaration($) {
30
30
  let hasTags = false;
31
31
  let hasApiGroup = false;
32
32
  let hasResponseType = false;
33
+ let responseType = "standard";
33
34
 
34
35
  const testNames = [];
35
36
 
@@ -80,8 +81,8 @@ export function queryDeclaration($) {
80
81
  $.CONSUME(ResponseTypeToken);
81
82
  $.CONSUME8(EqualToken);
82
83
  const valueToken = $.CONSUME2(StringLiteral);
83
- const value = getVarName(valueToken);
84
- if (!["stream", "standard"].includes(value)) {
84
+ responseType = getVarName(valueToken);
85
+ if (!["stream", "standard"].includes(responseType)) {
85
86
  $.addInvalidValueError(
86
87
  valueToken,
87
88
  "response_type must be either 'stream' or 'standard'",
@@ -193,10 +194,17 @@ export function queryDeclaration($) {
193
194
  $.addMissingError(parent, "{} is missing a stack clause");
194
195
  }
195
196
 
196
- if (!hasResponse) {
197
+ if (!hasResponse && responseType === "standard") {
197
198
  $.addMissingError(parent, "{} is missing a response clause");
198
199
  }
199
200
 
201
+ if (hasResponse && responseType === "stream") {
202
+ $.addInvalidValueError(
203
+ parent,
204
+ "response clause is not allowed when response_type is 'stream'",
205
+ );
206
+ }
207
+
200
208
  $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
201
209
  $.CONSUME(RCurly); // "}"
202
210
  $.MANY2(() => $.CONSUME2(NewlineToken)); // optional new line
@@ -74,7 +74,6 @@ describe("query_parser", () => {
74
74
  expect(parser.errors).to.be.empty;
75
75
 
76
76
  parser = xanoscriptParser(`query foo verb=GET {
77
-
78
77
  response_type = "stream"
79
78
 
80
79
  input {
@@ -82,8 +81,6 @@ describe("query_parser", () => {
82
81
 
83
82
  stack {
84
83
  }
85
-
86
- response = null
87
84
  }`);
88
85
  expect(parser.errors).to.be.empty;
89
86
  });
@@ -9,12 +9,12 @@ describe("Xanoscript Function Parser", () => {
9
9
  it("parses all the valid sources", () => {
10
10
  const errors = [];
11
11
  const files = readdirSync(
12
- join(dirname("./"), "parser/tests/function/valid_sources")
12
+ join(dirname("./"), "parser/tests/function/valid_sources"),
13
13
  );
14
14
  files.forEach((file) => {
15
15
  const source = readFileSync(
16
16
  join(dirname("./"), "parser/tests/function/valid_sources", file),
17
- "utf8"
17
+ "utf8",
18
18
  );
19
19
  const parser = xanoscriptParser(source);
20
20
 
@@ -26,7 +26,7 @@ describe("Xanoscript Function Parser", () => {
26
26
  error.previousToken.image,
27
27
  {
28
28
  length: 20,
29
- }
29
+ },
30
30
  )})`
31
31
  : "N/A",
32
32
  file: `parser/tests/function/valid_sources/${file}:${error.token.startLine}:${error.token.startColumn}`,
@@ -36,7 +36,7 @@ describe("Xanoscript Function Parser", () => {
36
36
  error.token.image,
37
37
  {
38
38
  length: 20,
39
- }
39
+ },
40
40
  )})`,
41
41
  });
42
42
  });
@@ -9,13 +9,13 @@ describe("Xanoscript Query Parser", () => {
9
9
  it("parses all the valid sources", () => {
10
10
  const errors = [];
11
11
  const files = readdirSync(
12
- join(dirname("./"), "parser/tests/query/valid_sources")
12
+ join(dirname("./"), "parser/tests/query/valid_sources"),
13
13
  );
14
14
 
15
15
  files.forEach((file) => {
16
16
  const source = readFileSync(
17
17
  join(dirname("./"), "parser/tests/query/valid_sources", file),
18
- "utf8"
18
+ "utf8",
19
19
  );
20
20
  const parser = xanoscriptParser(source);
21
21
 
@@ -27,7 +27,7 @@ describe("Xanoscript Query Parser", () => {
27
27
  error.previousToken.image,
28
28
  {
29
29
  length: 20,
30
- }
30
+ },
31
31
  )})`
32
32
  : "N/A",
33
33
  file: error.token
@@ -111,15 +111,24 @@ export function toolDeclaration($) {
111
111
  });
112
112
 
113
113
  if (!hasInput) {
114
- $.addMissingError(parent, "{} is missing an input clause");
114
+ $.addMissingError(
115
+ parent,
116
+ `{} ${nameToken.image} is missing an input clause`,
117
+ );
115
118
  }
116
119
 
117
120
  if (!hasStack) {
118
- $.addMissingError(parent, "{} is missing a stack clause");
121
+ $.addMissingError(
122
+ parent,
123
+ `{} ${nameToken.image} is missing a stack clause`,
124
+ );
119
125
  }
120
126
 
121
127
  if (!hasResponse) {
122
- $.addMissingError(parent, "{} is missing a response clause");
128
+ $.addMissingError(
129
+ parent,
130
+ `{} ${nameToken.image} is missing a response clause`,
131
+ );
123
132
  }
124
133
 
125
134
  $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
@@ -19,15 +19,28 @@ describe("tool_parser", () => {
19
19
 
20
20
  it("should parse a basic a string tool url", () => {
21
21
  const parser = xanoscriptParser(`tool "foo/bar" {
22
- input {
23
- }
22
+ input {
23
+ }
24
24
 
25
- stack {
26
- }
25
+ stack {
26
+ }
27
27
 
28
- response = null
29
- }`);
28
+ response = null
29
+ }`);
30
+
31
+ expect(parser.errors).to.be.empty;
32
+ });
33
+
34
+ it("should parse a tool with a string as a response", () => {
35
+ const parser = xanoscriptParser(`tool silent_exception {
36
+ input {
37
+ }
38
+
39
+ stack {
40
+ }
30
41
 
42
+ response = "something is wrong"
43
+ }`);
31
44
  expect(parser.errors).to.be.empty;
32
45
  });
33
46