@xano/xanoscript-language-server 11.8.4 → 11.8.5

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 (56) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/cache/documentCache.js +58 -10
  3. package/lexer/db.js +1 -2
  4. package/lexer/security.js +16 -0
  5. package/onCompletion/onCompletion.js +61 -1
  6. package/onDefinition/onDefinition.js +150 -0
  7. package/onDefinition/onDefinition.spec.js +313 -0
  8. package/onDidChangeContent/onDidChangeContent.js +52 -5
  9. package/onHover/functions.md +28 -0
  10. package/package.json +1 -1
  11. package/parser/base_parser.js +61 -3
  12. package/parser/clauses/middlewareClause.js +16 -0
  13. package/parser/definitions/columnDefinition.js +5 -0
  14. package/parser/functions/api/apiCallFn.js +5 -3
  15. package/parser/functions/controls/functionCallFn.js +5 -3
  16. package/parser/functions/controls/functionRunFn.js +61 -5
  17. package/parser/functions/controls/taskCallFn.js +5 -3
  18. package/parser/functions/db/captureFieldName.js +63 -0
  19. package/parser/functions/db/dbAddFn.js +5 -3
  20. package/parser/functions/db/dbAddOrEditFn.js +13 -3
  21. package/parser/functions/db/dbBulkAddFn.js +5 -3
  22. package/parser/functions/db/dbBulkDeleteFn.js +5 -3
  23. package/parser/functions/db/dbBulkPatchFn.js +5 -3
  24. package/parser/functions/db/dbBulkUpdateFn.js +5 -3
  25. package/parser/functions/db/dbDelFn.js +10 -3
  26. package/parser/functions/db/dbEditFn.js +13 -3
  27. package/parser/functions/db/dbGetFn.js +10 -3
  28. package/parser/functions/db/dbHasFn.js +9 -3
  29. package/parser/functions/db/dbPatchFn.js +10 -3
  30. package/parser/functions/db/dbQueryFn.js +29 -3
  31. package/parser/functions/db/dbSchemaFn.js +5 -3
  32. package/parser/functions/db/dbTruncateFn.js +5 -3
  33. package/parser/functions/middlewareCallFn.js +3 -1
  34. package/parser/functions/security/register.js +19 -9
  35. package/parser/functions/security/securityCreateAuthTokenFn.js +22 -0
  36. package/parser/functions/security/securityJweDecodeLegacyFn.js +24 -0
  37. package/parser/functions/security/securityJweDecodeLegacyFn.spec.js +26 -0
  38. package/parser/functions/security/securityJweEncodeLegacyFn.js +24 -0
  39. package/parser/functions/security/securityJweEncodeLegacyFn.spec.js +25 -0
  40. package/parser/functions/securityFn.js +2 -0
  41. package/parser/functions/varFn.js +1 -1
  42. package/parser/generic/asVariable.js +2 -0
  43. package/parser/generic/assignableVariableAs.js +1 -0
  44. package/parser/generic/assignableVariableProperty.js +5 -2
  45. package/parser/tests/variable_test/coverage_check.xs +293 -0
  46. package/parser/variableScanner.js +64 -0
  47. package/parser/variableValidator.js +44 -0
  48. package/parser/variableValidator.spec.js +179 -0
  49. package/server.js +164 -10
  50. package/utils.js +32 -0
  51. package/utils.spec.js +93 -1
  52. package/workspace/crossFileValidator.js +166 -0
  53. package/workspace/crossFileValidator.spec.js +654 -0
  54. package/workspace/referenceTracking.spec.js +420 -0
  55. package/workspace/workspaceIndex.js +149 -0
  56. package/workspace/workspaceIndex.spec.js +189 -0
