@rcrsr/rill-cli 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/check/config.d.ts +20 -0
  3. package/dist/check/config.d.ts.map +1 -0
  4. package/dist/check/config.js +151 -0
  5. package/dist/check/config.js.map +1 -0
  6. package/dist/check/fixer.d.ts +39 -0
  7. package/dist/check/fixer.d.ts.map +1 -0
  8. package/dist/check/fixer.js +119 -0
  9. package/dist/check/fixer.js.map +1 -0
  10. package/dist/check/index.d.ts +10 -0
  11. package/dist/check/index.d.ts.map +1 -0
  12. package/dist/check/index.js +21 -0
  13. package/dist/check/index.js.map +1 -0
  14. package/dist/check/rules/anti-patterns.d.ts +65 -0
  15. package/dist/check/rules/anti-patterns.d.ts.map +1 -0
  16. package/dist/check/rules/anti-patterns.js +481 -0
  17. package/dist/check/rules/anti-patterns.js.map +1 -0
  18. package/dist/check/rules/closures.d.ts +66 -0
  19. package/dist/check/rules/closures.d.ts.map +1 -0
  20. package/dist/check/rules/closures.js +370 -0
  21. package/dist/check/rules/closures.js.map +1 -0
  22. package/dist/check/rules/collections.d.ts +90 -0
  23. package/dist/check/rules/collections.d.ts.map +1 -0
  24. package/dist/check/rules/collections.js +373 -0
  25. package/dist/check/rules/collections.js.map +1 -0
  26. package/dist/check/rules/conditionals.d.ts +41 -0
  27. package/dist/check/rules/conditionals.d.ts.map +1 -0
  28. package/dist/check/rules/conditionals.js +134 -0
  29. package/dist/check/rules/conditionals.js.map +1 -0
  30. package/dist/check/rules/flow.d.ts +46 -0
  31. package/dist/check/rules/flow.d.ts.map +1 -0
  32. package/dist/check/rules/flow.js +206 -0
  33. package/dist/check/rules/flow.js.map +1 -0
  34. package/dist/check/rules/formatting.d.ts +143 -0
  35. package/dist/check/rules/formatting.d.ts.map +1 -0
  36. package/dist/check/rules/formatting.js +656 -0
  37. package/dist/check/rules/formatting.js.map +1 -0
  38. package/dist/check/rules/helpers.d.ts +26 -0
  39. package/dist/check/rules/helpers.d.ts.map +1 -0
  40. package/dist/check/rules/helpers.js +66 -0
  41. package/dist/check/rules/helpers.js.map +1 -0
  42. package/dist/check/rules/index.d.ts +21 -0
  43. package/dist/check/rules/index.d.ts.map +1 -0
  44. package/dist/check/rules/index.js +78 -0
  45. package/dist/check/rules/index.js.map +1 -0
  46. package/dist/check/rules/loops.d.ts +77 -0
  47. package/dist/check/rules/loops.d.ts.map +1 -0
  48. package/dist/check/rules/loops.js +310 -0
  49. package/dist/check/rules/loops.js.map +1 -0
  50. package/dist/check/rules/naming.d.ts +21 -0
  51. package/dist/check/rules/naming.d.ts.map +1 -0
  52. package/dist/check/rules/naming.js +174 -0
  53. package/dist/check/rules/naming.js.map +1 -0
  54. package/dist/check/rules/strings.d.ts +28 -0
  55. package/dist/check/rules/strings.d.ts.map +1 -0
  56. package/dist/check/rules/strings.js +79 -0
  57. package/dist/check/rules/strings.js.map +1 -0
  58. package/dist/check/rules/types.d.ts +41 -0
  59. package/dist/check/rules/types.d.ts.map +1 -0
  60. package/dist/check/rules/types.js +167 -0
  61. package/dist/check/rules/types.js.map +1 -0
  62. package/dist/check/types.d.ts +112 -0
  63. package/dist/check/types.d.ts.map +1 -0
  64. package/dist/check/types.js +6 -0
  65. package/dist/check/types.js.map +1 -0
  66. package/dist/check/validator.d.ts +18 -0
  67. package/dist/check/validator.d.ts.map +1 -0
  68. package/dist/check/validator.js +110 -0
  69. package/dist/check/validator.js.map +1 -0
  70. package/dist/check/visitor.d.ts +33 -0
  71. package/dist/check/visitor.d.ts.map +1 -0
  72. package/dist/check/visitor.js +259 -0
  73. package/dist/check/visitor.js.map +1 -0
  74. package/dist/cli-check.d.ts +43 -0
  75. package/dist/cli-check.d.ts.map +1 -0
  76. package/dist/cli-check.js +366 -0
  77. package/dist/cli-check.js.map +1 -0
  78. package/dist/cli-error-enrichment.d.ts +73 -0
  79. package/dist/cli-error-enrichment.d.ts.map +1 -0
  80. package/dist/cli-error-enrichment.js +205 -0
  81. package/dist/cli-error-enrichment.js.map +1 -0
  82. package/dist/cli-error-formatter.d.ts +45 -0
  83. package/dist/cli-error-formatter.d.ts.map +1 -0
  84. package/dist/cli-error-formatter.js +218 -0
  85. package/dist/cli-error-formatter.js.map +1 -0
  86. package/dist/cli-eval.d.ts +15 -0
  87. package/dist/cli-eval.d.ts.map +1 -0
  88. package/dist/cli-eval.js +116 -0
  89. package/dist/cli-eval.js.map +1 -0
  90. package/dist/cli-exec.d.ts +58 -0
  91. package/dist/cli-exec.d.ts.map +1 -0
  92. package/dist/cli-exec.js +326 -0
  93. package/dist/cli-exec.js.map +1 -0
  94. package/dist/cli-explain.d.ts +24 -0
  95. package/dist/cli-explain.d.ts.map +1 -0
  96. package/dist/cli-explain.js +68 -0
  97. package/dist/cli-explain.js.map +1 -0
  98. package/dist/cli-lsp-diagnostic.d.ts +35 -0
  99. package/dist/cli-lsp-diagnostic.d.ts.map +1 -0
  100. package/dist/cli-lsp-diagnostic.js +98 -0
  101. package/dist/cli-lsp-diagnostic.js.map +1 -0
  102. package/dist/cli-module-loader.d.ts +19 -0
  103. package/dist/cli-module-loader.d.ts.map +1 -0
  104. package/dist/cli-module-loader.js +83 -0
  105. package/dist/cli-module-loader.js.map +1 -0
  106. package/dist/cli-shared.d.ts +62 -0
  107. package/dist/cli-shared.d.ts.map +1 -0
  108. package/dist/cli-shared.js +158 -0
  109. package/dist/cli-shared.js.map +1 -0
  110. package/dist/cli.d.ts +13 -0
  111. package/dist/cli.d.ts.map +1 -0
  112. package/dist/cli.js +62 -0
  113. package/dist/cli.js.map +1 -0
  114. package/dist/test-internal-import.d.ts +2 -0
  115. package/dist/test-internal-import.d.ts.map +1 -0
  116. package/dist/test-internal-import.js +7 -0
  117. package/dist/test-internal-import.js.map +1 -0
  118. package/package.json +24 -0
  119. package/src/check/config.ts +202 -0
  120. package/src/check/fixer.ts +174 -0
  121. package/src/check/index.ts +39 -0
  122. package/src/check/rules/anti-patterns.ts +585 -0
  123. package/src/check/rules/closures.ts +445 -0
  124. package/src/check/rules/collections.ts +437 -0
  125. package/src/check/rules/conditionals.ts +155 -0
  126. package/src/check/rules/flow.ts +262 -0
  127. package/src/check/rules/formatting.ts +811 -0
  128. package/src/check/rules/helpers.ts +89 -0
  129. package/src/check/rules/index.ts +140 -0
  130. package/src/check/rules/loops.ts +372 -0
  131. package/src/check/rules/naming.ts +242 -0
  132. package/src/check/rules/strings.ts +104 -0
  133. package/src/check/rules/types.ts +214 -0
  134. package/src/check/types.ts +163 -0
  135. package/src/check/validator.ts +136 -0
  136. package/src/check/visitor.ts +338 -0
  137. package/src/cli-check.ts +456 -0
  138. package/src/cli-error-enrichment.ts +274 -0
  139. package/src/cli-error-formatter.ts +313 -0
  140. package/src/cli-eval.ts +145 -0
  141. package/src/cli-exec.ts +408 -0
  142. package/src/cli-explain.ts +76 -0
  143. package/src/cli-lsp-diagnostic.ts +132 -0
  144. package/src/cli-module-loader.ts +101 -0
  145. package/src/cli-shared.ts +187 -0
  146. package/tests/check/cli-check.test.ts +189 -0
  147. package/tests/check/config.test.ts +350 -0
  148. package/tests/check/fixer.test.ts +373 -0
  149. package/tests/check/format-diagnostics.test.ts +327 -0
  150. package/tests/check/rules/anti-patterns.test.ts +467 -0
  151. package/tests/check/rules/closures.test.ts +192 -0
  152. package/tests/check/rules/collections.test.ts +380 -0
  153. package/tests/check/rules/conditionals.test.ts +185 -0
  154. package/tests/check/rules/flow.test.ts +250 -0
  155. package/tests/check/rules/formatting.test.ts +755 -0
  156. package/tests/check/rules/loops.test.ts +334 -0
  157. package/tests/check/rules/naming.test.ts +336 -0
  158. package/tests/check/rules/strings.test.ts +129 -0
  159. package/tests/check/rules/types.test.ts +257 -0
  160. package/tests/check/validator.test.ts +444 -0
  161. package/tests/check/visitor.test.ts +171 -0
  162. package/tests/cli/check.test.ts +801 -0
  163. package/tests/cli/error-enrichment.test.ts +510 -0
  164. package/tests/cli/error-formatter.test.ts +631 -0
  165. package/tests/cli/eval.test.ts +85 -0
  166. package/tests/cli/exec.test.ts +537 -0
  167. package/tests/cli-explain.test.ts +249 -0
  168. package/tests/cli-lsp-diagnostic.test.ts +202 -0
  169. package/tests/cli-shared.test.ts +439 -0
  170. package/tsconfig.json +9 -0
  171. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,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
+ });