@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,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Operator Rules Tests
|
|
3
|
+
* Verify conventions for each, map, fold, and filter operators.
|
|
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 specific rule enabled.
|
|
17
|
+
*/
|
|
18
|
+
function createConfig(ruleCode: string): CheckConfig {
|
|
19
|
+
return {
|
|
20
|
+
rules: { [ruleCode]: 'on' },
|
|
21
|
+
severity: {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate source and extract diagnostic messages.
|
|
27
|
+
*/
|
|
28
|
+
function getDiagnostics(source: string, ruleCode: string): string[] {
|
|
29
|
+
const ast = parse(source);
|
|
30
|
+
const diagnostics = validateScript(ast, source, createConfig(ruleCode));
|
|
31
|
+
return diagnostics.map((d) => d.message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate source and check for violations.
|
|
36
|
+
*/
|
|
37
|
+
function hasViolations(source: string, ruleCode: string): boolean {
|
|
38
|
+
const ast = parse(source);
|
|
39
|
+
const diagnostics = validateScript(ast, source, createConfig(ruleCode));
|
|
40
|
+
return diagnostics.length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate source and get severity levels.
|
|
45
|
+
*/
|
|
46
|
+
function getSeverities(source: string, ruleCode: string): string[] {
|
|
47
|
+
const ast = parse(source);
|
|
48
|
+
const diagnostics = validateScript(ast, source, createConfig(ruleCode));
|
|
49
|
+
return diagnostics.map((d) => d.severity);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================
|
|
53
|
+
// BREAK_IN_PARALLEL TESTS
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
describe('BREAK_IN_PARALLEL', () => {
|
|
57
|
+
const rule = 'BREAK_IN_PARALLEL';
|
|
58
|
+
|
|
59
|
+
it('detects break in map', () => {
|
|
60
|
+
const source = `
|
|
61
|
+
[1, 2, 3] -> map {
|
|
62
|
+
($ == 2) ? break
|
|
63
|
+
$ * 2
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
67
|
+
|
|
68
|
+
const messages = getDiagnostics(source, rule);
|
|
69
|
+
expect(messages[0]).toContain("Break not allowed in 'map'");
|
|
70
|
+
expect(messages[0]).toContain('parallel operator');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('detects break in filter', () => {
|
|
74
|
+
const source = `
|
|
75
|
+
[1, 2, 3] -> filter {
|
|
76
|
+
($ > 2) ? break
|
|
77
|
+
$ > 1
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
81
|
+
|
|
82
|
+
const messages = getDiagnostics(source, rule);
|
|
83
|
+
expect(messages[0]).toContain("Break not allowed in 'filter'");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('detects break in nested conditional within map', () => {
|
|
87
|
+
const source = `
|
|
88
|
+
[1, 2, 3] -> map {
|
|
89
|
+
($ == 2) ? {
|
|
90
|
+
break
|
|
91
|
+
} ! {
|
|
92
|
+
$ * 2
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('detects break in pipe chain terminator within map', () => {
|
|
100
|
+
const source = `
|
|
101
|
+
[1, 2, 3] -> map {
|
|
102
|
+
$ -> break
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('allows map without break', () => {
|
|
109
|
+
const source = `
|
|
110
|
+
[1, 2, 3] -> map { $ * 2 }
|
|
111
|
+
`;
|
|
112
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('allows filter without break', () => {
|
|
116
|
+
const source = `
|
|
117
|
+
[1, 2, 3] -> filter { $ > 1 }
|
|
118
|
+
`;
|
|
119
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns error severity', () => {
|
|
123
|
+
const source = `
|
|
124
|
+
[1, 2, 3] -> map {
|
|
125
|
+
($ == 2) ? break
|
|
126
|
+
$ * 2
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
const severities = getSeverities(source, rule);
|
|
130
|
+
expect(severities[0]).toBe('error');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ============================================================
|
|
135
|
+
// PREFER_MAP TESTS
|
|
136
|
+
// ============================================================
|
|
137
|
+
|
|
138
|
+
describe('PREFER_MAP', () => {
|
|
139
|
+
const rule = 'PREFER_MAP';
|
|
140
|
+
|
|
141
|
+
it('suggests map for each without accumulator', () => {
|
|
142
|
+
const source = `
|
|
143
|
+
[1, 2, 3] -> each { $ * 2 }
|
|
144
|
+
`;
|
|
145
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
146
|
+
|
|
147
|
+
const messages = getDiagnostics(source, rule);
|
|
148
|
+
expect(messages[0]).toContain("Consider using 'map'");
|
|
149
|
+
expect(messages[0]).toContain('pure transformations');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('allows each with accumulator initialization', () => {
|
|
153
|
+
const source = `
|
|
154
|
+
[1, 2, 3] -> each(0) { $@ + $ }
|
|
155
|
+
`;
|
|
156
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('allows each with closure having accumulator parameter', () => {
|
|
160
|
+
const source = `
|
|
161
|
+
[1, 2, 3] -> each |x, acc = 0| ($acc + $x)
|
|
162
|
+
`;
|
|
163
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns info severity', () => {
|
|
167
|
+
const source = `
|
|
168
|
+
[1, 2, 3] -> each { $ * 2 }
|
|
169
|
+
`;
|
|
170
|
+
const severities = getSeverities(source, rule);
|
|
171
|
+
expect(severities[0]).toBe('info');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ============================================================
|
|
176
|
+
// FOLD_INTERMEDIATES TESTS
|
|
177
|
+
// ============================================================
|
|
178
|
+
|
|
179
|
+
describe('FOLD_INTERMEDIATES', () => {
|
|
180
|
+
const rule = 'FOLD_INTERMEDIATES';
|
|
181
|
+
|
|
182
|
+
it('is a placeholder rule (no violations yet)', () => {
|
|
183
|
+
const source = `
|
|
184
|
+
[1, 2, 3] -> fold(0) { $@ + $ }
|
|
185
|
+
`;
|
|
186
|
+
// Placeholder - no implementation yet
|
|
187
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('does not flag each with accumulator', () => {
|
|
191
|
+
const source = `
|
|
192
|
+
[1, 2, 3] -> each(0) { $@ + $ }
|
|
193
|
+
`;
|
|
194
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ============================================================
|
|
199
|
+
// FILTER_NEGATION TESTS
|
|
200
|
+
// ============================================================
|
|
201
|
+
|
|
202
|
+
describe('FILTER_NEGATION', () => {
|
|
203
|
+
const rule = 'FILTER_NEGATION';
|
|
204
|
+
|
|
205
|
+
it('warns about filter with .empty method (likely unintended)', () => {
|
|
206
|
+
const source = `
|
|
207
|
+
["", "a", "b"] -> filter .empty
|
|
208
|
+
`;
|
|
209
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
210
|
+
|
|
211
|
+
const messages = getDiagnostics(source, rule);
|
|
212
|
+
expect(messages[0]).toContain("Filter with '.empty' likely unintended");
|
|
213
|
+
expect(messages[0]).toContain('filter (!.empty)');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('allows filter with other methods', () => {
|
|
217
|
+
const source = `
|
|
218
|
+
["a", "b", "c"] -> filter .upper
|
|
219
|
+
`;
|
|
220
|
+
// Only .empty triggers warning for now
|
|
221
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('allows filter with grouped negation', () => {
|
|
225
|
+
const source = `
|
|
226
|
+
["", "a", "b"] -> filter (!.empty)
|
|
227
|
+
`;
|
|
228
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('allows filter with block containing complex logic', () => {
|
|
232
|
+
const source = `
|
|
233
|
+
["", "a", "b"] -> filter { !$.empty }
|
|
234
|
+
`;
|
|
235
|
+
// Block form doesn't trigger shorthand warning
|
|
236
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns warning severity', () => {
|
|
240
|
+
const source = `
|
|
241
|
+
["", "a", "b"] -> filter .empty
|
|
242
|
+
`;
|
|
243
|
+
const severities = getSeverities(source, rule);
|
|
244
|
+
expect(severities[0]).toBe('warning');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ============================================================
|
|
249
|
+
// METHOD_SHORTHAND TESTS
|
|
250
|
+
// ============================================================
|
|
251
|
+
|
|
252
|
+
describe('METHOD_SHORTHAND', () => {
|
|
253
|
+
const rule = 'METHOD_SHORTHAND';
|
|
254
|
+
|
|
255
|
+
it('suggests shorthand for map with block wrapping method', () => {
|
|
256
|
+
const source = `
|
|
257
|
+
["hello", "world"] -> map { $.upper() }
|
|
258
|
+
`;
|
|
259
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
260
|
+
|
|
261
|
+
const messages = getDiagnostics(source, rule);
|
|
262
|
+
expect(messages[0]).toContain("Prefer method shorthand '.upper'");
|
|
263
|
+
expect(messages[0]).toContain('{ $.upper() }');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('suggests shorthand for each with block wrapping method', () => {
|
|
267
|
+
const source = `
|
|
268
|
+
[1, 2, 3] -> each { $.str() }
|
|
269
|
+
`;
|
|
270
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
271
|
+
|
|
272
|
+
const messages = getDiagnostics(source, rule);
|
|
273
|
+
expect(messages[0]).toContain("Prefer method shorthand '.str'");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('suggests shorthand for filter with block wrapping method', () => {
|
|
277
|
+
const source = `
|
|
278
|
+
["", "a", "b"] -> filter { $.empty() }
|
|
279
|
+
`;
|
|
280
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('suggests shorthand for fold with block wrapping method', () => {
|
|
284
|
+
const source = `
|
|
285
|
+
["a", "b"] -> fold("") { $.upper() }
|
|
286
|
+
`;
|
|
287
|
+
// Note: This detects the pattern even though fold typically needs
|
|
288
|
+
// accumulator logic. This is a valid detection - if user writes
|
|
289
|
+
// { $.upper() }, the suggestion to use .upper shorthand is correct.
|
|
290
|
+
expect(hasViolations(source, rule)).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('allows direct method shorthand', () => {
|
|
294
|
+
const source = `
|
|
295
|
+
["hello", "world"] -> map .upper
|
|
296
|
+
`;
|
|
297
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('allows blocks with complex logic', () => {
|
|
301
|
+
const source = `
|
|
302
|
+
[1, 2, 3] -> map {
|
|
303
|
+
($ > 2) ? "big" ! "small"
|
|
304
|
+
}
|
|
305
|
+
`;
|
|
306
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('allows blocks with multiple statements', () => {
|
|
310
|
+
const source = `
|
|
311
|
+
[1, 2, 3] -> map {
|
|
312
|
+
$ * 2 => $doubled
|
|
313
|
+
$doubled + 1
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
expect(hasViolations(source, rule)).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('returns info severity', () => {
|
|
320
|
+
const source = `
|
|
321
|
+
["hello"] -> map { $.upper() }
|
|
322
|
+
`;
|
|
323
|
+
const severities = getSeverities(source, rule);
|
|
324
|
+
expect(severities[0]).toBe('info');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ============================================================
|
|
329
|
+
// INTEGRATION TESTS
|
|
330
|
+
// ============================================================
|
|
331
|
+
|
|
332
|
+
describe('Collection Rules Integration', () => {
|
|
333
|
+
it('detects multiple violations in same script', () => {
|
|
334
|
+
const source = `
|
|
335
|
+
[1, 2, 3] -> each { $ * 2 }
|
|
336
|
+
[1, 2, 3] -> map {
|
|
337
|
+
($ == 2) ? break
|
|
338
|
+
$ * 2
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
|
|
342
|
+
const ast = parse(source);
|
|
343
|
+
const config: CheckConfig = {
|
|
344
|
+
rules: {
|
|
345
|
+
PREFER_MAP: 'on',
|
|
346
|
+
BREAK_IN_PARALLEL: 'on',
|
|
347
|
+
},
|
|
348
|
+
severity: {},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const diagnostics = validateScript(ast, source, config);
|
|
352
|
+
expect(diagnostics.length).toBeGreaterThanOrEqual(2);
|
|
353
|
+
|
|
354
|
+
const codes = diagnostics.map((d) => d.code);
|
|
355
|
+
expect(codes).toContain('PREFER_MAP');
|
|
356
|
+
expect(codes).toContain('BREAK_IN_PARALLEL');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('respects rule configuration', () => {
|
|
360
|
+
const source = `
|
|
361
|
+
[1, 2, 3] -> each { $ * 2 }
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
// Rule off
|
|
365
|
+
const configOff: CheckConfig = {
|
|
366
|
+
rules: { PREFER_MAP: 'off' },
|
|
367
|
+
severity: {},
|
|
368
|
+
};
|
|
369
|
+
const diagnosticsOff = validateScript(parse(source), source, configOff);
|
|
370
|
+
expect(diagnosticsOff.length).toBe(0);
|
|
371
|
+
|
|
372
|
+
// Rule on
|
|
373
|
+
const configOn: CheckConfig = {
|
|
374
|
+
rules: { PREFER_MAP: 'on' },
|
|
375
|
+
severity: {},
|
|
376
|
+
};
|
|
377
|
+
const diagnosticsOn = validateScript(parse(source), source, configOn);
|
|
378
|
+
expect(diagnosticsOn.length).toBeGreaterThan(0);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional Convention Rules Tests
|
|
3
|
+
* Verify conditional 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 conditional rules enabled.
|
|
17
|
+
*/
|
|
18
|
+
function createConfig(rules: Record<string, 'on' | 'off'> = {}): CheckConfig {
|
|
19
|
+
return {
|
|
20
|
+
rules: {
|
|
21
|
+
USE_DEFAULT_OPERATOR: 'on',
|
|
22
|
+
CONDITION_TYPE: 'on',
|
|
23
|
+
...rules,
|
|
24
|
+
},
|
|
25
|
+
severity: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate source and extract diagnostic messages.
|
|
31
|
+
*/
|
|
32
|
+
function getDiagnostics(source: string, config?: CheckConfig): string[] {
|
|
33
|
+
const ast = parse(source);
|
|
34
|
+
const diagnostics = validateScript(ast, source, config ?? createConfig());
|
|
35
|
+
return diagnostics.map((d) => d.message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate source and check for violations.
|
|
40
|
+
*/
|
|
41
|
+
function hasViolations(source: string, config?: CheckConfig): boolean {
|
|
42
|
+
const ast = parse(source);
|
|
43
|
+
const diagnostics = validateScript(ast, source, config ?? createConfig());
|
|
44
|
+
return diagnostics.length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate source and get diagnostic codes.
|
|
49
|
+
*/
|
|
50
|
+
function getCodes(source: string, config?: CheckConfig): string[] {
|
|
51
|
+
const ast = parse(source);
|
|
52
|
+
const diagnostics = validateScript(ast, source, config ?? createConfig());
|
|
53
|
+
return diagnostics.map((d) => d.code);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// USE_DEFAULT_OPERATOR TESTS
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
describe('USE_DEFAULT_OPERATOR', () => {
|
|
61
|
+
const config = createConfig({ CONDITION_TYPE: 'off' });
|
|
62
|
+
|
|
63
|
+
it('accepts ?? for default values', () => {
|
|
64
|
+
expect(hasViolations('$dict.field ?? "default"', config)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('accepts simple conditionals without .? check', () => {
|
|
68
|
+
expect(hasViolations('$x > 0 ? "positive" ! "negative"', config)).toBe(
|
|
69
|
+
false
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('detects verbose default pattern with .? check', () => {
|
|
74
|
+
const source = '$data.?name ? $data.name ! "unknown"';
|
|
75
|
+
|
|
76
|
+
const messages = getDiagnostics(source, config);
|
|
77
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
78
|
+
expect(messages[0]).toContain('Use ?? for defaults');
|
|
79
|
+
expect(messages[0]).toContain('$dict.field ?? "default"');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('accepts conditional without else branch', () => {
|
|
83
|
+
expect(hasViolations('$data.?field ? "exists"', config)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('accepts negated conditionals without existence check', () => {
|
|
87
|
+
// Pattern: value -> ! { body } is a negated conditional, not a default pattern
|
|
88
|
+
expect(
|
|
89
|
+
hasViolations(
|
|
90
|
+
'ccr::file_exists($path) -> ! { ccr::error("...") }',
|
|
91
|
+
config
|
|
92
|
+
)
|
|
93
|
+
).toBe(false);
|
|
94
|
+
expect(hasViolations('true -> ! { "not true" }', config)).toBe(false);
|
|
95
|
+
expect(hasViolations('$ready -> ! { "not ready" }', config)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('has correct severity and code', () => {
|
|
99
|
+
const source = '$data.?field ? $data.field ! "default"';
|
|
100
|
+
const ast = parse(source);
|
|
101
|
+
const diagnostics = validateScript(ast, source, config);
|
|
102
|
+
|
|
103
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
104
|
+
expect(diagnostics[0]?.code).toBe('USE_DEFAULT_OPERATOR');
|
|
105
|
+
expect(diagnostics[0]?.severity).toBe('info');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ============================================================
|
|
110
|
+
// CONDITION_TYPE TESTS
|
|
111
|
+
// ============================================================
|
|
112
|
+
|
|
113
|
+
describe('CONDITION_TYPE', () => {
|
|
114
|
+
const config = createConfig({ USE_DEFAULT_OPERATOR: 'off' });
|
|
115
|
+
|
|
116
|
+
it('is currently disabled - accepts all conditionals', () => {
|
|
117
|
+
// Note: This rule is currently disabled because Rill allows truthy/falsy
|
|
118
|
+
// semantics in conditionals, and runtime handles type checking.
|
|
119
|
+
// These tests verify the rule doesn't produce false positives.
|
|
120
|
+
|
|
121
|
+
expect(hasViolations('true ? "yes" ! "no"', config)).toBe(false);
|
|
122
|
+
expect(hasViolations('false ? "yes" ! "no"', config)).toBe(false);
|
|
123
|
+
expect(hasViolations('$x > 0 ? "positive" ! "negative"', config)).toBe(
|
|
124
|
+
false
|
|
125
|
+
);
|
|
126
|
+
expect(hasViolations('$a == $b ? "equal" ! "different"', config)).toBe(
|
|
127
|
+
false
|
|
128
|
+
);
|
|
129
|
+
expect(hasViolations('$a && $b ? "both" ! "not both"', config)).toBe(false);
|
|
130
|
+
expect(hasViolations('!$ready ? "not ready" ! "ready"', config)).toBe(
|
|
131
|
+
false
|
|
132
|
+
);
|
|
133
|
+
expect(
|
|
134
|
+
hasViolations(
|
|
135
|
+
'"hello" -> .contains("ell") ? "found" ! "not found"',
|
|
136
|
+
config
|
|
137
|
+
)
|
|
138
|
+
).toBe(false);
|
|
139
|
+
expect(
|
|
140
|
+
hasViolations('$val -> :?string ? "is string" ! "not string"', config)
|
|
141
|
+
).toBe(false);
|
|
142
|
+
expect(hasViolations('$data.?field ? "exists" ! "missing"', config)).toBe(
|
|
143
|
+
false
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Currently these don't trigger warnings (rule disabled)
|
|
147
|
+
expect(hasViolations('"hello" ? "has value" ! "empty"', config)).toBe(
|
|
148
|
+
false
|
|
149
|
+
);
|
|
150
|
+
expect(hasViolations('42 ? "yes" ! "no"', config)).toBe(false);
|
|
151
|
+
expect(hasViolations('$name ? "has name" ! "no name"', config)).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ============================================================
|
|
156
|
+
// INTEGRATION TESTS
|
|
157
|
+
// ============================================================
|
|
158
|
+
|
|
159
|
+
describe('Conditional rules integration', () => {
|
|
160
|
+
it('can detect multiple violations in same code', () => {
|
|
161
|
+
const source = '$data.?field ? $data.field ! "default"';
|
|
162
|
+
|
|
163
|
+
const codes = getCodes(source);
|
|
164
|
+
// Should detect USE_DEFAULT_OPERATOR
|
|
165
|
+
expect(codes).toContain('USE_DEFAULT_OPERATOR');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('respects rule configuration', () => {
|
|
169
|
+
const source = '$data.?field ? $data.field ! "default"';
|
|
170
|
+
|
|
171
|
+
// With USE_DEFAULT_OPERATOR on
|
|
172
|
+
const withRule = createConfig({ USE_DEFAULT_OPERATOR: 'on' });
|
|
173
|
+
expect(hasViolations(source, withRule)).toBe(true);
|
|
174
|
+
|
|
175
|
+
// With USE_DEFAULT_OPERATOR off
|
|
176
|
+
const withoutRule = createConfig({ USE_DEFAULT_OPERATOR: 'off' });
|
|
177
|
+
expect(hasViolations(source, withoutRule)).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('accepts all conditional forms (CONDITION_TYPE disabled)', () => {
|
|
181
|
+
const config = createConfig({ USE_DEFAULT_OPERATOR: 'off' });
|
|
182
|
+
const source = '($x > 0 && $y < 10) || $z == 5 ? "yes" ! "no"';
|
|
183
|
+
expect(hasViolations(source, config)).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|