@@ -0,0 +1,293 @@
1
+ // Valide a couverture pour un assuré
2
+ query "coverage/check" verb=GET {
3
+ api_group = "coverage"
4
+
5
+ input {
6
+ // VIN (17 caractères)
7
+ text vin filters=trim|upper
8
+
9
+ // Code garage (optionnel)
10
+ text shop_code? filters=trim|upper
11
+ }
12
+
13
+ stack {
14
+ // Recherche et validation du véhicule
15
+ group {
16
+ stack {
17
+ // Trouve le vehicle a partir du VIN
18
+ db.query vehicle {
19
+ where = $db.vehicle.vin == $input.vin
20
+ return = {type: "single"}
21
+ mock = {
22
+ "devrait retourner véhicule sans police" : {id: 2, vin: "2HGCM82633A004353", make: "Toyota", model: "Camry", year: 2021, owner_name: "Marie Martin"}
23
+ "devrait échouer si VIN introuvable" : null
24
+ "devrait avoir coverages vide si pas de police": {id: 2, vin: "2HGCM82633A004353", make: "Toyota", model: "Camry", year: 2021, owner_name: "Marie Martin"}
25
+ }
26
+ } as $vehicle
27
+
28
+ // Véhicule requis
29
+ precondition ($vehicle != null) {
30
+ error_type = "notfound"
31
+ error = "Vehicle not found with VIN: " ~ $input.vin
32
+ }
33
+
34
+
35
+ var $unused_variable {
36
+ value = "This variable is not used anywhere in the code"
37
+ }
38
+
39
+ // Réponse véhicule
40
+ var $vehicle_response {
41
+ value = {
42
+ vin : $vehicle.vin
43
+ make : $vehicle.make
44
+ model : $vehicle.model
45
+ year : $vehicle.year
46
+ owner_name: $vehicle.owner_name
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ // Recherche de la police d'assurance active
53
+ group {
54
+ stack {
55
+ // Police active du véhicule
56
+ db.query insurance_policy {
57
+ where = $db.insurance_policy.vehicle_id == $vehicle.id && $db.insurance_policy.status == "active"
58
+ return = {type: "single"}
59
+ mock = {
60
+ "devrait retourner véhicule sans police" : null
61
+ "devrait avoir coverages vide si pas de police": null
62
+ }
63
+ } as $policy
64
+
65
+ // Init réponses
66
+ var $policy_response {
67
+ value = null
68
+ }
69
+
70
+ var $coverages_response {
71
+ value = []
72
+ }
73
+
74
+ var $claims_summary {
75
+ value = {total_claims: 0, recent_claims: []}
76
+ }
77
+ }
78
+ }
79
+
80
+ // Traitement si police active
81
+ conditional {
82
+ if ($policy != null) {
83
+ // Construction de la réponse police
84
+ group {
85
+ stack {
86
+ // Réponse police
87
+ var.update $policy_response {
88
+ value = {
89
+ policy_number : $policy.policy_number
90
+ status : $policy.status
91
+ effective_date : $policy.effective_date
92
+ expiration_date: $policy.expiration_date
93
+ insurer_name : $policy.insurer_name
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // Récupération des couvertures
100
+ group {
101
+ stack {
102
+ db.query policy_coverage {
103
+ where = $db.policy_coverage.policy_id == $policy.id
104
+ return = {type: "list"}
105
+ } as $policy_coverages
106
+
107
+ // Requête par couverture
108
+ foreach ($policy_coverages) {
109
+ each as $pc {
110
+ db.get coverage_type {
111
+ field_name = "id"
112
+ field_value = $pc.coverage_type_id
113
+ } as $ctype
114
+
115
+ // Objet couverture
116
+ var $coverage_item {
117
+ value = {
118
+ code : $ctype.code
119
+ name : $ctype.name
120
+ coverage_limit: $pc.coverage_limit
121
+ deductible : $pc.deductible
122
+ is_active : $pc.is_active
123
+ }
124
+ }
125
+
126
+ // Ajout au tableau
127
+ array.push $coverages_response {
128
+ value = $coverage_item
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Récupération et filtrage des sinistres
136
+ group {
137
+ stack {
138
+ // Récupère TOUS les sinistres puis filtre en code
139
+ db.query claim {
140
+ where = $db.claim.policy_id == $policy.id
141
+ return = {type: "list"}
142
+ } as $all_claims
143
+
144
+ // Total sinistres
145
+ var $total_claims_count {
146
+ value = $all_claims|count
147
+ }
148
+
149
+ // Date il y a 2 ans
150
+ var $two_years_ago {
151
+ value = now|transform_timestamp:"-2 years"
152
+ }
153
+
154
+ // Filtrage en code (au lieu de SQL)
155
+ var $recent_claims_list {
156
+ value = []
157
+ }
158
+
159
+ foreach ($all_claims) {
160
+ each as $claim {
161
+ // Date en timestamp
162
+ var $claim_date_ts {
163
+ value = $claim.incident_date|to_timestamp
164
+ }
165
+
166
+ // ⚠️ Filtre en code - devrait être en SQL
167
+ conditional {
168
+ if ($claim_date_ts >= $two_years_ago) {
169
+ var $recent_claim_item {
170
+ value = {
171
+ claim_number : $claim.claim_number
172
+ incident_date : $claim.incident_date
173
+ status : $claim.status
174
+ amount_claimed: $claim.amount_claimed
175
+ amount_paid : $claim.amount_paid
176
+ }
177
+ }
178
+
179
+ array.push $recent_claims_list {
180
+ value = $recent_claim_item
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ // Résumé sinistres
188
+ var.update $claims_summary {
189
+ value = {
190
+ total_claims : $total_claims_count
191
+ recent_claims: $recent_claims_list
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // Recherche du garage si code fourni
200
+ group {
201
+ stack {
202
+ var $queried_by {
203
+ value = null
204
+ }
205
+
206
+ conditional {
207
+ if ($input.shop_code != null && ($input.shop_code|strlen) > 0) {
208
+ // Récupère garage
209
+ db.query repair_shop {
210
+ where = $db.repair_shop.shop_code == $input.shop_code
211
+ return = {type: "single"}
212
+ } as $shop
213
+
214
+ // Si garage trouvé
215
+ conditional {
216
+ if ($shop != null) {
217
+ var.update $queried_by {
218
+ value = {shop_code: $shop.shop_code, shop_name: $shop.name}
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ // Construction de la réponse finale
228
+ group {
229
+ stack {
230
+ var $result {
231
+ value = {
232
+ vehicle : $vehicle_response
233
+ policy : $policy_response
234
+ coverages : $coverages_response
235
+ claims_summary: $claims_summary
236
+ }
237
+ }
238
+
239
+ // Ajoute queried_by si présent
240
+ conditional {
241
+ if ($queried_by != null) {
242
+ var.update $result {
243
+ value = $result|set:"queried_by":$queried_by
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ response = $result
252
+ history = 100
253
+
254
+ test "devrait retourner véhicule sans police" {
255
+ input = {vin: "2HGCM82633A004353"}
256
+
257
+ expect.to_equal ($response.vehicle.vin) {
258
+ value = "2HGCM82633A004353"
259
+ }
260
+
261
+ expect.to_equal ($response.vehicle.make) {
262
+ value = "Toyota"
263
+ }
264
+
265
+ expect.to_be_null ($response.policy)
266
+ expect.to_be_empty ($response.coverages)
267
+ expect.to_equal ($response.claims_summary.total_claims) {
268
+ value = 0
269
+ }
270
+ }
271
+
272
+ test "devrait échouer si VIN introuvable" {
273
+ input = {vin: "INVALID12345678901"}
274
+
275
+ expect.to_throw {
276
+ exception = "ERROR_CODE_NOT_FOUND"
277
+ }
278
+ }
279
+
280
+ test "devrait avoir coverages vide si pas de police" {
281
+ input = {vin: "2HGCM82633A004353"}
282
+
283
+ expect.to_be_empty ($response.coverages)
284
+ expect.to_be_null ($response.policy)
285
+ expect.to_equal ($response.claims_summary.total_claims) {
286
+ value = 0
287
+ }
288
+
289
+ expect.to_be_empty ($response.claims_summary.recent_claims)
290
+ }
291
+
292
+ guid = "n20O4mlUR72ZWVYiGAE-QFk_ZO4"
293
+ }
@@ -0,0 +1,64 @@
1
+ import { LongFormVariable,ShortFormVariable } from "../lexer/variables.js";
2
+ import { BLACKLISTED_VARIABLE_NAMES } from "./generic/assignableVariableProperty.js";
3
+
4
+ const SHORT_FORM = ShortFormVariable.name;
5
+ const LONG_FORM = LongFormVariable.name;
6
+
7
+ const BUILTIN_NAMES = new Set(BLACKLISTED_VARIABLE_NAMES.map((n) => `$${n}`));
8
+ // Also skip $$ (temporary variable)
9
+ BUILTIN_NAMES.add("$$");
10
+
11
+ /**
12
+ * Scan the token stream for variable usages (ShortFormVariable and LongFormVariable tokens).
13
+ * Excludes tokens at declaration offsets and built-in variable names.
14
+ * @param {import('chevrotain').IToken[]} tokens - The token stream
15
+ * @param {Array<{name: string, startOffset: number | null}>} declarations - Known declaration sites
16
+ * @returns {Array<{name: string, startOffset: number, endOffset: number}>}
17
+ */
18
+ export function scanVariableUsages(tokens, declarations) {
19
+ // Build set of declaration offsets to skip
20
+ const declOffsets = new Set();
21
+ for (const decl of declarations) {
22
+ if (decl.startOffset != null) {
23
+ declOffsets.add(decl.startOffset);
24
+ }
25
+ }
26
+
27
+ const usages = [];
28
+
29
+ for (let i = 0; i < tokens.length; i++) {
30
+ const token = tokens[i];
31
+ const typeName = token.tokenType.name;
32
+
33
+ if (typeName === SHORT_FORM) {
34
+ // Skip if this is a declaration site
35
+ if (declOffsets.has(token.startOffset)) continue;
36
+ // Skip built-in variable names
37
+ if (BUILTIN_NAMES.has(token.image)) continue;
38
+
39
+ usages.push({
40
+ name: token.image,
41
+ startOffset: token.startOffset,
42
+ endOffset: token.endOffset,
43
+ });
44
+ } else if (typeName === LONG_FORM) {
45
+ // $var.foo → resolve to $foo
46
+ // Next tokens should be Dot + Identifier
47
+ if (i + 2 < tokens.length) {
48
+ const identToken = tokens[i + 2];
49
+ const name = `$${identToken.image}`;
50
+
51
+ // Skip if this is a declaration site (check the identifier token)
52
+ if (declOffsets.has(identToken.startOffset)) continue;
53
+
54
+ usages.push({
55
+ name,
56
+ startOffset: token.startOffset,
57
+ endOffset: identToken.endOffset,
58
+ });
59
+ }
60
+ }
61
+ }
62
+
63
+ return usages;
64
+ }
@@ -0,0 +1,44 @@
1
+ import { scanVariableUsages } from "./variableScanner.js";
2
+
3
+ /**
4
+ * Validate variable declarations vs usages.
5
+ * Returns undefined-variable warnings and unused-variable hints.
6
+ * @param {Object} symbolTable - The parser's __symbolTable
7
+ * @param {import('chevrotain').IToken[]} tokens - The token stream from parser.input
8
+ * @returns {{ warnings: Array<{message: string, startOffset: number, endOffset: number}>, hints: Array<{message: string, startOffset: number, endOffset: number}> }}
9
+ */
10
+ export function validateVariables(symbolTable, tokens) {
11
+ const warnings = [];
12
+ const hints = [];
13
+
14
+ const declarations = symbolTable.varDeclarations || [];
15
+ const declaredNames = new Set(declarations.map((d) => d.name));
16
+
17
+ const usages = scanVariableUsages(tokens, declarations);
18
+ const usedNames = new Set(usages.map((u) => u.name));
19
+
20
+ // Undefined: used but not declared
21
+ for (const usage of usages) {
22
+ if (!declaredNames.has(usage.name)) {
23
+ warnings.push({
24
+ message: `Unknown variable '${usage.name}'`,
25
+ startOffset: usage.startOffset,
26
+ endOffset: usage.endOffset,
27
+ });
28
+ }
29
+ }
30
+
31
+ // Unused: declared but never used
32
+ for (const decl of declarations) {
33
+ if (decl.startOffset == null) continue;
34
+ if (!usedNames.has(decl.name)) {
35
+ hints.push({
36
+ message: `Variable '${decl.name}' is declared but never used`,
37
+ startOffset: decl.startOffset,
38
+ endOffset: decl.endOffset,
39
+ });
40
+ }
41
+ }
42
+
43
+ return { warnings, hints };
44
+ }
@@ -0,0 +1,179 @@
1
+ import { expect } from "chai";
2
+ import { readFileSync } from "fs";
3
+ import { before, describe, it } from "mocha";
4
+ import { lexDocument } from "../lexer/lexer.js";
5
+ import { xanoscriptParser } from "./parser.js";
6
+ import { scanVariableUsages } from "./variableScanner.js";
7
+ import { validateVariables } from "./variableValidator.js";
8
+
9
+ describe("variable tracking - coverage_check.xs", () => {
10
+ let varDeclarations;
11
+ let usages;
12
+ let result;
13
+ let text;
14
+
15
+ before(() => {
16
+ text = readFileSync(
17
+ new URL("./tests/variable_test/coverage_check.xs", import.meta.url),
18
+ "utf8",
19
+ );
20
+ const lexResult = lexDocument(text);
21
+ const parser = xanoscriptParser(text);
22
+ expect(parser.errors).to.be.empty;
23
+ varDeclarations = [...parser.__symbolTable.varDeclarations];
24
+ usages = scanVariableUsages(lexResult.tokens, varDeclarations);
25
+ result = validateVariables(parser.__symbolTable, lexResult.tokens);
26
+ });
27
+
28
+ function lineOf(offset) {
29
+ return text.substring(0, offset).split("\n").length;
30
+ }
31
+
32
+ function declLines(name) {
33
+ return varDeclarations
34
+ .filter((d) => d.name === name)
35
+ .map((d) => lineOf(d.startOffset));
36
+ }
37
+
38
+ function usageLines(name) {
39
+ return usages.filter((u) => u.name === name).map((u) => lineOf(u.startOffset));
40
+ }
41
+
42
+ // --- Declarations: each variable should be declared exactly once ---
43
+
44
+ it("$vehicle declared once at line 26 via 'as $vehicle'", () => {
45
+ expect(declLines("$vehicle")).to.deep.equal([26]);
46
+ });
47
+
48
+ it("$unused_variable declared once at line 35 via 'var'", () => {
49
+ expect(declLines("$unused_variable")).to.deep.equal([35]);
50
+ });
51
+
52
+ it("$vehicle_response declared once at line 40 via 'var'", () => {
53
+ expect(declLines("$vehicle_response")).to.deep.equal([40]);
54
+ });
55
+
56
+ it("$policy declared once at line 63 via 'as $policy'", () => {
57
+ expect(declLines("$policy")).to.deep.equal([63]);
58
+ });
59
+
60
+ it("$policy_response declared once at line 66 via 'var' (NOT again at line 87 via var.update)", () => {
61
+ expect(declLines("$policy_response")).to.deep.equal([66]);
62
+ });
63
+
64
+ it("$coverages_response declared once at line 70 via 'var'", () => {
65
+ expect(declLines("$coverages_response")).to.deep.equal([70]);
66
+ });
67
+
68
+ it("$claims_summary declared once at line 74 via 'var' (NOT again at line 188 via var.update)", () => {
69
+ expect(declLines("$claims_summary")).to.deep.equal([74]);
70
+ });
71
+
72
+ it("$policy_coverages declared once at line 105 via 'as'", () => {
73
+ expect(declLines("$policy_coverages")).to.deep.equal([105]);
74
+ });
75
+
76
+ it("$pc declared once at line 109 via 'each as $pc'", () => {
77
+ expect(declLines("$pc")).to.deep.equal([109]);
78
+ });
79
+
80
+ it("$ctype declared once at line 113 via 'as $ctype'", () => {
81
+ expect(declLines("$ctype")).to.deep.equal([113]);
82
+ });
83
+
84
+ it("$coverage_item declared once at line 116 via 'var'", () => {
85
+ expect(declLines("$coverage_item")).to.deep.equal([116]);
86
+ });
87
+
88
+ it("$all_claims declared once at line 142 via 'as'", () => {
89
+ expect(declLines("$all_claims")).to.deep.equal([142]);
90
+ });
91
+
92
+ it("$total_claims_count declared once at line 145 via 'var'", () => {
93
+ expect(declLines("$total_claims_count")).to.deep.equal([145]);
94
+ });
95
+
96
+ it("$two_years_ago declared once at line 150 via 'var'", () => {
97
+ expect(declLines("$two_years_ago")).to.deep.equal([150]);
98
+ });
99
+
100
+ it("$recent_claims_list declared once at line 155 via 'var'", () => {
101
+ expect(declLines("$recent_claims_list")).to.deep.equal([155]);
102
+ });
103
+
104
+ it("$claim declared once at line 160 via 'each as $claim'", () => {
105
+ expect(declLines("$claim")).to.deep.equal([160]);
106
+ });
107
+
108
+ it("$claim_date_ts declared once at line 162 via 'var'", () => {
109
+ expect(declLines("$claim_date_ts")).to.deep.equal([162]);
110
+ });
111
+
112
+ it("$recent_claim_item declared once at line 169 via 'var'", () => {
113
+ expect(declLines("$recent_claim_item")).to.deep.equal([169]);
114
+ });
115
+
116
+ it("$queried_by declared once at line 202 via 'var' (NOT again at line 217 via var.update)", () => {
117
+ expect(declLines("$queried_by")).to.deep.equal([202]);
118
+ });
119
+
120
+ it("$shop declared once at line 212 via 'as'", () => {
121
+ expect(declLines("$shop")).to.deep.equal([212]);
122
+ });
123
+
124
+ it("$result declared once at line 230 via 'var' (NOT again at line 242 via var.update)", () => {
125
+ expect(declLines("$result")).to.deep.equal([230]);
126
+ });
127
+
128
+ // --- Usages: var.update targets count as usages ---
129
+
130
+ it("$vehicle used at lines 29, 42-46, 57", () => {
131
+ expect(usageLines("$vehicle")).to.deep.equal([29, 42, 43, 44, 45, 46, 57]);
132
+ });
133
+
134
+ it("$policy_response used at line 87 (var.update) and 233", () => {
135
+ expect(usageLines("$policy_response")).to.include(87);
136
+ expect(usageLines("$policy_response")).to.include(233);
137
+ });
138
+
139
+ it("$claims_summary used at line 188 (var.update) and 235", () => {
140
+ expect(usageLines("$claims_summary")).to.include(188);
141
+ expect(usageLines("$claims_summary")).to.include(235);
142
+ });
143
+
144
+ it("$queried_by used at line 217 (var.update), 241, 243", () => {
145
+ expect(usageLines("$queried_by")).to.include(217);
146
+ expect(usageLines("$queried_by")).to.include(241);
147
+ expect(usageLines("$queried_by")).to.include(243);
148
+ });
149
+
150
+ it("$result used at line 242 (var.update), 243, 251", () => {
151
+ expect(usageLines("$result")).to.include(242);
152
+ expect(usageLines("$result")).to.include(243);
153
+ expect(usageLines("$result")).to.include(251);
154
+ });
155
+
156
+ it("$coverages_response used at lines 127 and 234", () => {
157
+ expect(usageLines("$coverages_response")).to.include(127);
158
+ expect(usageLines("$coverages_response")).to.include(234);
159
+ });
160
+
161
+ it("$pc used at lines 112, 120-122", () => {
162
+ expect(usageLines("$pc")).to.deep.equal([112, 120, 121, 122]);
163
+ });
164
+
165
+ it("$claim used at lines 163, 171-175", () => {
166
+ expect(usageLines("$claim")).to.deep.equal([163, 171, 172, 173, 174, 175]);
167
+ });
168
+
169
+ // --- Validation results ---
170
+
171
+ it("should produce zero undefined-variable warnings", () => {
172
+ expect(result.warnings).to.be.empty;
173
+ });
174
+
175
+ it("should flag only $unused_variable as unused", () => {
176
+ expect(result.hints).to.have.lengthOf(1);
177
+ expect(result.hints[0].message).to.include("$unused_variable");
178
+ });
179
+ });