@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.
- package/LICENSE +21 -0
- package/dist/check/config.d.ts +20 -0
- package/dist/check/config.d.ts.map +1 -0
- package/dist/check/config.js +151 -0
- package/dist/check/config.js.map +1 -0
- package/dist/check/fixer.d.ts +39 -0
- package/dist/check/fixer.d.ts.map +1 -0
- package/dist/check/fixer.js +119 -0
- package/dist/check/fixer.js.map +1 -0
- package/dist/check/index.d.ts +10 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +21 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/rules/anti-patterns.d.ts +65 -0
- package/dist/check/rules/anti-patterns.d.ts.map +1 -0
- package/dist/check/rules/anti-patterns.js +481 -0
- package/dist/check/rules/anti-patterns.js.map +1 -0
- package/dist/check/rules/closures.d.ts +66 -0
- package/dist/check/rules/closures.d.ts.map +1 -0
- package/dist/check/rules/closures.js +370 -0
- package/dist/check/rules/closures.js.map +1 -0
- package/dist/check/rules/collections.d.ts +90 -0
- package/dist/check/rules/collections.d.ts.map +1 -0
- package/dist/check/rules/collections.js +373 -0
- package/dist/check/rules/collections.js.map +1 -0
- package/dist/check/rules/conditionals.d.ts +41 -0
- package/dist/check/rules/conditionals.d.ts.map +1 -0
- package/dist/check/rules/conditionals.js +134 -0
- package/dist/check/rules/conditionals.js.map +1 -0
- package/dist/check/rules/flow.d.ts +46 -0
- package/dist/check/rules/flow.d.ts.map +1 -0
- package/dist/check/rules/flow.js +206 -0
- package/dist/check/rules/flow.js.map +1 -0
- package/dist/check/rules/formatting.d.ts +143 -0
- package/dist/check/rules/formatting.d.ts.map +1 -0
- package/dist/check/rules/formatting.js +656 -0
- package/dist/check/rules/formatting.js.map +1 -0
- package/dist/check/rules/helpers.d.ts +26 -0
- package/dist/check/rules/helpers.d.ts.map +1 -0
- package/dist/check/rules/helpers.js +66 -0
- package/dist/check/rules/helpers.js.map +1 -0
- package/dist/check/rules/index.d.ts +21 -0
- package/dist/check/rules/index.d.ts.map +1 -0
- package/dist/check/rules/index.js +78 -0
- package/dist/check/rules/index.js.map +1 -0
- package/dist/check/rules/loops.d.ts +77 -0
- package/dist/check/rules/loops.d.ts.map +1 -0
- package/dist/check/rules/loops.js +310 -0
- package/dist/check/rules/loops.js.map +1 -0
- package/dist/check/rules/naming.d.ts +21 -0
- package/dist/check/rules/naming.d.ts.map +1 -0
- package/dist/check/rules/naming.js +174 -0
- package/dist/check/rules/naming.js.map +1 -0
- package/dist/check/rules/strings.d.ts +28 -0
- package/dist/check/rules/strings.d.ts.map +1 -0
- package/dist/check/rules/strings.js +79 -0
- package/dist/check/rules/strings.js.map +1 -0
- package/dist/check/rules/types.d.ts +41 -0
- package/dist/check/rules/types.d.ts.map +1 -0
- package/dist/check/rules/types.js +167 -0
- package/dist/check/rules/types.js.map +1 -0
- package/dist/check/types.d.ts +112 -0
- package/dist/check/types.d.ts.map +1 -0
- package/dist/check/types.js +6 -0
- package/dist/check/types.js.map +1 -0
- package/dist/check/validator.d.ts +18 -0
- package/dist/check/validator.d.ts.map +1 -0
- package/dist/check/validator.js +110 -0
- package/dist/check/validator.js.map +1 -0
- package/dist/check/visitor.d.ts +33 -0
- package/dist/check/visitor.d.ts.map +1 -0
- package/dist/check/visitor.js +259 -0
- package/dist/check/visitor.js.map +1 -0
- package/dist/cli-check.d.ts +43 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli-check.js +366 -0
- package/dist/cli-check.js.map +1 -0
- package/dist/cli-error-enrichment.d.ts +73 -0
- package/dist/cli-error-enrichment.d.ts.map +1 -0
- package/dist/cli-error-enrichment.js +205 -0
- package/dist/cli-error-enrichment.js.map +1 -0
- package/dist/cli-error-formatter.d.ts +45 -0
- package/dist/cli-error-formatter.d.ts.map +1 -0
- package/dist/cli-error-formatter.js +218 -0
- package/dist/cli-error-formatter.js.map +1 -0
- package/dist/cli-eval.d.ts +15 -0
- package/dist/cli-eval.d.ts.map +1 -0
- package/dist/cli-eval.js +116 -0
- package/dist/cli-eval.js.map +1 -0
- package/dist/cli-exec.d.ts +58 -0
- package/dist/cli-exec.d.ts.map +1 -0
- package/dist/cli-exec.js +326 -0
- package/dist/cli-exec.js.map +1 -0
- package/dist/cli-explain.d.ts +24 -0
- package/dist/cli-explain.d.ts.map +1 -0
- package/dist/cli-explain.js +68 -0
- package/dist/cli-explain.js.map +1 -0
- package/dist/cli-lsp-diagnostic.d.ts +35 -0
- package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
- package/dist/cli-lsp-diagnostic.js +98 -0
- package/dist/cli-lsp-diagnostic.js.map +1 -0
- package/dist/cli-module-loader.d.ts +19 -0
- package/dist/cli-module-loader.d.ts.map +1 -0
- package/dist/cli-module-loader.js +83 -0
- package/dist/cli-module-loader.js.map +1 -0
- package/dist/cli-shared.d.ts +62 -0
- package/dist/cli-shared.d.ts.map +1 -0
- package/dist/cli-shared.js +158 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -0
- package/dist/test-internal-import.d.ts +2 -0
- package/dist/test-internal-import.d.ts.map +1 -0
- package/dist/test-internal-import.js +7 -0
- package/dist/test-internal-import.js.map +1 -0
- package/package.json +24 -0
- package/src/check/config.ts +202 -0
- package/src/check/fixer.ts +174 -0
- package/src/check/index.ts +39 -0
- package/src/check/rules/anti-patterns.ts +585 -0
- package/src/check/rules/closures.ts +445 -0
- package/src/check/rules/collections.ts +437 -0
- package/src/check/rules/conditionals.ts +155 -0
- package/src/check/rules/flow.ts +262 -0
- package/src/check/rules/formatting.ts +811 -0
- package/src/check/rules/helpers.ts +89 -0
- package/src/check/rules/index.ts +140 -0
- package/src/check/rules/loops.ts +372 -0
- package/src/check/rules/naming.ts +242 -0
- package/src/check/rules/strings.ts +104 -0
- package/src/check/rules/types.ts +214 -0
- package/src/check/types.ts +163 -0
- package/src/check/validator.ts +136 -0
- package/src/check/visitor.ts +338 -0
- package/src/cli-check.ts +456 -0
- package/src/cli-error-enrichment.ts +274 -0
- package/src/cli-error-formatter.ts +313 -0
- package/src/cli-eval.ts +145 -0
- package/src/cli-exec.ts +408 -0
- package/src/cli-explain.ts +76 -0
- package/src/cli-lsp-diagnostic.ts +132 -0
- package/src/cli-module-loader.ts +101 -0
- package/src/cli-shared.ts +187 -0
- package/tests/check/cli-check.test.ts +189 -0
- package/tests/check/config.test.ts +350 -0
- package/tests/check/fixer.test.ts +373 -0
- package/tests/check/format-diagnostics.test.ts +327 -0
- package/tests/check/rules/anti-patterns.test.ts +467 -0
- package/tests/check/rules/closures.test.ts +192 -0
- package/tests/check/rules/collections.test.ts +380 -0
- package/tests/check/rules/conditionals.test.ts +185 -0
- package/tests/check/rules/flow.test.ts +250 -0
- package/tests/check/rules/formatting.test.ts +755 -0
- package/tests/check/rules/loops.test.ts +334 -0
- package/tests/check/rules/naming.test.ts +336 -0
- package/tests/check/rules/strings.test.ts +129 -0
- package/tests/check/rules/types.test.ts +257 -0
- package/tests/check/validator.test.ts +444 -0
- package/tests/check/visitor.test.ts +171 -0
- package/tests/cli/check.test.ts +801 -0
- package/tests/cli/error-enrichment.test.ts +510 -0
- package/tests/cli/error-formatter.test.ts +631 -0
- package/tests/cli/eval.test.ts +85 -0
- package/tests/cli/exec.test.ts +537 -0
- package/tests/cli-explain.test.ts +249 -0
- package/tests/cli-lsp-diagnostic.test.ts +202 -0
- package/tests/cli-shared.test.ts +439 -0
- package/tsconfig.json +9 -0
- 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
|
+
});
|