@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,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop Convention Rules Tests
|
|
3
|
+
* Verify loop convention enforcement.
|
|
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 { CheckConfig } from '../../../src/check/types.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// TEST HELPERS
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a config with loop rules enabled.
|
|
17
|
+
*/
|
|
18
|
+
function createConfig(rules: Record<string, 'on' | 'off'> = {}): CheckConfig {
|
|
19
|
+
return {
|
|
20
|
+
rules: {
|
|
21
|
+
LOOP_ACCUMULATOR: 'on',
|
|
22
|
+
PREFER_DO_WHILE: 'on',
|
|
23
|
+
USE_EACH: 'on',
|
|
24
|
+
...rules,
|
|
25
|
+
},
|
|
26
|
+
severity: {},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate source and extract diagnostic messages.
|
|
32
|
+
*/
|
|
33
|
+
function getDiagnostics(source: string, config?: CheckConfig): string[] {
|
|
34
|
+
const ast = parse(source);
|
|
35
|
+
const diagnostics = validateScript(ast, source, config ?? createConfig());
|
|
36
|
+
return diagnostics.map((d) => d.message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate source and check for violations.
|
|
41
|
+
*/
|
|
42
|
+
function hasViolations(source: string, config?: CheckConfig): boolean {
|
|
43
|
+
const ast = parse(source);
|
|
44
|
+
const diagnostics = validateScript(ast, source, config ?? createConfig());
|
|
45
|
+
return diagnostics.length > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate source and get diagnostic codes.
|
|
50
|
+
*/
|
|
51
|
+
function getCodes(source: string, config?: CheckConfig): string[] {
|
|
52
|
+
const ast = parse(source);
|
|
53
|
+
const diagnostics = validateScript(ast, source, config ?? createConfig());
|
|
54
|
+
return diagnostics.map((d) => d.code);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================
|
|
58
|
+
// LOOP_ACCUMULATOR TESTS
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
describe('LOOP_ACCUMULATOR', () => {
|
|
62
|
+
const config = createConfig({ PREFER_DO_WHILE: 'off', USE_EACH: 'off' });
|
|
63
|
+
|
|
64
|
+
it('accepts $ as accumulator in while loop', () => {
|
|
65
|
+
expect(hasViolations('0 -> ($ < 5) @ { $ + 1 }', config)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('accepts $ as accumulator in do-while loop', () => {
|
|
69
|
+
expect(hasViolations('@ { $ + 1 } ? ($ < 5)', config)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('accepts captures only used within iteration', () => {
|
|
73
|
+
const source = `
|
|
74
|
+
0 -> ($ < 5) @ {
|
|
75
|
+
$ => $x
|
|
76
|
+
log($x)
|
|
77
|
+
$x + 1
|
|
78
|
+
}
|
|
79
|
+
`.trim();
|
|
80
|
+
|
|
81
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('detects captured variable referenced in while loop condition', () => {
|
|
85
|
+
const source = `
|
|
86
|
+
0 -> ($x < 5) @ {
|
|
87
|
+
$ => $x
|
|
88
|
+
$x + 1
|
|
89
|
+
}
|
|
90
|
+
`.trim();
|
|
91
|
+
|
|
92
|
+
const messages = getDiagnostics(source, config);
|
|
93
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
94
|
+
expect(messages[0]).toContain(
|
|
95
|
+
'$x captured in loop body but referenced in condition'
|
|
96
|
+
);
|
|
97
|
+
expect(messages[0]).toContain('reset each iteration');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('detects captured variable referenced in do-while loop condition', () => {
|
|
101
|
+
const source = `
|
|
102
|
+
@ {
|
|
103
|
+
$ => $val
|
|
104
|
+
$val + 1
|
|
105
|
+
} ? ($val < 10)
|
|
106
|
+
`.trim();
|
|
107
|
+
|
|
108
|
+
const messages = getDiagnostics(source, config);
|
|
109
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
110
|
+
expect(messages[0]).toContain(
|
|
111
|
+
'$val captured in loop body but referenced in condition'
|
|
112
|
+
);
|
|
113
|
+
expect(messages[0]).toContain('reset each iteration');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('accepts loop without captures', () => {
|
|
117
|
+
const source = `
|
|
118
|
+
0 -> ($ < 5) @ {
|
|
119
|
+
log($)
|
|
120
|
+
$ + 1
|
|
121
|
+
}
|
|
122
|
+
`.trim();
|
|
123
|
+
|
|
124
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('accepts captures not referenced in condition', () => {
|
|
128
|
+
const source = `
|
|
129
|
+
0 => $i
|
|
130
|
+
($i < 5) @ {
|
|
131
|
+
$ => $temp
|
|
132
|
+
log($temp)
|
|
133
|
+
$ + 1
|
|
134
|
+
}
|
|
135
|
+
`.trim();
|
|
136
|
+
|
|
137
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('detects multiple captured variables in condition', () => {
|
|
141
|
+
const source = `
|
|
142
|
+
0 -> ($x < $y) @ {
|
|
143
|
+
$ => $x
|
|
144
|
+
$ => $y
|
|
145
|
+
$x + $y
|
|
146
|
+
}
|
|
147
|
+
`.trim();
|
|
148
|
+
|
|
149
|
+
const messages = getDiagnostics(source, config);
|
|
150
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
151
|
+
expect(messages[0]).toContain(
|
|
152
|
+
'captured in loop body but referenced in condition'
|
|
153
|
+
);
|
|
154
|
+
// Should mention both variables
|
|
155
|
+
expect(messages[0]).toMatch(/\$x.*\$y|\$y.*\$x/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('has correct severity and code', () => {
|
|
159
|
+
const source = `
|
|
160
|
+
0 -> ($x < 5) @ {
|
|
161
|
+
$ => $x
|
|
162
|
+
$x + 1
|
|
163
|
+
}
|
|
164
|
+
`.trim();
|
|
165
|
+
const ast = parse(source);
|
|
166
|
+
const diagnostics = validateScript(ast, source, config);
|
|
167
|
+
|
|
168
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
169
|
+
expect(diagnostics[0]?.code).toBe('LOOP_ACCUMULATOR');
|
|
170
|
+
expect(diagnostics[0]?.severity).toBe('info');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ============================================================
|
|
175
|
+
// PREFER_DO_WHILE TESTS
|
|
176
|
+
// ============================================================
|
|
177
|
+
|
|
178
|
+
describe('PREFER_DO_WHILE', () => {
|
|
179
|
+
const config = createConfig({ LOOP_ACCUMULATOR: 'off', USE_EACH: 'off' });
|
|
180
|
+
|
|
181
|
+
it('accepts do-while for retry patterns', () => {
|
|
182
|
+
const source = `
|
|
183
|
+
@ {
|
|
184
|
+
attemptOperation()
|
|
185
|
+
} ? (.contains("RETRY"))
|
|
186
|
+
`.trim();
|
|
187
|
+
|
|
188
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('suggests do-while for while loop with retry function', () => {
|
|
192
|
+
const source = `
|
|
193
|
+
(true) @ {
|
|
194
|
+
retryOperation()
|
|
195
|
+
}
|
|
196
|
+
`.trim();
|
|
197
|
+
|
|
198
|
+
const messages = getDiagnostics(source, config);
|
|
199
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
200
|
+
expect(messages[0]).toContain('do-while for retry patterns');
|
|
201
|
+
expect(messages[0]).toContain('@ { body } ? (condition)');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('suggests do-while for while loop with attempt function', () => {
|
|
205
|
+
const source = `
|
|
206
|
+
($ < 3) @ {
|
|
207
|
+
attemptConnection()
|
|
208
|
+
}
|
|
209
|
+
`.trim();
|
|
210
|
+
|
|
211
|
+
const messages = getDiagnostics(source, config);
|
|
212
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
213
|
+
expect(messages[0]).toContain('do-while for retry patterns');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('accepts while loop without retry pattern', () => {
|
|
217
|
+
const source = '0 -> ($ < 5) @ { $ + 1 }';
|
|
218
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('has correct severity and code', () => {
|
|
222
|
+
const source = '(true) @ { retryOp() }';
|
|
223
|
+
const ast = parse(source);
|
|
224
|
+
const diagnostics = validateScript(ast, source, config);
|
|
225
|
+
|
|
226
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
227
|
+
expect(diagnostics[0]?.code).toBe('PREFER_DO_WHILE');
|
|
228
|
+
expect(diagnostics[0]?.severity).toBe('info');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ============================================================
|
|
233
|
+
// USE_EACH TESTS
|
|
234
|
+
// ============================================================
|
|
235
|
+
|
|
236
|
+
describe('USE_EACH', () => {
|
|
237
|
+
const config = createConfig({
|
|
238
|
+
LOOP_ACCUMULATOR: 'off',
|
|
239
|
+
PREFER_DO_WHILE: 'off',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('accepts each for collection iteration', () => {
|
|
243
|
+
expect(hasViolations('$items -> each { process($) }', config)).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('suggests each for while loop with .len check', () => {
|
|
247
|
+
const source = `
|
|
248
|
+
0 => $i
|
|
249
|
+
($i < $items.len) @ {
|
|
250
|
+
$items[$i] -> process()
|
|
251
|
+
$i + 1
|
|
252
|
+
}
|
|
253
|
+
`.trim();
|
|
254
|
+
|
|
255
|
+
const messages = getDiagnostics(source, config);
|
|
256
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
257
|
+
expect(messages[0]).toContain("Use 'each' for collection iteration");
|
|
258
|
+
expect(messages[0]).toContain('collection -> each { body }');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('suggests each for while loop with array indexing', () => {
|
|
262
|
+
const source = `
|
|
263
|
+
($idx < 10) @ {
|
|
264
|
+
$data[$idx]
|
|
265
|
+
}
|
|
266
|
+
`.trim();
|
|
267
|
+
|
|
268
|
+
const messages = getDiagnostics(source, config);
|
|
269
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
270
|
+
expect(messages[0]).toContain("Use 'each'");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('accepts while loop without collection pattern', () => {
|
|
274
|
+
// Simple counter without array indexing or .len
|
|
275
|
+
expect(hasViolations('0 -> ($ == 0) @ { 1 }', config)).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('has correct severity and code', () => {
|
|
279
|
+
const source = '($i < $items.len) @ { $i + 1 }';
|
|
280
|
+
const ast = parse(source);
|
|
281
|
+
const diagnostics = validateScript(ast, source, config);
|
|
282
|
+
|
|
283
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
284
|
+
expect(diagnostics[0]?.code).toBe('USE_EACH');
|
|
285
|
+
expect(diagnostics[0]?.severity).toBe('info');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ============================================================
|
|
290
|
+
// INTEGRATION TESTS
|
|
291
|
+
// ============================================================
|
|
292
|
+
|
|
293
|
+
describe('Loop rules integration', () => {
|
|
294
|
+
it('can detect multiple violations in same code', () => {
|
|
295
|
+
const source = `
|
|
296
|
+
0 => $i
|
|
297
|
+
($index < $items.len) @ {
|
|
298
|
+
$i => $index
|
|
299
|
+
$items[$index]
|
|
300
|
+
$index + 1
|
|
301
|
+
}
|
|
302
|
+
`.trim();
|
|
303
|
+
|
|
304
|
+
const codes = getCodes(source);
|
|
305
|
+
// Should detect both USE_EACH and LOOP_ACCUMULATOR
|
|
306
|
+
expect(codes).toContain('USE_EACH');
|
|
307
|
+
expect(codes).toContain('LOOP_ACCUMULATOR');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('respects rule configuration', () => {
|
|
311
|
+
const source = `
|
|
312
|
+
0 -> ($x < 5) @ {
|
|
313
|
+
$ => $x
|
|
314
|
+
$x + 1
|
|
315
|
+
}
|
|
316
|
+
`.trim();
|
|
317
|
+
|
|
318
|
+
// With LOOP_ACCUMULATOR on
|
|
319
|
+
const withRule = createConfig({
|
|
320
|
+
LOOP_ACCUMULATOR: 'on',
|
|
321
|
+
USE_EACH: 'off',
|
|
322
|
+
PREFER_DO_WHILE: 'off',
|
|
323
|
+
});
|
|
324
|
+
expect(hasViolations(source, withRule)).toBe(true);
|
|
325
|
+
|
|
326
|
+
// With LOOP_ACCUMULATOR off
|
|
327
|
+
const withoutRule = createConfig({
|
|
328
|
+
LOOP_ACCUMULATOR: 'off',
|
|
329
|
+
USE_EACH: 'off',
|
|
330
|
+
PREFER_DO_WHILE: 'off',
|
|
331
|
+
});
|
|
332
|
+
expect(hasViolations(source, withoutRule)).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming Rules Tests
|
|
3
|
+
* Verify snake_case enforcement for variables, parameters, and dict keys.
|
|
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 { CheckConfig } from '../../../src/check/types.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// TEST HELPERS
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a config with NAMING_SNAKE_CASE rule enabled.
|
|
17
|
+
*/
|
|
18
|
+
function createConfig(): CheckConfig {
|
|
19
|
+
return {
|
|
20
|
+
rules: { NAMING_SNAKE_CASE: 'on' },
|
|
21
|
+
severity: {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate source and extract diagnostic messages.
|
|
27
|
+
*/
|
|
28
|
+
function getDiagnostics(source: string): string[] {
|
|
29
|
+
const ast = parse(source);
|
|
30
|
+
const diagnostics = validateScript(ast, source, createConfig());
|
|
31
|
+
return diagnostics.map((d) => d.message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate source and check for violations.
|
|
36
|
+
*/
|
|
37
|
+
function hasViolations(source: string): boolean {
|
|
38
|
+
const ast = parse(source);
|
|
39
|
+
const diagnostics = validateScript(ast, source, createConfig());
|
|
40
|
+
return diagnostics.length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate source and get fix suggestions.
|
|
45
|
+
*/
|
|
46
|
+
function getFixes(
|
|
47
|
+
source: string
|
|
48
|
+
): Array<{ description: string; replacement: string }> {
|
|
49
|
+
const ast = parse(source);
|
|
50
|
+
const diagnostics = validateScript(ast, source, createConfig());
|
|
51
|
+
return diagnostics
|
|
52
|
+
.filter((d) => d.fix !== null)
|
|
53
|
+
.map((d) => ({
|
|
54
|
+
description: d.fix!.description,
|
|
55
|
+
replacement: d.fix!.replacement,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// TESTS
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
describe('NAMING_SNAKE_CASE', () => {
|
|
64
|
+
describe('variables', () => {
|
|
65
|
+
it('accepts valid snake_case variables', () => {
|
|
66
|
+
expect(hasViolations('"test" => $user_name')).toBe(false);
|
|
67
|
+
expect(hasViolations('"test" => $item_list')).toBe(false);
|
|
68
|
+
expect(hasViolations('"test" => $is_valid')).toBe(false);
|
|
69
|
+
expect(hasViolations('"test" => $count')).toBe(false);
|
|
70
|
+
expect(hasViolations('"test" => $x')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('accepts pipe variable', () => {
|
|
74
|
+
expect(hasViolations('"test" -> $')).toBe(false);
|
|
75
|
+
expect(hasViolations('"test" -> $ -> .len')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects camelCase variables', () => {
|
|
79
|
+
const messages = getDiagnostics('"test" => $userName');
|
|
80
|
+
expect(messages).toHaveLength(1);
|
|
81
|
+
expect(messages[0]).toContain(
|
|
82
|
+
"Captured variable 'userName' should use snake_case"
|
|
83
|
+
);
|
|
84
|
+
expect(messages[0]).toContain('user_name');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rejects PascalCase variables', () => {
|
|
88
|
+
const messages = getDiagnostics('"test" => $UserName');
|
|
89
|
+
expect(messages).toHaveLength(1);
|
|
90
|
+
expect(messages[0]).toContain(
|
|
91
|
+
"Captured variable 'UserName' should use snake_case"
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('rejects kebab-case variables', () => {
|
|
96
|
+
const messages = getDiagnostics('"test" => $user_name');
|
|
97
|
+
// This should pass - it's already snake_case
|
|
98
|
+
expect(hasViolations('"test" => $user_name')).toBe(false);
|
|
99
|
+
|
|
100
|
+
// Test actual kebab-case (with hyphens) - but this would be a parse error
|
|
101
|
+
// so we skip testing invalid syntax
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects variables with consecutive underscores', () => {
|
|
105
|
+
const messages = getDiagnostics('"test" => $user__name');
|
|
106
|
+
expect(messages).toHaveLength(1);
|
|
107
|
+
expect(messages[0]).toContain(
|
|
108
|
+
"Captured variable 'user__name' should use snake_case"
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects variables with trailing underscore', () => {
|
|
113
|
+
const messages = getDiagnostics('"test" => $user_name_');
|
|
114
|
+
expect(messages).toHaveLength(1);
|
|
115
|
+
expect(messages[0]).toContain(
|
|
116
|
+
"Captured variable 'user_name_' should use snake_case"
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('provides fix suggestion for camelCase', () => {
|
|
121
|
+
const fixes = getFixes('"test" => $userName');
|
|
122
|
+
expect(fixes).toHaveLength(1);
|
|
123
|
+
expect(fixes[0].description).toBe("Rename 'userName' to 'user_name'");
|
|
124
|
+
expect(fixes[0].replacement).toContain('$user_name');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('provides fix suggestion for PascalCase', () => {
|
|
128
|
+
const fixes = getFixes('"test" => $ItemList');
|
|
129
|
+
expect(fixes).toHaveLength(1);
|
|
130
|
+
expect(fixes[0].description).toBe("Rename 'ItemList' to 'item_list'");
|
|
131
|
+
expect(fixes[0].replacement).toContain('$item_list');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('closure parameters', () => {
|
|
136
|
+
it('accepts valid snake_case parameters', () => {
|
|
137
|
+
expect(hasViolations('|user_name| $user_name')).toBe(false);
|
|
138
|
+
expect(hasViolations('|item_count| $item_count')).toBe(false);
|
|
139
|
+
expect(hasViolations('|x| $x')).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('accepts single-letter parameters', () => {
|
|
143
|
+
expect(hasViolations('|x| $x')).toBe(false);
|
|
144
|
+
expect(hasViolations('|a, b| ($a + $b)')).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('rejects camelCase parameters', () => {
|
|
148
|
+
const messages = getDiagnostics('|userName| $userName');
|
|
149
|
+
expect(messages).toHaveLength(1);
|
|
150
|
+
expect(messages[0]).toContain(
|
|
151
|
+
"Parameter 'userName' should use snake_case"
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('rejects PascalCase parameters', () => {
|
|
156
|
+
const messages = getDiagnostics('|UserName| $UserName');
|
|
157
|
+
expect(messages).toHaveLength(1);
|
|
158
|
+
expect(messages[0]).toContain(
|
|
159
|
+
"Parameter 'UserName' should use snake_case"
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('detects violations in multiple parameters', () => {
|
|
164
|
+
const messages = getDiagnostics(
|
|
165
|
+
'|firstName, lastName| "{$firstName} {$lastName}"'
|
|
166
|
+
);
|
|
167
|
+
expect(messages).toHaveLength(2);
|
|
168
|
+
expect(messages[0]).toContain(
|
|
169
|
+
"Parameter 'firstName' should use snake_case"
|
|
170
|
+
);
|
|
171
|
+
expect(messages[1]).toContain(
|
|
172
|
+
"Parameter 'lastName' should use snake_case"
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('provides fix suggestion for camelCase parameter', () => {
|
|
177
|
+
const fixes = getFixes('|userName| $userName');
|
|
178
|
+
expect(fixes).toHaveLength(1);
|
|
179
|
+
expect(fixes[0].description).toBe("Rename 'userName' to 'user_name'");
|
|
180
|
+
expect(fixes[0].replacement).toContain('user_name');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('dict keys', () => {
|
|
185
|
+
it('accepts valid snake_case dict keys', () => {
|
|
186
|
+
expect(hasViolations('[user_name: "Alice"]')).toBe(false);
|
|
187
|
+
expect(hasViolations('[first_name: "Alice", last_name: "Smith"]')).toBe(
|
|
188
|
+
false
|
|
189
|
+
);
|
|
190
|
+
expect(hasViolations('[is_active: true]')).toBe(false);
|
|
191
|
+
expect(hasViolations('[count: 42]')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('rejects camelCase dict keys', () => {
|
|
195
|
+
const messages = getDiagnostics('[userName: "Alice"]');
|
|
196
|
+
expect(messages).toHaveLength(1);
|
|
197
|
+
expect(messages[0]).toContain(
|
|
198
|
+
"Dict key 'userName' should use snake_case"
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('rejects PascalCase dict keys', () => {
|
|
203
|
+
const messages = getDiagnostics('[UserName: "Alice"]');
|
|
204
|
+
expect(messages).toHaveLength(1);
|
|
205
|
+
expect(messages[0]).toContain(
|
|
206
|
+
"Dict key 'UserName' should use snake_case"
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('detects violations in multiple dict keys', () => {
|
|
211
|
+
const messages = getDiagnostics(
|
|
212
|
+
'[firstName: "Alice", lastName: "Smith"]'
|
|
213
|
+
);
|
|
214
|
+
expect(messages).toHaveLength(2);
|
|
215
|
+
expect(messages[0]).toContain(
|
|
216
|
+
"Dict key 'firstName' should use snake_case"
|
|
217
|
+
);
|
|
218
|
+
expect(messages[1]).toContain(
|
|
219
|
+
"Dict key 'lastName' should use snake_case"
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('provides fix suggestion for camelCase dict key', () => {
|
|
224
|
+
const fixes = getFixes('[userName: "Alice"]');
|
|
225
|
+
expect(fixes).toHaveLength(1);
|
|
226
|
+
expect(fixes[0].description).toBe("Rename 'userName' to 'user_name'");
|
|
227
|
+
expect(fixes[0].replacement).toContain('user_name:');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('handles mixed valid and invalid keys', () => {
|
|
231
|
+
const messages = getDiagnostics('[user_name: "Alice", isActive: true]');
|
|
232
|
+
expect(messages).toHaveLength(1);
|
|
233
|
+
expect(messages[0]).toContain(
|
|
234
|
+
"Dict key 'isActive' should use snake_case"
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('captured variables', () => {
|
|
240
|
+
it('accepts valid snake_case captures', () => {
|
|
241
|
+
expect(hasViolations('"test" => $result_data')).toBe(false);
|
|
242
|
+
expect(hasViolations('42 => $item_count')).toBe(false);
|
|
243
|
+
expect(hasViolations('true => $is_valid')).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('rejects camelCase captures', () => {
|
|
247
|
+
const messages = getDiagnostics('"test" => $resultData');
|
|
248
|
+
expect(messages).toHaveLength(1);
|
|
249
|
+
expect(messages[0]).toContain(
|
|
250
|
+
"Captured variable 'resultData' should use snake_case"
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('rejects PascalCase captures', () => {
|
|
255
|
+
const messages = getDiagnostics('"test" => $ResultData');
|
|
256
|
+
expect(messages).toHaveLength(1);
|
|
257
|
+
expect(messages[0]).toContain(
|
|
258
|
+
"Captured variable 'ResultData' should use snake_case"
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('provides fix suggestion for camelCase capture', () => {
|
|
263
|
+
const fixes = getFixes('"test" => $resultData');
|
|
264
|
+
expect(fixes).toHaveLength(1);
|
|
265
|
+
expect(fixes[0].description).toBe("Rename 'resultData' to 'result_data'");
|
|
266
|
+
expect(fixes[0].replacement).toContain('result_data');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('edge cases', () => {
|
|
271
|
+
it('handles numbers in names', () => {
|
|
272
|
+
expect(hasViolations('"test" => $item_1')).toBe(false);
|
|
273
|
+
expect(hasViolations('"test" => $user_2_name')).toBe(false);
|
|
274
|
+
expect(hasViolations('[item_1: "test"]')).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('handles single underscore', () => {
|
|
278
|
+
expect(hasViolations('"test" => $_')).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('rejects empty variable name (parser should prevent this)', () => {
|
|
282
|
+
// This would be a parse error, so we don't test it
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('handles leading underscore', () => {
|
|
286
|
+
expect(hasViolations('"test" => $_private')).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('converts mixed case correctly', () => {
|
|
290
|
+
const fixes = getFixes('"test" => $getUserByID');
|
|
291
|
+
expect(fixes).toHaveLength(1);
|
|
292
|
+
expect(fixes[0].replacement).toContain('$get_user_by_id');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('handles consecutive uppercase correctly', () => {
|
|
296
|
+
const fixes = getFixes('"test" => $XMLParser');
|
|
297
|
+
expect(fixes[0].replacement).toContain('$xml_parser');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('detects multiple violations in same script', () => {
|
|
301
|
+
const source = `
|
|
302
|
+
"Alice" => $userName
|
|
303
|
+
"Smith" => $lastName
|
|
304
|
+
[firstName: $userName, lastName: $lastName]
|
|
305
|
+
`;
|
|
306
|
+
const messages = getDiagnostics(source);
|
|
307
|
+
expect(messages.length).toBeGreaterThanOrEqual(4); // 2 variables + 2 dict keys
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('rule configuration', () => {
|
|
312
|
+
it('does not report violations when rule is disabled', () => {
|
|
313
|
+
const source = '"test" => $userName';
|
|
314
|
+
const ast = parse(source);
|
|
315
|
+
const config: CheckConfig = {
|
|
316
|
+
rules: { NAMING_SNAKE_CASE: 'off' },
|
|
317
|
+
severity: {},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const diagnostics = validateScript(ast, source, config);
|
|
321
|
+
expect(diagnostics).toHaveLength(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('reports violations when rule is set to warn', () => {
|
|
325
|
+
const source = '"test" => $userName';
|
|
326
|
+
const ast = parse(source);
|
|
327
|
+
const config: CheckConfig = {
|
|
328
|
+
rules: { NAMING_SNAKE_CASE: 'warn' },
|
|
329
|
+
severity: {},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const diagnostics = validateScript(ast, source, config);
|
|
333
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|