@rcrsr/rill-cli 0.6.0

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/check/config.d.ts +20 -0
  3. package/dist/check/config.d.ts.map +1 -0
  4. package/dist/check/config.js +151 -0
  5. package/dist/check/config.js.map +1 -0
  6. package/dist/check/fixer.d.ts +39 -0
  7. package/dist/check/fixer.d.ts.map +1 -0
  8. package/dist/check/fixer.js +119 -0
  9. package/dist/check/fixer.js.map +1 -0
  10. package/dist/check/index.d.ts +10 -0
  11. package/dist/check/index.d.ts.map +1 -0
  12. package/dist/check/index.js +21 -0
  13. package/dist/check/index.js.map +1 -0
  14. package/dist/check/rules/anti-patterns.d.ts +65 -0
  15. package/dist/check/rules/anti-patterns.d.ts.map +1 -0
  16. package/dist/check/rules/anti-patterns.js +481 -0
  17. package/dist/check/rules/anti-patterns.js.map +1 -0
  18. package/dist/check/rules/closures.d.ts +66 -0
  19. package/dist/check/rules/closures.d.ts.map +1 -0
  20. package/dist/check/rules/closures.js +370 -0
  21. package/dist/check/rules/closures.js.map +1 -0
  22. package/dist/check/rules/collections.d.ts +90 -0
  23. package/dist/check/rules/collections.d.ts.map +1 -0
  24. package/dist/check/rules/collections.js +373 -0
  25. package/dist/check/rules/collections.js.map +1 -0
  26. package/dist/check/rules/conditionals.d.ts +41 -0
  27. package/dist/check/rules/conditionals.d.ts.map +1 -0
  28. package/dist/check/rules/conditionals.js +134 -0
  29. package/dist/check/rules/conditionals.js.map +1 -0
  30. package/dist/check/rules/flow.d.ts +46 -0
  31. package/dist/check/rules/flow.d.ts.map +1 -0
  32. package/dist/check/rules/flow.js +206 -0
  33. package/dist/check/rules/flow.js.map +1 -0
  34. package/dist/check/rules/formatting.d.ts +143 -0
  35. package/dist/check/rules/formatting.d.ts.map +1 -0
  36. package/dist/check/rules/formatting.js +656 -0
  37. package/dist/check/rules/formatting.js.map +1 -0
  38. package/dist/check/rules/helpers.d.ts +26 -0
  39. package/dist/check/rules/helpers.d.ts.map +1 -0
  40. package/dist/check/rules/helpers.js +66 -0
  41. package/dist/check/rules/helpers.js.map +1 -0
  42. package/dist/check/rules/index.d.ts +21 -0
  43. package/dist/check/rules/index.d.ts.map +1 -0
  44. package/dist/check/rules/index.js +78 -0
  45. package/dist/check/rules/index.js.map +1 -0
  46. package/dist/check/rules/loops.d.ts +77 -0
  47. package/dist/check/rules/loops.d.ts.map +1 -0
  48. package/dist/check/rules/loops.js +310 -0
  49. package/dist/check/rules/loops.js.map +1 -0
  50. package/dist/check/rules/naming.d.ts +21 -0
  51. package/dist/check/rules/naming.d.ts.map +1 -0
  52. package/dist/check/rules/naming.js +174 -0
  53. package/dist/check/rules/naming.js.map +1 -0
  54. package/dist/check/rules/strings.d.ts +28 -0
  55. package/dist/check/rules/strings.d.ts.map +1 -0
  56. package/dist/check/rules/strings.js +79 -0
  57. package/dist/check/rules/strings.js.map +1 -0
  58. package/dist/check/rules/types.d.ts +41 -0
  59. package/dist/check/rules/types.d.ts.map +1 -0
  60. package/dist/check/rules/types.js +167 -0
  61. package/dist/check/rules/types.js.map +1 -0
  62. package/dist/check/types.d.ts +112 -0
  63. package/dist/check/types.d.ts.map +1 -0
  64. package/dist/check/types.js +6 -0
  65. package/dist/check/types.js.map +1 -0
  66. package/dist/check/validator.d.ts +18 -0
  67. package/dist/check/validator.d.ts.map +1 -0
  68. package/dist/check/validator.js +110 -0
  69. package/dist/check/validator.js.map +1 -0
  70. package/dist/check/visitor.d.ts +33 -0
  71. package/dist/check/visitor.d.ts.map +1 -0
  72. package/dist/check/visitor.js +259 -0
  73. package/dist/check/visitor.js.map +1 -0
  74. package/dist/cli-check.d.ts +43 -0
  75. package/dist/cli-check.d.ts.map +1 -0
  76. package/dist/cli-check.js +366 -0
  77. package/dist/cli-check.js.map +1 -0
  78. package/dist/cli-error-enrichment.d.ts +73 -0
  79. package/dist/cli-error-enrichment.d.ts.map +1 -0
  80. package/dist/cli-error-enrichment.js +205 -0
  81. package/dist/cli-error-enrichment.js.map +1 -0
  82. package/dist/cli-error-formatter.d.ts +45 -0
  83. package/dist/cli-error-formatter.d.ts.map +1 -0
  84. package/dist/cli-error-formatter.js +218 -0
  85. package/dist/cli-error-formatter.js.map +1 -0
  86. package/dist/cli-eval.d.ts +15 -0
  87. package/dist/cli-eval.d.ts.map +1 -0
  88. package/dist/cli-eval.js +116 -0
  89. package/dist/cli-eval.js.map +1 -0
  90. package/dist/cli-exec.d.ts +58 -0
  91. package/dist/cli-exec.d.ts.map +1 -0
  92. package/dist/cli-exec.js +326 -0
  93. package/dist/cli-exec.js.map +1 -0
  94. package/dist/cli-explain.d.ts +24 -0
  95. package/dist/cli-explain.d.ts.map +1 -0
  96. package/dist/cli-explain.js +68 -0
  97. package/dist/cli-explain.js.map +1 -0
  98. package/dist/cli-lsp-diagnostic.d.ts +35 -0
  99. package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
  100. package/dist/cli-lsp-diagnostic.js +98 -0
  101. package/dist/cli-lsp-diagnostic.js.map +1 -0
  102. package/dist/cli-module-loader.d.ts +19 -0
  103. package/dist/cli-module-loader.d.ts.map +1 -0
  104. package/dist/cli-module-loader.js +83 -0
  105. package/dist/cli-module-loader.js.map +1 -0
  106. package/dist/cli-shared.d.ts +62 -0
  107. package/dist/cli-shared.d.ts.map +1 -0
  108. package/dist/cli-shared.js +158 -0
  109. package/dist/cli-shared.js.map +1 -0
  110. package/dist/cli.d.ts +13 -0
  111. package/dist/cli.d.ts.map +1 -0
  112. package/dist/cli.js +62 -0
  113. package/dist/cli.js.map +1 -0
  114. package/dist/test-internal-import.d.ts +2 -0
  115. package/dist/test-internal-import.d.ts.map +1 -0
  116. package/dist/test-internal-import.js +7 -0
  117. package/dist/test-internal-import.js.map +1 -0
  118. package/package.json +24 -0
  119. package/src/check/config.ts +202 -0
  120. package/src/check/fixer.ts +174 -0
  121. package/src/check/index.ts +39 -0
  122. package/src/check/rules/anti-patterns.ts +585 -0
  123. package/src/check/rules/closures.ts +445 -0
  124. package/src/check/rules/collections.ts +437 -0
  125. package/src/check/rules/conditionals.ts +155 -0
  126. package/src/check/rules/flow.ts +262 -0
  127. package/src/check/rules/formatting.ts +811 -0
  128. package/src/check/rules/helpers.ts +89 -0
  129. package/src/check/rules/index.ts +140 -0
  130. package/src/check/rules/loops.ts +372 -0
  131. package/src/check/rules/naming.ts +242 -0
  132. package/src/check/rules/strings.ts +104 -0
  133. package/src/check/rules/types.ts +214 -0
  134. package/src/check/types.ts +163 -0
  135. package/src/check/validator.ts +136 -0
  136. package/src/check/visitor.ts +338 -0
  137. package/src/cli-check.ts +456 -0
  138. package/src/cli-error-enrichment.ts +274 -0
  139. package/src/cli-error-formatter.ts +313 -0
  140. package/src/cli-eval.ts +145 -0
  141. package/src/cli-exec.ts +408 -0
  142. package/src/cli-explain.ts +76 -0
  143. package/src/cli-lsp-diagnostic.ts +132 -0
  144. package/src/cli-module-loader.ts +101 -0
  145. package/src/cli-shared.ts +187 -0
  146. package/tests/check/cli-check.test.ts +189 -0
  147. package/tests/check/config.test.ts +350 -0
  148. package/tests/check/fixer.test.ts +373 -0
  149. package/tests/check/format-diagnostics.test.ts +327 -0
  150. package/tests/check/rules/anti-patterns.test.ts +467 -0
  151. package/tests/check/rules/closures.test.ts +192 -0
  152. package/tests/check/rules/collections.test.ts +380 -0
  153. package/tests/check/rules/conditionals.test.ts +185 -0
  154. package/tests/check/rules/flow.test.ts +250 -0
  155. package/tests/check/rules/formatting.test.ts +755 -0
  156. package/tests/check/rules/loops.test.ts +334 -0
  157. package/tests/check/rules/naming.test.ts +336 -0
  158. package/tests/check/rules/strings.test.ts +129 -0
  159. package/tests/check/rules/types.test.ts +257 -0
  160. package/tests/check/validator.test.ts +444 -0
  161. package/tests/check/visitor.test.ts +171 -0
  162. package/tests/cli/check.test.ts +801 -0
  163. package/tests/cli/error-enrichment.test.ts +510 -0
  164. package/tests/cli/error-formatter.test.ts +631 -0
  165. package/tests/cli/eval.test.ts +85 -0
  166. package/tests/cli/exec.test.ts +537 -0
  167. package/tests/cli-explain.test.ts +249 -0
  168. package/tests/cli-lsp-diagnostic.test.ts +202 -0
  169. package/tests/cli-shared.test.ts +439 -0
  170. package/tsconfig.json +9 -0
  171. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Validator Tests
