@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,437 @@
1
+ /**
2
+ * Collection Operator Rules
3
+ * Enforces conventions for each, map, fold, and filter operators.
4
+ */
5
+
6
+ import type {
7
+ ValidationRule,
8
+ Diagnostic,
9
+ ValidationContext,
10
+ } from '../types.js';
11
+ import type {
12
+ ASTNode,
13
+ EachExprNode,
14
+ MapExprNode,
15
+ FoldExprNode,
16
+ FilterExprNode,
17
+ IteratorBody,
18
+ BlockNode,
19
+ } from '@rcrsr/rill';
20
+ import { extractContextLine } from './helpers.js';
21
+
22
+ // ============================================================
23
+ // HELPER FUNCTIONS
24
+ // ============================================================
25
+
26
+ /**
27
+ * Check if an AST subtree contains a Break node.
28
+ * Recursively traverses all node types.
29
+ */
30
+ function containsBreak(node: ASTNode): boolean {
31
+ if (node.type === 'Break') {
32
+ return true;
33
+ }
34
+
35
+ // Recursively check children based on node type
36
+ switch (node.type) {
37
+ case 'Block':
38
+ return node.statements.some((stmt) => containsBreak(stmt));
39
+
40
+ case 'Statement':
41
+ return containsBreak(node.expression);
42
+
43
+ case 'AnnotatedStatement':
44
+ return containsBreak(node.statement);
45
+
46
+ case 'PipeChain':
47
+ if (containsBreak(node.head)) return true;
48
+ if (node.pipes.some((pipe) => containsBreak(pipe as ASTNode)))
49
+ return true;
50
+ if (node.terminator && node.terminator.type === 'Break') return true;
51
+ return false;
52
+
53
+ case 'PostfixExpr':
54
+ if (containsBreak(node.primary)) return true;
55
+ return node.methods.some((method) => containsBreak(method));
56
+
57
+ case 'BinaryExpr':
58
+ return containsBreak(node.left) || containsBreak(node.right);
59
+
60
+ case 'UnaryExpr':
61
+ return containsBreak(node.operand);
62
+
63
+ case 'GroupedExpr':
64
+ return containsBreak(node.expression);
65
+
66
+ case 'Conditional':
67
+ if (node.input && containsBreak(node.input)) return true;
68
+ if (node.condition && containsBreak(node.condition)) return true;
69
+ if (containsBreak(node.thenBranch)) return true;
70
+ if (node.elseBranch && containsBreak(node.elseBranch)) return true;
71
+ return false;
72
+
73
+ case 'WhileLoop':
74
+ case 'DoWhileLoop':
75
+ return containsBreak(node.body);
76
+
77
+ case 'Closure':
78
+ return containsBreak(node.body);
79
+
80
+ case 'EachExpr':
81
+ case 'MapExpr':
82
+ case 'FoldExpr':
83
+ case 'FilterExpr':
84
+ return containsBreak(node.body);
85
+
86
+ case 'HostCall':
87
+ case 'ClosureCall':
88
+ case 'MethodCall':
89
+ case 'Invoke':
90
+ case 'PipeInvoke':
91
+ return node.args.some((arg) => containsBreak(arg));
92
+
93
+ case 'StringLiteral':
94
+ return node.parts.some(
95
+ (part) => typeof part !== 'string' && containsBreak(part)
96
+ );
97
+
98
+ case 'Tuple':
99
+ return node.elements.some((elem) => containsBreak(elem));
100
+
101
+ case 'Dict':
102
+ return node.entries.some((entry) => containsBreak(entry));
103
+
104
+ case 'DictEntry':
105
+ return containsBreak(node.value);
106
+
107
+ default:
108
+ // Leaf nodes and other types don't contain breaks
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Check if a body is a simple method shorthand.
115
+ * Body structure for .method shorthand is PostfixExpr with MethodCall as primary.
116
+ * Examples: .upper, .len, .trim
117
+ */
118
+ function isMethodShorthand(body: IteratorBody): boolean {
119
+ if (body.type !== 'PostfixExpr') return false;
120
+ return body.primary.type === 'MethodCall';
121
+ }
122
+
123
+ /**
124
+ * Check if a body is a block wrapping a single method call on $.
125
+ * Example: { $.upper() } when it could be .upper
126
+ * Structure: Block -> Statement -> PipeChain -> PostfixExpr($) with methods
127
+ */
128
+ function isBlockWrappingMethod(
129
+ body: IteratorBody
130
+ ): body is BlockNode & { statements: Array<{ expression: ASTNode }> } {
131
+ if (body.type !== 'Block') return false;
132
+ if (body.statements.length !== 1) return false;
133
+
134
+ const stmt = body.statements[0];
135
+ if (!stmt || stmt.type !== 'Statement') return false;
136
+
137
+ const expr = stmt.expression;
138
+ if (expr.type !== 'PipeChain') return false;
139
+
140
+ // Should have no pipes (direct method call on head)
141
+ if (expr.pipes.length !== 0) return false;
142
+
143
+ const head = expr.head;
144
+ if (head.type !== 'PostfixExpr') return false;
145
+
146
+ // Primary should be pipe variable ($)
147
+ if (head.primary.type !== 'Variable') return false;
148
+ const variable = head.primary;
149
+ if (!('isPipeVar' in variable) || !variable.isPipeVar) return false;
150
+
151
+ // Should have exactly one method in the methods array
152
+ if (head.methods.length !== 1) return false;
153
+ if (head.methods[0]?.type !== 'MethodCall') return false;
154
+
155
+ return true;
156
+ }
157
+
158
+ /**
159
+ * Get method name from iterator body.
160
+ * Handles both PostfixExpr (shorthand) and BlockNode (wrapped) forms.
161
+ */
162
+ function getMethodName(body: IteratorBody): string | null {
163
+ // Shorthand form: PostfixExpr with MethodCall primary
164
+ if (body.type === 'PostfixExpr' && body.primary.type === 'MethodCall') {
165
+ return body.primary.name;
166
+ }
167
+
168
+ // Block form: $.method()
169
+ if (isBlockWrappingMethod(body)) {
170
+ const stmt = body.statements[0];
171
+ if (!stmt || stmt.type !== 'Statement') return null;
172
+
173
+ const expr = stmt.expression;
174
+ if (expr.type !== 'PipeChain') return null;
175
+
176
+ const head = expr.head;
177
+ if (head.type !== 'PostfixExpr') return null;
178
+
179
+ const method = head.methods[0];
180
+ if (method && method.type === 'MethodCall') {
181
+ return method.name;
182
+ }
183
+ }
184
+
185
+ return null;
186
+ }
187
+
188
+ // ============================================================
189
+ // BREAK_IN_PARALLEL RULE
190
+ // ============================================================
191
+
192
+ /**
193
+ * Validates that break is not used in parallel operators (map, filter).
194
+ *
195
+ * Break is semantically invalid in parallel execution contexts:
196
+ * - map: executes in parallel, no iteration order
197
+ * - filter: parallel predicate evaluation
198
+ *
199
+ * Break is valid in sequential operators:
200
+ * - each: sequential iteration with early termination
201
+ * - fold: sequential reduction (though uncommon)
202
+ *
203
+ * Error severity because this is semantically wrong, not just stylistic.
204
+ *
205
+ * References:
206
+ * - docs/guide-conventions.md:90-149
207
+ * - docs/topic-collections.md
208
+ */
209
+ export const BREAK_IN_PARALLEL: ValidationRule = {
210
+ code: 'BREAK_IN_PARALLEL',
211
+ category: 'collections',
212
+ severity: 'error',
213
+ nodeTypes: ['MapExpr', 'FilterExpr'],
214
+
215
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
216
+ const collectionExpr = node as MapExprNode | FilterExprNode;
217
+ const operatorName = node.type === 'MapExpr' ? 'map' : 'filter';
218
+
219
+ if (containsBreak(collectionExpr.body)) {
220
+ return [
221
+ {
222
+ location: node.span.start,
223
+ severity: 'error',
224
+ code: 'BREAK_IN_PARALLEL',
225
+ message: `Break not allowed in '${operatorName}' (parallel operator). Use 'each' for sequential iteration with break.`,
226
+ context: extractContextLine(node.span.start.line, context.source),
227
+ fix: null, // Cannot auto-fix operator replacement
228
+ },
229
+ ];
230
+ }
231
+
232
+ return [];
233
+ },
234
+ };
235
+
236
+ // ============================================================
237
+ // PREFER_MAP RULE
238
+ // ============================================================
239
+
240
+ /**
241
+ * Suggests using map over each when no side effects are present.
242
+ *
243
+ * Map is semantically clearer for pure transformations:
244
+ * - Signals no side effects (parallel execution)
245
+ * - Better performance potential
246
+ * - More functional style
247
+ *
248
+ * Detects each expressions where:
249
+ * - Body doesn't reference accumulator ($@)
250
+ * - No accumulator initialization
251
+ * - Body doesn't contain side-effecting operations (host calls, logging)
252
+ *
253
+ * This is informational - both work, but map is clearer for pure transforms.
254
+ *
255
+ * References:
256
+ * - docs/guide-conventions.md:90-149
257
+ */
258
+ export const PREFER_MAP: ValidationRule = {
259
+ code: 'PREFER_MAP',
260
+ category: 'collections',
261
+ severity: 'info',
262
+ nodeTypes: ['EachExpr'],
263
+
264
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
265
+ const eachExpr = node as EachExprNode;
266
+
267
+ // If accumulator is present, each is correct choice
268
+ if (eachExpr.accumulator !== null) {
269
+ return [];
270
+ }
271
+
272
+ // Check if body is a closure with accumulator parameter
273
+ if (eachExpr.body.type === 'Closure') {
274
+ const closure = eachExpr.body;
275
+ const hasAccumulator = closure.params.length > 1;
276
+ if (hasAccumulator) {
277
+ return [];
278
+ }
279
+ }
280
+
281
+ // Simple heuristic: if body is pure (no side effects), suggest map
282
+ // For now, suggest map for simple transformations
283
+ // Full implementation would check for host calls, logging, etc.
284
+
285
+ return [
286
+ {
287
+ location: node.span.start,
288
+ severity: 'info',
289
+ code: 'PREFER_MAP',
290
+ message:
291
+ "Consider using 'map' instead of 'each' for pure transformations (no side effects)",
292
+ context: extractContextLine(node.span.start.line, context.source),
293
+ fix: null, // Could generate fix by replacing 'each' with 'map'
294
+ },
295
+ ];
296
+ },
297
+ };
298
+
299
+ // ============================================================
300
+ // FOLD_INTERMEDIATES RULE
301
+ // ============================================================
302
+
303
+ /**
304
+ * Suggests using fold for final-only results, each(init) for running totals.
305
+ *
306
+ * Semantic distinction:
307
+ * - fold: returns final accumulated value only
308
+ * - each(init): returns list of all intermediate results
309
+ *
310
+ * Detects patterns that might benefit from one or the other:
311
+ * - fold used when intermediate results might be needed
312
+ * - each(init) used when only final result matters
313
+ *
314
+ * This is informational - helps users choose the right operator.
315
+ *
316
+ * References:
317
+ * - docs/guide-conventions.md:90-149
318
+ * - docs/topic-collections.md
319
+ */
320
+ export const FOLD_INTERMEDIATES: ValidationRule = {
321
+ code: 'FOLD_INTERMEDIATES',
322
+ category: 'collections',
323
+ severity: 'info',
324
+ nodeTypes: ['EachExpr', 'FoldExpr'],
325
+
326
+ validate(_node: ASTNode, _context: ValidationContext): Diagnostic[] {
327
+ // This rule is informational and would require flow analysis
328
+ // to detect whether intermediate values are used.
329
+ // Placeholder for future implementation.
330
+ return [];
331
+ },
332
+ };
333
+
334
+ // ============================================================
335
+ // FILTER_NEGATION RULE
336
+ // ============================================================
337
+
338
+ /**
339
+ * Validates that negation in filter uses grouped form.
340
+ *
341
+ * Grouped negation is clearer and prevents bugs:
342
+ * - Correct: filter (!.empty) -- grouped negation
343
+ * - Wrong: filter .empty -- filters for empty elements (likely bug)
344
+ *
345
+ * The ungrouped form .empty would return truthy elements,
346
+ * which is likely not intended when filtering.
347
+ *
348
+ * References:
349
+ * - docs/guide-conventions.md:90-149
350
+ */
351
+ export const FILTER_NEGATION: ValidationRule = {
352
+ code: 'FILTER_NEGATION',
353
+ category: 'collections',
354
+ severity: 'warning',
355
+ nodeTypes: ['FilterExpr'],
356
+
357
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
358
+ const filterExpr = node as FilterExprNode;
359
+ const body = filterExpr.body;
360
+
361
+ // Check if body is a simple method call (ungrouped)
362
+ if (isMethodShorthand(body)) {
363
+ const methodName = getMethodName(body);
364
+
365
+ // Check if method is likely a negation-intended method
366
+ // Common methods that might indicate user wants to negate:
367
+ // .empty, .is_match, etc.
368
+ if (methodName === 'empty') {
369
+ return [
370
+ {
371
+ location: node.span.start,
372
+ severity: 'warning',
373
+ code: 'FILTER_NEGATION',
374
+ message: `Filter with '.${methodName}' likely unintended. Use grouped negation: 'filter (!.${methodName})' to filter non-${methodName} elements`,
375
+ context: extractContextLine(node.span.start.line, context.source),
376
+ fix: null, // Could generate fix wrapping in (!...)
377
+ },
378
+ ];
379
+ }
380
+ }
381
+
382
+ return [];
383
+ },
384
+ };
385
+
386
+ // ============================================================
387
+ // METHOD_SHORTHAND RULE
388
+ // ============================================================
389
+
390
+ /**
391
+ * Suggests using method shorthand over block form in collection operators.
392
+ *
393
+ * Method shorthand is more concise and clearer:
394
+ * - Preferred: map .upper
395
+ * - Verbose: map { $.upper() }
396
+ *
397
+ * Detects block forms that wrap a single method call and suggests shorthand.
398
+ *
399
+ * This is informational - both forms work identically.
400
+ *
401
+ * References:
402
+ * - docs/guide-conventions.md:90-149
403
+ */
404
+ export const METHOD_SHORTHAND: ValidationRule = {
405
+ code: 'METHOD_SHORTHAND',
406
+ category: 'collections',
407
+ severity: 'info',
408
+ nodeTypes: ['EachExpr', 'MapExpr', 'FoldExpr', 'FilterExpr'],
409
+
410
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
411
+ const collectionNode = node as
412
+ | EachExprNode
413
+ | MapExprNode
414
+ | FoldExprNode
415
+ | FilterExprNode;
416
+ const body = collectionNode.body;
417
+
418
+ if (isBlockWrappingMethod(body)) {
419
+ const methodName = getMethodName(body);
420
+
421
+ if (methodName) {
422
+ return [
423
+ {
424
+ location: node.span.start,
425
+ severity: 'info',
426
+ code: 'METHOD_SHORTHAND',
427
+ message: `Prefer method shorthand '.${methodName}' over block form '{ $.${methodName}() }'`,
428
+ context: extractContextLine(node.span.start.line, context.source),
429
+ fix: null, // Could generate fix replacing block with .method
430
+ },
431
+ ];
432
+ }
433
+ }
434
+
435
+ return [];
436
+ },
437
+ };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Conditional Convention Rules
3
+ * Enforces conventions for conditional expressions.
4
+ */
5
+
6
+ import type {
7
+ ValidationRule,
8
+ Diagnostic,
9
+ ValidationContext,
10
+ } from '../types.js';
11
+ import type { ASTNode, ConditionalNode } from '@rcrsr/rill';
12
+ import { extractContextLine } from './helpers.js';
13
+
14
+ // ============================================================
15
+ // HELPER FUNCTIONS
16
+ // ============================================================
17
+
18
+ /**
19
+ * Check if a node tree contains an existence check (.?field).
20
+ */
21
+ function hasExistenceCheck(node: ASTNode): boolean {
22
+ if (!node || typeof node !== 'object') return false;
23
+
24
+ // Check if this node is a Variable with existenceCheck
25
+ if (
26
+ node.type === 'Variable' &&
27
+ 'existenceCheck' in node &&
28
+ node.existenceCheck !== null
29
+ ) {
30
+ return true;
31
+ }
32
+
33
+ // Recursively check child nodes
34
+ for (const key of Object.keys(node)) {
35
+ const value = (node as unknown as Record<string, unknown>)[key];
36
+ if (value && typeof value === 'object') {
37
+ if (Array.isArray(value)) {
38
+ for (const item of value) {
39
+ if (hasExistenceCheck(item as ASTNode)) return true;
40
+ }
41
+ } else {
42
+ if (hasExistenceCheck(value as ASTNode)) return true;
43
+ }
44
+ }
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Check if a conditional is using the ?? pattern with .? check.
52
+ * Pattern: $dict.?field ? $dict.field ! "default"
53
+ * This should be simplified to: $dict.field ?? "default"
54
+ */
55
+ function isVerboseDefaultPattern(node: ConditionalNode): boolean {
56
+ // Check if there's an else branch (required for default pattern)
57
+ if (!node.elseBranch) return false;
58
+
59
+ // Must have an explicit condition (not a piped truthy check)
60
+ if (!node.condition) return false;
61
+
62
+ // Check if condition contains an existence check (.?field)
63
+ if (!hasExistenceCheck(node.condition)) return false;
64
+
65
+ return true;
66
+ }
67
+
68
+ // ============================================================
69
+ // USE_DEFAULT_OPERATOR RULE
70
+ // ============================================================
71
+
72
+ /**
73
+ * Suggests using ?? for defaults instead of verbose conditionals.
74
+ *
75
+ * The ?? operator is more concise for providing default values:
76
+ *
77
+ * Good (concise default):
78
+ * $dict.field ?? "default"
79
+ *
80
+ * Avoid (verbose conditional):
81
+ * $dict.?field ? $dict.field ! "default"
82
+ *
83
+ * This is informational - both patterns work identically.
84
+ *
85
+ * References:
86
+ * - docs/guide-conventions.md:219-234
87
+ */
88
+ export const USE_DEFAULT_OPERATOR: ValidationRule = {
89
+ code: 'USE_DEFAULT_OPERATOR',
90
+ category: 'conditionals',
91
+ severity: 'info',
92
+ nodeTypes: ['Conditional'],
93
+
94
+ validate(node: ASTNode, context: ValidationContext): Diagnostic[] {
95
+ const conditional = node as ConditionalNode;
96
+
97
+ // Check for verbose default pattern
98
+ if (isVerboseDefaultPattern(conditional)) {
99
+ return [
100
+ {
101
+ location: node.span.start,
102
+ severity: 'info',
103
+ code: 'USE_DEFAULT_OPERATOR',
104
+ message:
105
+ 'Use ?? for defaults instead of conditionals: $dict.field ?? "default"',
106
+ context: extractContextLine(node.span.start.line, context.source),
107
+ fix: null, // Complex fix - requires AST restructuring
108
+ },
109
+ ];
110
+ }
111
+
112
+ return [];
113
+ },
114
+ };
115
+
116
+ // ============================================================
117
+ // CONDITION_TYPE RULE
118
+ // ============================================================
119
+
120
+ /**
121
+ * Validates that conditional conditions evaluate to boolean.
122
+ *
123
+ * Rill requires explicit boolean conditions in conditionals.
124
+ * The condition in `cond ? then ! else` must evaluate to boolean.
125
+ *
126
+ * Correct (boolean condition):
127
+ * "hello" -> .contains("ell") ? "found" ! "not found"
128
+ *
129
+ * Incorrect (non-boolean):
130
+ * "hello" ? "has value" ! "empty" # strings don't auto-convert to boolean
131
+ *
132
+ * This is a warning because it's likely a bug, not just stylistic.
133
+ *
134
+ * References:
135
+ * - docs/guide-conventions.md:199-215
136
+ */
137
+ export const CONDITION_TYPE: ValidationRule = {
138
+ code: 'CONDITION_TYPE',
139
+ category: 'conditionals',
140
+ severity: 'warning',
141
+ nodeTypes: ['Conditional'],
142
+
143
+ validate(_node: ASTNode, _context: ValidationContext): Diagnostic[] {
144
+ // Rill conditionals don't enforce boolean type checking at the static analysis level
145
+ // The language allows truthy/falsy semantics, and runtime will handle type errors
146
+ // This rule is disabled for now - the convention is informational only
147
+
148
+ // Note: If we wanted to enforce this, we would need to check:
149
+ // - When condition is null: input is the tested value (truthy check)
150
+ // - When condition exists: condition body must evaluate to boolean
151
+
152
+ // For now, return no diagnostics
153
+ return [];
154
+ },
155
+ };