@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,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-Pattern Rules Tests
|
|
3
|
+
* Verify anti-pattern detection 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 anti-pattern rules enabled.
|
|
17
|
+
*/
|
|
18
|
+
function createConfig(rules: Record<string, 'on' | 'off'> = {}): CheckConfig {
|
|
19
|
+
return {
|
|
20
|
+
rules: {
|
|
21
|
+
AVOID_REASSIGNMENT: 'on',
|
|
22
|
+
COMPLEX_CONDITION: 'on',
|
|
23
|
+
LOOP_OUTER_CAPTURE: '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
|
+
// AVOID_REASSIGNMENT TESTS
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
describe('AVOID_REASSIGNMENT', () => {
|
|
62
|
+
const config = createConfig({
|
|
63
|
+
COMPLEX_CONDITION: 'off',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accepts first variable assignment', () => {
|
|
67
|
+
expect(hasViolations('"initial" => $x', config)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('accepts multiple different variables', () => {
|
|
71
|
+
const source = `
|
|
72
|
+
"first" => $x
|
|
73
|
+
"second" => $y
|
|
74
|
+
"third" => $z
|
|
75
|
+
`;
|
|
76
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('warns on variable reassignment', () => {
|
|
80
|
+
const source = `
|
|
81
|
+
"initial" => $x
|
|
82
|
+
"updated" => $x
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
86
|
+
const messages = getDiagnostics(source, config);
|
|
87
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
88
|
+
expect(messages[0]).toContain('reassignment');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('includes line number of first definition', () => {
|
|
92
|
+
const source = `
|
|
93
|
+
"initial" => $x
|
|
94
|
+
"updated" => $x
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const messages = getDiagnostics(source, config);
|
|
98
|
+
expect(messages[0]).toContain('line');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('suggests alternatives in message', () => {
|
|
102
|
+
const source = `
|
|
103
|
+
"first" => $x
|
|
104
|
+
"second" => $x
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const messages = getDiagnostics(source, config);
|
|
108
|
+
expect(messages[0]).toMatch(/new variable|functional/i);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('has correct severity and code', () => {
|
|
112
|
+
const source = `
|
|
113
|
+
"a" => $x
|
|
114
|
+
"b" => $x
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
const ast = parse(source);
|
|
118
|
+
const diagnostics = validateScript(ast, source, config);
|
|
119
|
+
|
|
120
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
121
|
+
expect(diagnostics[0]?.code).toBe('AVOID_REASSIGNMENT');
|
|
122
|
+
expect(diagnostics[0]?.severity).toBe('warning');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('detects multiple reassignments', () => {
|
|
126
|
+
const source = `
|
|
127
|
+
"first" => $x
|
|
128
|
+
"second" => $x
|
|
129
|
+
"third" => $x
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
const diagnostics = getDiagnostics(source, config);
|
|
133
|
+
expect(diagnostics.length).toBe(2); // Two reassignments (second and third)
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not warn for variables in sibling closures', () => {
|
|
137
|
+
// Variables captured in different sibling closures are independent
|
|
138
|
+
// and should not be considered reassignments
|
|
139
|
+
const source = `
|
|
140
|
+
|skill_name| {
|
|
141
|
+
"output" => $result
|
|
142
|
+
$result
|
|
143
|
+
} => $run_skill
|
|
144
|
+
|
|
145
|
+
|doc_path| {
|
|
146
|
+
"output" => $result
|
|
147
|
+
$result
|
|
148
|
+
} => $review_loop
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('does warn for variables in same closure', () => {
|
|
155
|
+
// Variables reassigned within the same closure should trigger warning
|
|
156
|
+
const source = `
|
|
157
|
+
|param| {
|
|
158
|
+
"first" => $result
|
|
159
|
+
"second" => $result
|
|
160
|
+
$result
|
|
161
|
+
} => $fn
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
165
|
+
const codes = getCodes(source, config);
|
|
166
|
+
expect(codes).toContain('AVOID_REASSIGNMENT');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does warn for variables in parent scope', () => {
|
|
170
|
+
// Variables defined in outer scope and reassigned in nested closure
|
|
171
|
+
// should trigger warning
|
|
172
|
+
const source = `
|
|
173
|
+
"outer" => $result
|
|
174
|
+
|
|
175
|
+
|param| {
|
|
176
|
+
"inner" => $result
|
|
177
|
+
$result
|
|
178
|
+
} => $fn
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
182
|
+
const codes = getCodes(source, config);
|
|
183
|
+
expect(codes).toContain('AVOID_REASSIGNMENT');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ============================================================
|
|
188
|
+
// COMPLEX_CONDITION TESTS
|
|
189
|
+
// ============================================================
|
|
190
|
+
|
|
191
|
+
describe('COMPLEX_CONDITION', () => {
|
|
192
|
+
const config = createConfig({
|
|
193
|
+
AVOID_REASSIGNMENT: 'off',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('accepts simple conditions', () => {
|
|
197
|
+
expect(hasViolations('($x > 5) ? "big"', config)).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('accepts conditions with one operator', () => {
|
|
201
|
+
expect(hasViolations('(($x > 5) && ($y < 10)) ? "valid"', config)).toBe(
|
|
202
|
+
false
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('accepts conditions with two operators', () => {
|
|
207
|
+
expect(
|
|
208
|
+
hasViolations('(($x > 5) && ($y < 10) && ($z == 0)) ? "ok"', config)
|
|
209
|
+
).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('warns on conditions with 3+ boolean operators', () => {
|
|
213
|
+
const source =
|
|
214
|
+
'(($x > 5) && (($y < 10) || ($z == 0)) && ($a != 1)) ? "complex"';
|
|
215
|
+
|
|
216
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
217
|
+
const messages = getDiagnostics(source, config);
|
|
218
|
+
expect(messages[0]).toContain('Complex condition');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('warns on deeply nested conditions', () => {
|
|
222
|
+
const source =
|
|
223
|
+
'((($x > 5) && ($y < 10)) || (($z == 0) && ($a != 1))) ? "nested"';
|
|
224
|
+
|
|
225
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('suggests extracting to named checks', () => {
|
|
229
|
+
const source =
|
|
230
|
+
'(($x > 5) && ($y < 10) && ($z == 0) && ($a != 1)) ? "extract"';
|
|
231
|
+
|
|
232
|
+
const messages = getDiagnostics(source, config);
|
|
233
|
+
expect(messages[0]).toMatch(/extract|named/i);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('has correct severity and code', () => {
|
|
237
|
+
const source = '(($x > 5) && ($y < 10) && ($z == 0) && ($a != 1)) ? "test"';
|
|
238
|
+
|
|
239
|
+
const ast = parse(source);
|
|
240
|
+
const diagnostics = validateScript(ast, source, config);
|
|
241
|
+
|
|
242
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
243
|
+
expect(diagnostics[0]?.code).toBe('COMPLEX_CONDITION');
|
|
244
|
+
expect(diagnostics[0]?.severity).toBe('info');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('checks nesting depth independent of operator count', () => {
|
|
248
|
+
// High nesting but few operators
|
|
249
|
+
const source = '(((($x > 5))) || ((($y < 10)))) ? "deep"';
|
|
250
|
+
|
|
251
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('does not flag non-boolean operators', () => {
|
|
255
|
+
const source = '((($x + 5) * ($y - 10)) > 0) ? "arithmetic"';
|
|
256
|
+
|
|
257
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ============================================================
|
|
262
|
+
// LOOP_OUTER_CAPTURE TESTS
|
|
263
|
+
// ============================================================
|
|
264
|
+
|
|
265
|
+
describe('LOOP_OUTER_CAPTURE', () => {
|
|
266
|
+
const config = createConfig({
|
|
267
|
+
AVOID_REASSIGNMENT: 'off',
|
|
268
|
+
COMPLEX_CONDITION: 'off',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('accepts captures of new variables in loop body', () => {
|
|
272
|
+
// This is fine - $temp is new, not modifying outer scope
|
|
273
|
+
const source = `
|
|
274
|
+
[1, 2, 3] -> each {
|
|
275
|
+
$ * 2 => $temp
|
|
276
|
+
$temp
|
|
277
|
+
}
|
|
278
|
+
`;
|
|
279
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('accepts loops without captures', () => {
|
|
283
|
+
const source = '[1, 2, 3] -> each { $ * 2 }';
|
|
284
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('accepts fold with accumulator pattern', () => {
|
|
288
|
+
const source = '[1, 2, 3] -> fold(0) { $@ + $ }';
|
|
289
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('warns when each body captures outer variable', () => {
|
|
293
|
+
const source = `
|
|
294
|
+
0 => $count
|
|
295
|
+
[1, 2, 3] -> each { $count + 1 => $count }
|
|
296
|
+
`;
|
|
297
|
+
|
|
298
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
299
|
+
const codes = getCodes(source, config);
|
|
300
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('warns when map body captures outer variable', () => {
|
|
304
|
+
const source = `
|
|
305
|
+
"" => $result
|
|
306
|
+
[1, 2, 3] -> map { $result + $ => $result }
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
310
|
+
const codes = getCodes(source, config);
|
|
311
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('warns when while loop body captures outer variable', () => {
|
|
315
|
+
const source = `
|
|
316
|
+
0 => $i
|
|
317
|
+
0 -> ($ < 3) @ {
|
|
318
|
+
$i + 1 => $i
|
|
319
|
+
$ + 1
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
322
|
+
|
|
323
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
324
|
+
const codes = getCodes(source, config);
|
|
325
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('warns when filter body captures outer variable', () => {
|
|
329
|
+
const source = `
|
|
330
|
+
0 => $count
|
|
331
|
+
[1, 2, 3] -> filter {
|
|
332
|
+
$count + 1 => $count
|
|
333
|
+
($ > 1)
|
|
334
|
+
}
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
338
|
+
const codes = getCodes(source, config);
|
|
339
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('provides helpful message with line reference', () => {
|
|
343
|
+
const source = `
|
|
344
|
+
0 => $sum
|
|
345
|
+
[1, 2, 3] -> each { $sum + $ => $sum }
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
const messages = getDiagnostics(source, config);
|
|
349
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
350
|
+
expect(messages[0]).toContain('Cannot modify outer variable');
|
|
351
|
+
expect(messages[0]).toContain('$sum');
|
|
352
|
+
expect(messages[0]).toContain('fold');
|
|
353
|
+
expect(messages[0]).toContain('line');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('has warning severity', () => {
|
|
357
|
+
const source = `
|
|
358
|
+
0 => $x
|
|
359
|
+
[1, 2, 3] -> each { $x + 1 => $x }
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
const ast = parse(source);
|
|
363
|
+
const diagnostics = validateScript(ast, source, config);
|
|
364
|
+
const loopCapture = diagnostics.find(
|
|
365
|
+
(d) => d.code === 'LOOP_OUTER_CAPTURE'
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(loopCapture).toBeDefined();
|
|
369
|
+
expect(loopCapture?.severity).toBe('warning');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('detects multiple outer captures in same loop', () => {
|
|
373
|
+
const source = `
|
|
374
|
+
0 => $a
|
|
375
|
+
0 => $b
|
|
376
|
+
[1, 2, 3] -> each {
|
|
377
|
+
$a + 1 => $a
|
|
378
|
+
$b + 1 => $b
|
|
379
|
+
}
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
const ast = parse(source);
|
|
383
|
+
const diagnostics = validateScript(ast, source, config);
|
|
384
|
+
const loopCaptures = diagnostics.filter(
|
|
385
|
+
(d) => d.code === 'LOOP_OUTER_CAPTURE'
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
expect(loopCaptures.length).toBe(2);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('warns when do-while loop body captures outer variable', () => {
|
|
392
|
+
const source = `
|
|
393
|
+
0 => $count
|
|
394
|
+
0 -> @ {
|
|
395
|
+
$count + 1 => $count
|
|
396
|
+
$ + 1
|
|
397
|
+
} ? ($ < 3)
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
401
|
+
const codes = getCodes(source, config);
|
|
402
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('accepts closures that capture outer variables (different scope)', () => {
|
|
406
|
+
// Closures have their own scope, so captures inside them shouldn't trigger
|
|
407
|
+
const source = `
|
|
408
|
+
10 => $multiplier
|
|
409
|
+
[1, 2, 3] -> map {
|
|
410
|
+
|x| ($x * $multiplier) => $fn
|
|
411
|
+
$fn($)
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('warns when fold body captures outer variable (distinct from accumulator)', () => {
|
|
419
|
+
const source = `
|
|
420
|
+
0 => $extraSum
|
|
421
|
+
[1, 2, 3] -> fold(0) {
|
|
422
|
+
$extraSum + 1 => $extraSum
|
|
423
|
+
$@ + $ + $extraSum
|
|
424
|
+
}
|
|
425
|
+
`;
|
|
426
|
+
|
|
427
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
428
|
+
const codes = getCodes(source, config);
|
|
429
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('does not warn for variables in sibling closures', () => {
|
|
433
|
+
// Variables captured in different sibling closures should not be
|
|
434
|
+
// considered "outer" to each other
|
|
435
|
+
const source = `
|
|
436
|
+
|skill_name| {
|
|
437
|
+
"output" => $result
|
|
438
|
+
$result
|
|
439
|
+
} => $run_skill
|
|
440
|
+
|
|
441
|
+
|doc_path| {
|
|
442
|
+
^(limit: 5) 0 -> ($ < 3) @ {
|
|
443
|
+
"output" => $result
|
|
444
|
+
$ + 1
|
|
445
|
+
}
|
|
446
|
+
} => $review_loop
|
|
447
|
+
`;
|
|
448
|
+
|
|
449
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('does warn for variables in parent closure captured in nested loop', () => {
|
|
453
|
+
// Variable in parent closure should trigger warning when captured in nested loop
|
|
454
|
+
const source = `
|
|
455
|
+
|outer_param| {
|
|
456
|
+
0 => $count
|
|
457
|
+
[1, 2, 3] -> each {
|
|
458
|
+
$count + 1 => $count
|
|
459
|
+
}
|
|
460
|
+
} => $fn
|
|
461
|
+
`;
|
|
462
|
+
|
|
463
|
+
expect(hasViolations(source, config)).toBe(true);
|
|
464
|
+
const codes = getCodes(source, config);
|
|
465
|
+
expect(codes).toContain('LOOP_OUTER_CAPTURE');
|
|
466
|
+
});
|
|
467
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closure Convention Rules Tests
|
|
3
|
+
* Verify closure 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 closure rules enabled.
|
|
17
|
+
*/
|
|
18
|
+
function createConfig(rules: Record<string, 'on' | 'off'> = {}): CheckConfig {
|
|
19
|
+
return {
|
|
20
|
+
rules: {
|
|
21
|
+
CLOSURE_BARE_DOLLAR: 'on',
|
|
22
|
+
CLOSURE_BRACES: 'on',
|
|
23
|
+
CLOSURE_LATE_BINDING: '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
|
+
// CLOSURE_BARE_DOLLAR TESTS
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
describe('CLOSURE_BARE_DOLLAR', () => {
|
|
62
|
+
const config = createConfig({
|
|
63
|
+
CLOSURE_BRACES: 'off',
|
|
64
|
+
CLOSURE_LATE_BINDING: 'off',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('accepts closures with parameters', () => {
|
|
68
|
+
expect(hasViolations('|x|($x * 2) => $fn', config)).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('accepts closures without $ reference', () => {
|
|
72
|
+
expect(hasViolations('||{ 42 } => $fn', config)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('accepts inline blocks with bare $', () => {
|
|
76
|
+
// Inline blocks are not Closure nodes, they're immediate evaluation
|
|
77
|
+
expect(hasViolations('5 -> { $ * 2 }', config)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('warns on bare $ in zero-param stored closure', () => {
|
|
81
|
+
const source = '||{ $ * 2 } => $fn';
|
|
82
|
+
|
|
83
|
+
const messages = getDiagnostics(source, config);
|
|
84
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
85
|
+
expect(messages[0]).toContain('Bare $ in stored closure');
|
|
86
|
+
expect(messages[0]).toContain('ambiguous binding');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('suggests explicit capture', () => {
|
|
90
|
+
const source = '||{ $ + 5 } => $fn';
|
|
91
|
+
|
|
92
|
+
const messages = getDiagnostics(source, config);
|
|
93
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
94
|
+
expect(messages[0]).toContain('$ => $item');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('has correct severity and code', () => {
|
|
98
|
+
const source = '||{ $ } => $fn';
|
|
99
|
+
const ast = parse(source);
|
|
100
|
+
const diagnostics = validateScript(ast, source, config);
|
|
101
|
+
|
|
102
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
103
|
+
expect(diagnostics[0]?.code).toBe('CLOSURE_BARE_DOLLAR');
|
|
104
|
+
expect(diagnostics[0]?.severity).toBe('warning');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ============================================================
|
|
109
|
+
// CLOSURE_BRACES TESTS
|
|
110
|
+
// ============================================================
|
|
111
|
+
|
|
112
|
+
describe('CLOSURE_BRACES', () => {
|
|
113
|
+
const config = createConfig({
|
|
114
|
+
CLOSURE_BARE_DOLLAR: 'off',
|
|
115
|
+
CLOSURE_LATE_BINDING: 'off',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('accepts simple closure with parentheses', () => {
|
|
119
|
+
expect(hasViolations('|x|($x * 2) => $fn', config)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('accepts complex closure with braces', () => {
|
|
123
|
+
const source = `
|
|
124
|
+
|n| {
|
|
125
|
+
($n < 1) ? 1 ! ($n * 2)
|
|
126
|
+
} => $fn
|
|
127
|
+
`;
|
|
128
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('recommends braces for conditional in closure body', () => {
|
|
132
|
+
const source = '|n|(($n < 1) ? 1 ! ($n * 2)) => $fn';
|
|
133
|
+
|
|
134
|
+
const messages = getDiagnostics(source, config);
|
|
135
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
136
|
+
expect(messages[0]).toContain('braces for complex closure bodies');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('has correct severity and code', () => {
|
|
140
|
+
const source = '|x|(($x > 0) ? "pos" ! "neg") => $fn';
|
|
141
|
+
const ast = parse(source);
|
|
142
|
+
const diagnostics = validateScript(ast, source, config);
|
|
143
|
+
|
|
144
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
145
|
+
expect(diagnostics[0]?.code).toBe('CLOSURE_BRACES');
|
|
146
|
+
expect(diagnostics[0]?.severity).toBe('info');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ============================================================
|
|
151
|
+
// CLOSURE_LATE_BINDING TESTS
|
|
152
|
+
// ============================================================
|
|
153
|
+
|
|
154
|
+
describe('CLOSURE_LATE_BINDING', () => {
|
|
155
|
+
const config = createConfig({
|
|
156
|
+
CLOSURE_BARE_DOLLAR: 'off',
|
|
157
|
+
CLOSURE_BRACES: 'off',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('accepts each loops without closure creation', () => {
|
|
161
|
+
expect(hasViolations('[1, 2, 3] -> each { $ * 2 }', config)).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('accepts closures with explicit capture', () => {
|
|
165
|
+
const source = `
|
|
166
|
+
[1, 2, 3] -> each {
|
|
167
|
+
$ => $item
|
|
168
|
+
||{ $item }
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('warns on closure creation without explicit capture', () => {
|
|
175
|
+
const source = '[1, 2, 3] -> each { ||{ $ } }';
|
|
176
|
+
|
|
177
|
+
const messages = getDiagnostics(source, config);
|
|
178
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
179
|
+
expect(messages[0]).toContain('Capture loop variable explicitly');
|
|
180
|
+
expect(messages[0]).toContain('$ => $item');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('has correct severity and code', () => {
|
|
184
|
+
const source = '[1, 2, 3] -> each { ||{ $ } }';
|
|
185
|
+
const ast = parse(source);
|
|
186
|
+
const diagnostics = validateScript(ast, source, config);
|
|
187
|
+
|
|
188
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
189
|
+
expect(diagnostics[0]?.code).toBe('CLOSURE_LATE_BINDING');
|
|
190
|
+
expect(diagnostics[0]?.severity).toBe('warning');
|
|
191
|
+
});
|
|
192
|
+
});
|