3
+ * Verify validateScript orchestrator behavior.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { parse } from '@rcrsr/rill';
8
+ import { validateScript } from '../../src/check/validator.js';
9
+ import type {
10
+ CheckConfig,
11
+ ValidationRule,
12
+ Diagnostic,
13
+ ASTNode,
14
+ ValidationContext,
15
+ } from '../../src/check/types.js';
16
+ import type { NodeType } from '@rcrsr/rill';
17
+
18
+ // ============================================================
19
+ // TEST HELPERS
20
+ // ============================================================
21
+
22
+ /**
23
+ * Create a minimal CheckConfig for testing.
24
+ */
25
+ function createConfig(
26
+ rules: Record<string, 'on' | 'off' | 'warn'> = {},
27
+ severity: Record<string, 'error' | 'warning' | 'info'> = {}
28
+ ): CheckConfig {
29
+ return { rules, severity };
30
+ }
31
+
32
+ /**
33
+ * Create a mock validation rule for testing.
34
+ */
35
+ function createMockRule(
36
+ code: string,
37
+ nodeTypes: NodeType[],
38
+ diagnosticCount: number = 1
39
+ ): ValidationRule {
40
+ return {
41
+ code,
42
+ category: 'naming',
43
+ severity: 'error',
44
+ nodeTypes,
45
+ validate(node: ASTNode, _context: ValidationContext): Diagnostic[] {
46
+ const diagnostics: Diagnostic[] = [];
47
+ for (let i = 0; i < diagnosticCount; i++) {
48
+ diagnostics.push({
49
+ location: node.span.start,
50
+ severity: 'error',
51
+ code,
52
+ message: `Mock diagnostic ${i} from ${code}`,
53
+ context: '',
54
+ fix: null,
55
+ });
56
+ }
57
+ return diagnostics;
58
+ },
59
+ };
60
+ }
61
+
62
+ // ============================================================
63
+ // TESTS
64
+ // ============================================================
65
+
66
+ describe('validateScript', () => {
67
+ describe('orchestration', () => {
68
+ it('returns empty array when no rules registered', () => {
69
+ const source = '"hello"';
70
+ const ast = parse(source);
71
+ const config = createConfig();
72
+
73
+ const diagnostics = validateScript(ast, source, config);
74
+
75
+ expect(diagnostics).toEqual([]);
76
+ });
77
+
78
+ it('returns empty array when all rules disabled', () => {
79
+ const source = '"hello"';
80
+ const ast = parse(source);
81
+ const config = createConfig({ MOCK_RULE: 'off' });
82
+
83
+ const diagnostics = validateScript(ast, source, config);
84
+
85
+ expect(diagnostics).toEqual([]);
86
+ });
87
+
88
+ it('creates validation context with correct structure', () => {
89
+ const source = '"hello"';
90
+ const ast = parse(source);
91
+ const config = createConfig();
92
+
93
+ // Validation context is internal, verify through behavior
94
+ const diagnostics = validateScript(ast, source, config);
95
+
96
+ // Should complete without errors
97
+ expect(Array.isArray(diagnostics)).toBe(true);
98
+ });
99
+ });
100
+
101
+ describe('diagnostic sorting', () => {
102
+ it('sorts diagnostics by line number', () => {
103
+ const source = `"line1"
104
+ "line2"
105
+ "line3"`;
106
+ const ast = parse(source);
107
+ const config = createConfig();
108
+
109
+ // Create mock diagnostics with different line numbers
110
+ const mockDiagnostics: Diagnostic[] = [
111
+ {
112
+ location: { line: 3, column: 1 },
113
+ severity: 'error',
114
+ code: 'TEST',
115
+ message: 'Line 3',
116
+ context: '',
117
+ fix: null,
118
+ },
119
+ {
120
+ location: { line: 1, column: 1 },
121
+ severity: 'error',
122
+ code: 'TEST',
123
+ message: 'Line 1',
124
+ context: '',
125
+ fix: null,
126
+ },
127
+ {
128
+ location: { line: 2, column: 1 },
129
+ severity: 'error',
130
+ code: 'TEST',
131
+ message: 'Line 2',
132
+ context: '',
133
+ fix: null,
134
+ },
135
+ ];
136
+
137
+ // Sort using same logic as validateScript
138
+ const sorted = [...mockDiagnostics].sort((a, b) => {
139
+ if (a.location.line !== b.location.line) {
140
+ return a.location.line - b.location.line;
141
+ }
142
+ return a.location.column - b.location.column;
143
+ });
144
+
145
+ expect(sorted[0].message).toBe('Line 1');
146
+ expect(sorted[1].message).toBe('Line 2');
147
+ expect(sorted[2].message).toBe('Line 3');
148
+ });
149
+
150
+ it('sorts diagnostics by column when lines equal', () => {
151
+ const source = '"hello"';
152
+ const mockDiagnostics: Diagnostic[] = [
153
+ {
154
+ location: { line: 1, column: 5 },
155
+ severity: 'error',
156
+ code: 'TEST',
157
+ message: 'Col 5',
158
+ context: '',
159
+ fix: null,
160
+ },
161
+ {
162
+ location: { line: 1, column: 1 },
163
+ severity: 'error',
164
+ code: 'TEST',
165
+ message: 'Col 1',
166
+ context: '',
167
+ fix: null,
168
+ },
169
+ {
170
+ location: { line: 1, column: 3 },
171
+ severity: 'error',
172
+ code: 'TEST',
173
+ message: 'Col 3',
174
+ context: '',
175
+ fix: null,
176
+ },
177
+ ];
178
+
179
+ const sorted = [...mockDiagnostics].sort((a, b) => {
180
+ if (a.location.line !== b.location.line) {
181
+ return a.location.line - b.location.line;
182
+ }
183
+ return a.location.column - b.location.column;
184
+ });
185
+
186
+ expect(sorted[0].message).toBe('Col 1');
187
+ expect(sorted[1].message).toBe('Col 3');
188
+ expect(sorted[2].message).toBe('Col 5');
189
+ });
190
+
191
+ it('sorts by line first, then column', () => {
192
+ const mockDiagnostics: Diagnostic[] = [
193
+ {
194
+ location: { line: 2, column: 1 },
195
+ severity: 'error',
196
+ code: 'TEST',
197
+ message: 'L2 C1',
198
+ context: '',
199
+ fix: null,
200
+ },
201
+ {
202
+ location: { line: 1, column: 5 },
203
+ severity: 'error',
204
+ code: 'TEST',
205
+ message: 'L1 C5',
206
+ context: '',
207
+ fix: null,
208
+ },
209
+ {
210
+ location: { line: 1, column: 1 },
211
+ severity: 'error',
212
+ code: 'TEST',
213
+ message: 'L1 C1',
214
+ context: '',
215
+ fix: null,
216
+ },
217
+ {
218
+ location: { line: 2, column: 3 },
219
+ severity: 'error',
220
+ code: 'TEST',
221
+ message: 'L2 C3',
222
+ context: '',
223
+ fix: null,
224
+ },
225
+ ];
226
+
227
+ const sorted = [...mockDiagnostics].sort((a, b) => {
228
+ if (a.location.line !== b.location.line) {
229
+ return a.location.line - b.location.line;
230
+ }
231
+ return a.location.column - b.location.column;
232
+ });
233
+
234
+ expect(sorted[0].message).toBe('L1 C1');
235
+ expect(sorted[1].message).toBe('L1 C5');
236
+ expect(sorted[2].message).toBe('L2 C1');
237
+ expect(sorted[3].message).toBe('L2 C3');
238
+ });
239
+
240
+ it('preserves original order for diagnostics at same location', () => {
241
+ const mockDiagnostics: Diagnostic[] = [
242
+ {
243
+ location: { line: 1, column: 1 },
244
+ severity: 'error',
245
+ code: 'RULE_A',
246
+ message: 'First',
247
+ context: '',
248
+ fix: null,
249
+ },
250
+ {
251
+ location: { line: 1, column: 1 },
252
+ severity: 'error',
253
+ code: 'RULE_B',
254
+ message: 'Second',
255
+ context: '',
256
+ fix: null,
257
+ },
258
+ {
259
+ location: { line: 1, column: 1 },
260
+ severity: 'error',
261
+ code: 'RULE_C',
262
+ message: 'Third',
263
+ context: '',
264
+ fix: null,
265
+ },
266
+ ];
267
+
268
+ // JavaScript sort is stable, so order should be preserved
269
+ const sorted = [...mockDiagnostics].sort((a, b) => {
270
+ if (a.location.line !== b.location.line) {
271
+ return a.location.line - b.location.line;
272
+ }
273
+ return a.location.column - b.location.column;
274
+ });
275
+
276
+ expect(sorted[0].code).toBe('RULE_A');
277
+ expect(sorted[1].code).toBe('RULE_B');
278
+ expect(sorted[2].code).toBe('RULE_C');
279
+ });
280
+ });
281
+
282
+ describe('rule enablement', () => {
283
+ it('invokes rules with state "on"', () => {
284
+ const source = '"hello"';
285
+ const ast = parse(source);
286
+
287
+ // Rules would be in VALIDATION_RULES registry
288
+ // This test verifies the enablement logic indirectly
289
+ const config = createConfig({ RULE_A: 'on' });
290
+ const diagnostics = validateScript(ast, source, config);
291
+
292
+ // Currently VALIDATION_RULES is empty, so no diagnostics
293
+ expect(diagnostics).toEqual([]);
294
+ });
295
+
296
+ it('invokes rules with state "warn"', () => {
297
+ const source = '"hello"';
298
+ const ast = parse(source);
299
+ const config = createConfig({ RULE_A: 'warn' });
300
+
301
+ const diagnostics = validateScript(ast, source, config);
302
+
303
+ // Currently VALIDATION_RULES is empty, so no diagnostics
304
+ expect(diagnostics).toEqual([]);
305
+ });
306
+
307
+ it('skips rules with state "off"', () => {
308
+ const source = '"hello"';
309
+ const ast = parse(source);
310
+ const config = createConfig({ RULE_A: 'off' });
311
+
312
+ const diagnostics = validateScript(ast, source, config);
313
+
314
+ expect(diagnostics).toEqual([]);
315
+ });
316
+ });
317
+
318
+ describe('node type filtering', () => {
319
+ it('only invokes rules for matching node types', () => {
320
+ const source = '"hello"';
321
+ const ast = parse(source);
322
+ const config = createConfig();
323
+
324
+ // Rule system filters by nodeTypes array
325
+ // Verified through rule.nodeTypes.includes(node.type) check
326
+ const diagnostics = validateScript(ast, source, config);
327
+
328
+ expect(diagnostics).toEqual([]);
329
+ });
330
+ });
331
+
332
+ describe('complex scripts', () => {
333
+ it('validates script with multiple statements', () => {
334
+ const source = `
335
+ "hello" => $greeting
336
+ $greeting -> .upper => $shouted
337
+ $shouted
338
+ `;
339
+ const ast = parse(source);
340
+ const config = createConfig();
341
+
342
+ const diagnostics = validateScript(ast, source, config);
343
+
344
+ expect(Array.isArray(diagnostics)).toBe(true);
345
+ });
346
+
347
+ it('validates script with conditionals', () => {
348
+ const source = 'true ? "yes" ! "no"';
349
+ const ast = parse(source);
350
+ const config = createConfig();
351
+
352
+ const diagnostics = validateScript(ast, source, config);
353
+
354
+ expect(Array.isArray(diagnostics)).toBe(true);
355
+ });
356
+
357
+ it('validates script with loops', () => {
358
+ const source = '[1, 2, 3] -> each { $ * 2 }';
359
+ const ast = parse(source);
360
+ const config = createConfig();
361
+
362
+ const diagnostics = validateScript(ast, source, config);
363
+
364
+ expect(Array.isArray(diagnostics)).toBe(true);
365
+ });
366
+
367
+ it('validates script with closures', () => {
368
+ const source = '|x: number| ($x * 2)';
369
+ const ast = parse(source);
370
+ const config = createConfig();
371
+
372
+ const diagnostics = validateScript(ast, source, config);
373
+
374
+ expect(Array.isArray(diagnostics)).toBe(true);
375
+ });
376
+
377
+ it('validates script with destructuring', () => {
378
+ const source = '[1, 2, 3] -> *<$a, $b, $c>';
379
+ const ast = parse(source);
380
+ const config = createConfig();
381
+
382
+ const diagnostics = validateScript(ast, source, config);
383
+
384
+ expect(Array.isArray(diagnostics)).toBe(true);
385
+ });
386
+ });
387
+
388
+ describe('integration behavior', () => {
389
+ it('passes source to validation context', () => {
390
+ const source = '"test source"';
391
+ const ast = parse(source);
392
+ const config = createConfig();
393
+
394
+ // Context created internally with source
395
+ const diagnostics = validateScript(ast, source, config);
396
+
397
+ expect(diagnostics).toEqual([]);
398
+ });
399
+
400
+ it('passes AST to validation context', () => {
401
+ const source = '"test"';
402
+ const ast = parse(source);
403
+ const config = createConfig();
404
+
405
+ // Context created internally with ast
406
+ const diagnostics = validateScript(ast, source, config);
407
+
408
+ expect(diagnostics).toEqual([]);
409
+ });
410
+
411
+ it('passes config to validation context', () => {
412
+ const source = '"test"';
413
+ const ast = parse(source);
414
+ const config = createConfig({ RULE_A: 'on' });
415
+
416
+ // Context created internally with config
417
+ const diagnostics = validateScript(ast, source, config);
418
+
419
+ expect(diagnostics).toEqual([]);
420
+ });
421
+
422
+ it('initializes empty diagnostics array', () => {
423
+ const source = '"test"';
424
+ const ast = parse(source);
425
+ const config = createConfig();
426
+
427
+ const diagnostics = validateScript(ast, source, config);
428
+
429
+ expect(Array.isArray(diagnostics)).toBe(true);
430
+ expect(diagnostics.length).toBe(0);
431
+ });
432
+
433
+ it('initializes empty variables map', () => {
434
+ const source = '$x';
435
+ const ast = parse(source);
436
+ const config = createConfig();
437
+
438
+ // Variables map created and available to rules
439
+ const diagnostics = validateScript(ast, source, config);
440
+
441
+ expect(diagnostics).toEqual([]);
442
+ });
443
+ });
444
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Visitor Tests
3
+ * Verify AST traversal with enter/exit callbacks.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { parse } from '@rcrsr/rill';
8
+ import { visitNode, type RuleVisitor } from '../../src/check/visitor.js';
9
+ import type { ASTNode } from '@rcrsr/rill';
10
+ import type { ValidationContext } from '../../src/check/types.js';
11
+
12
+ function createTestContext(source: string): ValidationContext {
13
+ const ast = parse(source);
14
+ return {
15
+ source,
16
+ ast,
17
+ config: { rules: {}, severity: {} },
18
+ diagnostics: [],
19
+ variables: new Map(),
20
+ };
21
+ }
22
+
23
+ describe('visitNode', () => {
24
+ it('calls enter before children and exit after', () => {
25
+ const source = '1 + 2';
26
+ const context = createTestContext(source);
27
+ const order: string[] = [];
28
+
29
+ const visitor: RuleVisitor = {
30
+ enter: (node: ASTNode) => {
31
+ order.push(`enter:${node.type}`);
32
+ },
33
+ exit: (node: ASTNode) => {
34
+ order.push(`exit:${node.type}`);
35
+ },
36
+ };
37
+
38
+ visitNode(context.ast, context, visitor);
39
+
40
+ // Verify enter/exit order
41
+ expect(order[0]).toBe('enter:Script');
42
+ expect(order[order.length - 1]).toBe('exit:Script');
43
+
44
+ // Every enter should have a matching exit
45
+ const enterCount = order.filter((s) => s.startsWith('enter:')).length;
46
+ const exitCount = order.filter((s) => s.startsWith('exit:')).length;
47
+ expect(enterCount).toBe(exitCount);
48
+ });
49
+
50
+ it('visits all node types in a complex expression', () => {
51
+ const source = `
52
+ "hello" => $greeting
53
+ $greeting -> .upper
54
+ `;
55
+ const context = createTestContext(source);
56
+ const nodeTypes = new Set<string>();
57
+
58
+ const visitor: RuleVisitor = {
59
+ enter: (node: ASTNode) => {
60
+ nodeTypes.add(node.type);
61
+ },
62
+ exit: () => {},
63
+ };
64
+
65
+ visitNode(context.ast, context, visitor);
66
+
67
+ // Verify key node types are visited
68
+ expect(nodeTypes.has('Script')).toBe(true);
69
+ expect(nodeTypes.has('Statement')).toBe(true);
70
+ expect(nodeTypes.has('PipeChain')).toBe(true);
71
+ expect(nodeTypes.has('StringLiteral')).toBe(true);
72
+ expect(nodeTypes.has('Capture')).toBe(true);
73
+ expect(nodeTypes.has('Variable')).toBe(true);
74
+ expect(nodeTypes.has('MethodCall')).toBe(true);
75
+ });
76
+
77
+ it('visits conditional branches', () => {
78
+ const source = 'true ? "yes" ! "no"';
79
+ const context = createTestContext(source);
80
+ const nodeTypes = new Set<string>();
81
+
82
+ const visitor: RuleVisitor = {
83
+ enter: (node: ASTNode) => {
84
+ nodeTypes.add(node.type);
85
+ },
86
+ exit: () => {},
87
+ };
88
+
89
+ visitNode(context.ast, context, visitor);
90
+
91
+ expect(nodeTypes.has('Conditional')).toBe(true);
92
+ expect(nodeTypes.has('BoolLiteral')).toBe(true);
93
+ expect(nodeTypes.has('StringLiteral')).toBe(true);
94
+ });
95
+
96
+ it('visits loop bodies', () => {
97
+ const source = '[1, 2, 3] -> each { $ * 2 }';
98
+ const context = createTestContext(source);
99
+ const nodeTypes = new Set<string>();
100
+
101
+ const visitor: RuleVisitor = {
102
+ enter: (node: ASTNode) => {
103
+ nodeTypes.add(node.type);
104
+ },
105
+ exit: () => {},
106
+ };
107
+
108
+ visitNode(context.ast, context, visitor);
109
+
110
+ expect(nodeTypes.has('EachExpr')).toBe(true);
111
+ expect(nodeTypes.has('Block')).toBe(true);
112
+ expect(nodeTypes.has('Tuple')).toBe(true);
113
+ });
114
+
115
+ it('visits closure parameters and body', () => {
116
+ const source = '|x: number| ($x * 2)';
117
+ const context = createTestContext(source);
118
+ const nodeTypes = new Set<string>();
119
+
120
+ const visitor: RuleVisitor = {
121
+ enter: (node: ASTNode) => {
122
+ nodeTypes.add(node.type);
123
+ },
124
+ exit: () => {},
125
+ };
126
+
127
+ visitNode(context.ast, context, visitor);
128
+
129
+ expect(nodeTypes.has('Closure')).toBe(true);
130
+ expect(nodeTypes.has('ClosureParam')).toBe(true);
131
+ expect(nodeTypes.has('GroupedExpr')).toBe(true);
132
+ expect(nodeTypes.has('BinaryExpr')).toBe(true);
133
+ });
134
+
135
+ it('visits destructure patterns', () => {
136
+ const source = '[1, 2, 3] -> *<$a, $b, $c>';
137
+ const context = createTestContext(source);
138
+ const nodeTypes = new Set<string>();
139
+
140
+ const visitor: RuleVisitor = {
141
+ enter: (node: ASTNode) => {
142
+ nodeTypes.add(node.type);
143
+ },
144
+ exit: () => {},
145
+ };
146
+
147
+ visitNode(context.ast, context, visitor);
148
+
149
+ expect(nodeTypes.has('Destructure')).toBe(true);
150
+ expect(nodeTypes.has('DestructPattern')).toBe(true);
151
+ });
152
+
153
+ it('counts nodes correctly', () => {
154
+ const source = '1 + 2 + 3';
155
+ const context = createTestContext(source);
156
+ let nodeCount = 0;
157
+
158
+ const visitor: RuleVisitor = {
159
+ enter: () => {
160
+ nodeCount++;
161
+ },
162
+ exit: () => {},
163
+ };
164
+
165
+ visitNode(context.ast, context, visitor);
166
+
167
+ // Script, Statement, PipeChain, BinaryExpr (outer), BinaryExpr (inner),
168
+ // PostfixExpr nodes, NumberLiterals
169
+ expect(nodeCount).toBeGreaterThan(5);
170
+ });
171
+ });