@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,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
+ });