@rcrsr/rill-cli 0.17.0 → 0.18.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.
@@ -62,3 +62,23 @@ export declare const COMPLEX_CONDITION: ValidationRule;
62
62
  * - docs/topic-variables.md (Scope Rules)
63
63
  */
64
64
  export declare const LOOP_OUTER_CAPTURE: ValidationRule;
65
+ /**
66
+ * Warns when a stream variable is invoked before any iteration consumes it.
67
+ * Invoking a stream closure ($s()) before iterating ($s -> each { ... })
68
+ * consumes chunks internally, leaving no data for iteration.
69
+ *
70
+ * Detection:
71
+ * - Tracks variables captured from stream closures (returnTypeTarget = stream)
72
+ * or captured with explicit :stream type annotation
73
+ * - Records first invocation (ClosureCall) and first iteration (each/map/filter/fold)
74
+ * - Warns when invocation precedes iteration in source order
75
+ *
76
+ * No warning if:
77
+ * - Iteration appears before invocation
78
+ * - Variable is only iterated (never invoked before iteration)
79
+ * - Variable is only invoked (no iteration to conflict)
80
+ *
81
+ * References:
82
+ * - IR-15: Lint Warning for Pre-Iteration Invocation
83
+ */
84
+ export declare const STREAM_PRE_ITERATION: ValidationRule;
@@ -3,6 +3,7 @@
3
3
  * Enforces best practices from docs/guide-conventions.md:411-462.
4
4
  */
5
5
  import { extractContextLine } from './helpers.js';
6
+ import { visitNode } from '../visitor.js';
6
7
  // ============================================================
7
8
  // AVOID_REASSIGNMENT RULE
8
9
  // ============================================================
@@ -342,77 +343,30 @@ function isVariableInParentScope(variableScope, currentClosureScope, scopeStack)
342
343
  return variableIndex < currentIndex;
343
344
  }
344
345
  /**
345
- * Recursively find all Capture nodes in a loop body.
346
+ * Find all Capture nodes in a loop body, excluding closures.
347
+ * Uses visitNode for full AST traversal with closure depth tracking
348
+ * to skip captures inside nested closures (they have their own scope).
346
349
  */
347
350
  function findCapturesInBody(node) {
348
351
  const captures = [];
349
- function traverse(n) {
350
- if (n.type === 'Capture') {
351
- captures.push(n);
352
- return;
353
- }
354
- // Traverse children based on node type
355
- switch (n.type) {
356
- case 'Block':
357
- for (const stmt of n.statements)
358
- traverse(stmt);
359
- break;
360
- case 'Statement':
361
- traverse(n.expression);
362
- break;
363
- case 'AnnotatedStatement':
364
- traverse(n.statement);
365
- break;
366
- case 'PipeChain':
367
- traverse(n.head);
368
- for (const pipe of n.pipes)
369
- traverse(pipe);
370
- if (n.terminator)
371
- traverse(n.terminator);
372
- break;
373
- case 'PostfixExpr':
374
- traverse(n.primary);
375
- for (const method of n.methods)
376
- traverse(method);
377
- break;
378
- case 'BinaryExpr':
379
- traverse(n.left);
380
- traverse(n.right);
381
- break;
382
- case 'UnaryExpr':
383
- traverse(n.operand);
384
- break;
385
- case 'GroupedExpr':
386
- traverse(n.expression);
387
- break;
388
- case 'Conditional':
389
- if (n.input)
390
- traverse(n.input);
391
- if (n.condition)
392
- traverse(n.condition);
393
- traverse(n.thenBranch);
394
- if (n.elseBranch)
395
- traverse(n.elseBranch);
396
- break;
397
- case 'Closure':
398
- // Don't traverse into closures - they have their own scope
399
- break;
400
- // Nested loops - traverse their bodies too
401
- case 'WhileLoop':
402
- traverse(n.body);
403
- break;
404
- case 'DoWhileLoop':
405
- traverse(n.body);
406
- break;
407
- case 'EachExpr':
408
- case 'MapExpr':
409
- case 'FilterExpr':
410
- case 'FoldExpr':
411
- traverse(n.body);
412
- break;
413
- }
414
- }
415
- traverse(node);
352
+ let closureDepth = 0;
353
+ const ctx = {};
354
+ visitNode(node, ctx, {
355
+ enter(n) {
356
+ if (n.type === 'Closure') {
357
+ closureDepth++;
358
+ return;
359
+ }
360
+ if (n.type === 'Capture' && closureDepth === 0) {
361
+ captures.push(n);
362
+ }
363
+ },
364
+ exit(n) {
365
+ if (n.type === 'Closure') {
366
+ closureDepth--;
367
+ }
368
+ },
369
+ });
416
370
  return captures;
417
371
  }
418
372
  /**
@@ -478,3 +432,206 @@ function getParenNestingDepth(node) {
478
432
  traverse(node, 0);
479
433
  return maxDepth;
480
434
  }
435
+ // ============================================================
436
+ // STREAM_PRE_ITERATION RULE
437
+ // ============================================================
438
+ /** Collection operator node types that consume a stream via iteration */
439
+ const ITERATION_NODE_TYPES = new Set([
440
+ 'EachExpr',
441
+ 'MapExpr',
442
+ 'FilterExpr',
443
+ 'FoldExpr',
444
+ ]);
445
+ /**
446
+ * Warns when a stream variable is invoked before any iteration consumes it.
447
+ * Invoking a stream closure ($s()) before iterating ($s -> each { ... })
448
+ * consumes chunks internally, leaving no data for iteration.
449
+ *
450
+ * Detection:
451
+ * - Tracks variables captured from stream closures (returnTypeTarget = stream)
452
+ * or captured with explicit :stream type annotation
453
+ * - Records first invocation (ClosureCall) and first iteration (each/map/filter/fold)
454
+ * - Warns when invocation precedes iteration in source order
455
+ *
456
+ * No warning if:
457
+ * - Iteration appears before invocation
458
+ * - Variable is only iterated (never invoked before iteration)
459
+ * - Variable is only invoked (no iteration to conflict)
460
+ *
461
+ * References:
462
+ * - IR-15: Lint Warning for Pre-Iteration Invocation
463
+ */
464
+ export const STREAM_PRE_ITERATION = {
465
+ code: 'STREAM_PRE_ITERATION',
466
+ category: 'anti-patterns',
467
+ severity: 'warning',
468
+ nodeTypes: ['Script'],
469
+ validate(node, context) {
470
+ const scriptNode = node;
471
+ const diagnostics = [];
472
+ // Phase 1: Collect stream variable names from capture sites
473
+ const streamVars = new Set();
474
+ collectStreamVariables(scriptNode, streamVars);
475
+ if (streamVars.size === 0) {
476
+ return diagnostics;
477
+ }
478
+ // Phase 2: Find first invocation and first iteration for each stream variable
479
+ const firstInvocation = new Map();
480
+ const firstIteration = new Map();
481
+ collectStreamUsages(scriptNode, streamVars, firstInvocation, firstIteration);
482
+ // Phase 3: Emit warning for each variable invoked before iteration
483
+ for (const varName of streamVars) {
484
+ const invocation = firstInvocation.get(varName);
485
+ const iteration = firstIteration.get(varName);
486
+ if (!invocation) {
487
+ continue;
488
+ }
489
+ // Warn if invoked and no iteration exists, or invocation precedes iteration
490
+ const invokedBeforeIteration = !iteration ||
491
+ invocation.span.start.line < iteration.span.start.line ||
492
+ (invocation.span.start.line === iteration.span.start.line &&
493
+ invocation.span.start.column < iteration.span.start.column);
494
+ if (invokedBeforeIteration) {
495
+ diagnostics.push({
496
+ location: invocation.span.start,
497
+ severity: 'warning',
498
+ code: 'STREAM_PRE_ITERATION',
499
+ message: `Stream invoked before iteration; chunks consumed internally. '$${varName}' at line ${invocation.span.start.line}`,
500
+ context: extractContextLine(invocation.span.start.line, context.source),
501
+ fix: null,
502
+ });
503
+ }
504
+ }
505
+ return diagnostics;
506
+ },
507
+ };
508
+ /**
509
+ * Collect variable names captured from stream closures or with :stream type.
510
+ * Traverses AST to find PipeChains containing a Capture in their pipes where:
511
+ * - The capture has typeRef with typeName 'stream'
512
+ * - The PipeChain head is a stream-returning closure
513
+ *
514
+ * Note: Capture nodes appear in PipeChain.pipes (not .terminator).
515
+ * Only Break/Return/Yield use the terminator slot.
516
+ */
517
+ function collectStreamVariables(node, streamVars) {
518
+ const ctx = {};
519
+ visitNode(node, ctx, {
520
+ enter(n) {
521
+ if (n.type !== 'PipeChain')
522
+ return;
523
+ const chain = n;
524
+ // Find the last Capture in the pipes array
525
+ const capture = findTrailingCapture(chain);
526
+ if (!capture)
527
+ return;
528
+ const varName = capture.name;
529
+ // Check 1: Capture with explicit :stream type annotation
530
+ if (capture.typeRef &&
531
+ capture.typeRef.kind === 'static' &&
532
+ capture.typeRef.typeName === 'stream') {
533
+ streamVars.add(varName);
534
+ return;
535
+ }
536
+ // Check 2: Head is a closure with stream return type
537
+ if (isStreamClosure(chain)) {
538
+ streamVars.add(varName);
539
+ }
540
+ },
541
+ exit() { },
542
+ });
543
+ }
544
+ /**
545
+ * Find the trailing Capture node in a PipeChain's pipes array.
546
+ * Returns null if the last pipe element is not a Capture.
547
+ */
548
+ function findTrailingCapture(chain) {
549
+ const lastPipe = chain.pipes[chain.pipes.length - 1];
550
+ if (lastPipe && lastPipe.type === 'Capture') {
551
+ return lastPipe;
552
+ }
553
+ return null;
554
+ }
555
+ /**
556
+ * Check if a PipeChain's head (or piped result) is a stream-returning closure.
557
+ * Detects closures with returnTypeTarget of stream type.
558
+ */
559
+ function isStreamClosure(chain) {
560
+ const head = chain.head;
561
+ if (head.type !== 'PostfixExpr')
562
+ return false;
563
+ const postfix = head;
564
+ if (postfix.primary.type !== 'Closure')
565
+ return false;
566
+ const closure = postfix.primary;
567
+ const returnType = closure.returnTypeTarget;
568
+ if (!returnType)
569
+ return false;
570
+ // TypeConstructorNode: :stream(), :stream(number), :stream(number):string
571
+ if ('type' in returnType && returnType.type === 'TypeConstructor') {
572
+ return returnType.constructorName === 'stream';
573
+ }
574
+ // TypeRef: :stream (simple form)
575
+ if ('kind' in returnType && returnType.kind === 'static') {
576
+ return returnType.typeName === 'stream';
577
+ }
578
+ return false;
579
+ }
580
+ /**
581
+ * Collect first invocation and first iteration sites for stream variables.
582
+ * Traverses AST in source order, recording only the first occurrence of each.
583
+ */
584
+ function collectStreamUsages(node, streamVars, firstInvocation, firstIteration) {
585
+ const ctx = {};
586
+ visitNode(node, ctx, {
587
+ enter(n) {
588
+ // Detect invocation: $s() as ClosureCall
589
+ if (n.type === 'ClosureCall') {
590
+ const call = n;
591
+ if (streamVars.has(call.name) &&
592
+ call.accessChain.length === 0 &&
593
+ !firstInvocation.has(call.name)) {
594
+ firstInvocation.set(call.name, call);
595
+ }
596
+ return;
597
+ }
598
+ // Detect iteration: $s -> each/map/filter/fold
599
+ if (n.type === 'PipeChain') {
600
+ const chain = n;
601
+ const varName = getPipeHeadVariableName(chain);
602
+ if (varName !== null &&
603
+ streamVars.has(varName) &&
604
+ !firstIteration.has(varName)) {
605
+ // Check if any pipe target is an iteration operator
606
+ for (const pipe of chain.pipes) {
607
+ if (ITERATION_NODE_TYPES.has(pipe.type)) {
608
+ firstIteration.set(varName, pipe);
609
+ break;
610
+ }
611
+ }
612
+ }
613
+ }
614
+ },
615
+ exit() { },
616
+ });
617
+ }
618
+ /**
619
+ * Extract the variable name from a PipeChain head if it's a simple variable reference.
620
+ * Returns null for complex heads (binary expressions, host calls, etc.).
621
+ */
622
+ function getPipeHeadVariableName(chain) {
623
+ const head = chain.head;
624
+ if (head.type !== 'PostfixExpr')
625
+ return null;
626
+ const postfix = head;
627
+ if (postfix.primary.type !== 'Variable')
628
+ return null;
629
+ if (postfix.methods.length > 0)
630
+ return null;
631
+ const variable = postfix.primary;
632
+ if (variable.isPipeVar || variable.name === null)
633
+ return null;
634
+ if (variable.accessChain.length > 0)
635
+ return null;
636
+ return variable.name;
637
+ }
@@ -3,6 +3,7 @@
3
3
  * Enforces closure best practices from docs/guide-conventions.md:237-286.
4
4
  */
5
5
  import { extractContextLine } from './helpers.js';
6
+ import { visitNode } from '../visitor.js';
6
7
  // ============================================================
7
8
  // CLOSURE_BARE_DOLLAR RULE
8
9
  // ============================================================
@@ -54,118 +55,20 @@ export const CLOSURE_BARE_DOLLAR = {
54
55
  };
55
56
  /**
56
57
  * Check if a node tree contains bare $ variable references.
57
- * Recursively walks the AST looking for VariableNode with isPipeVar=true.
58
+ * Uses visitNode for full AST traversal, detecting VariableNode with isPipeVar=true.
58
59
  */
59
60
  function containsBareReference(node) {
60
- if (node.type === 'Variable') {
61
- const varNode = node;
62
- // $ is represented as isPipeVar: true with name: null
63
- if (varNode.isPipeVar) {
64
- return true;
65
- }
66
- }
67
- // Recursively check child nodes based on node type
68
- switch (node.type) {
69
- case 'Block': {
70
- const blockNode = node;
71
- for (const stmt of blockNode.statements) {
72
- if (containsBareReference(stmt))
73
- return true;
74
- }
75
- break;
76
- }
77
- case 'Statement': {
78
- const stmtNode = node;
79
- if (stmtNode.expression && containsBareReference(stmtNode.expression))
80
- return true;
81
- break;
82
- }
83
- case 'PipeChain': {
84
- const pipeNode = node;
85
- if (pipeNode.head && containsBareReference(pipeNode.head))
86
- return true;
87
- if (pipeNode.pipes) {
88
- for (const pipe of pipeNode.pipes) {
89
- if (containsBareReference(pipe))
90
- return true;
91
- }
92
- }
93
- break;
94
- }
95
- case 'PostfixExpr': {
96
- const postfixNode = node;
97
- if (postfixNode.primary && containsBareReference(postfixNode.primary))
98
- return true;
99
- if (postfixNode.methods) {
100
- for (const method of postfixNode.methods) {
101
- if (containsBareReference(method))
102
- return true;
103
- }
104
- }
105
- break;
106
- }
107
- case 'BinaryExpr': {
108
- const binaryNode = node;
109
- if (binaryNode.left && containsBareReference(binaryNode.left))
110
- return true;
111
- if (binaryNode.right && containsBareReference(binaryNode.right))
112
- return true;
113
- break;
114
- }
115
- case 'UnaryExpr': {
116
- const unaryNode = node;
117
- if (unaryNode.operand && containsBareReference(unaryNode.operand))
118
- return true;
119
- break;
120
- }
121
- case 'GroupedExpr': {
122
- const groupedNode = node;
123
- if (groupedNode.expression &&
124
- containsBareReference(groupedNode.expression))
125
- return true;
126
- break;
127
- }
128
- case 'StringLiteral': {
129
- const stringNode = node;
130
- if (stringNode.parts) {
131
- for (const part of stringNode.parts) {
132
- if (typeof part === 'object' && containsBareReference(part))
133
- return true;
134
- }
135
- }
136
- break;
137
- }
138
- case 'Interpolation': {
139
- const interpNode = node;
140
- if (interpNode.expression && containsBareReference(interpNode.expression))
141
- return true;
142
- break;
143
- }
144
- case 'Conditional': {
145
- const condNode = node;
146
- if (condNode.condition && containsBareReference(condNode.condition))
147
- return true;
148
- if (condNode.thenBranch && containsBareReference(condNode.thenBranch))
149
- return true;
150
- if (condNode.elseBranch && containsBareReference(condNode.elseBranch))
151
- return true;
152
- break;
153
- }
154
- case 'MethodCall':
155
- case 'HostCall':
156
- case 'ClosureCall':
157
- case 'Invoke': {
158
- const callNode = node;
159
- if (callNode.args) {
160
- for (const arg of callNode.args) {
161
- if (containsBareReference(arg))
162
- return true;
163
- }
61
+ let found = false;
62
+ const ctx = {};
63
+ visitNode(node, ctx, {
64
+ enter(n) {
65
+ if (n.type === 'Variable' && n.isPipeVar) {
66
+ found = true;
164
67
  }
165
- break;
166
- }
167
- }
168
- return false;
68
+ },
69
+ exit() { },
70
+ });
71
+ return found;
169
72
  }
170
73
  // ============================================================
171
74
  // CLOSURE_BRACES RULE
@@ -288,82 +191,63 @@ export const CLOSURE_LATE_BINDING = {
288
191
  };
289
192
  /**
290
193
  * Check if a node contains a closure creation (Closure node).
194
+ * Uses visitNode for full AST traversal.
291
195
  */
292
196
  function containsClosureCreation(node) {
293
- if (node.type === 'Closure') {
294
- return true;
295
- }
296
- // Recursively check child nodes
297
- switch (node.type) {
298
- case 'Block': {
299
- const blockNode = node;
300
- for (const stmt of blockNode.statements) {
301
- if (containsClosureCreation(stmt))
302
- return true;
197
+ let found = false;
198
+ const ctx = {};
199
+ visitNode(node, ctx, {
200
+ enter(n) {
201
+ if (n.type === 'Closure') {
202
+ found = true;
303
203
  }
304
- break;
305
- }
306
- case 'Statement': {
307
- const stmtNode = node;
308
- if (stmtNode.expression && containsClosureCreation(stmtNode.expression))
309
- return true;
310
- break;
311
- }
312
- case 'PipeChain': {
313
- const pipeNode = node;
314
- if (pipeNode.head && containsClosureCreation(pipeNode.head))
315
- return true;
316
- if (pipeNode.pipes) {
317
- for (const pipe of pipeNode.pipes) {
318
- if (containsClosureCreation(pipe))
319
- return true;
320
- }
321
- }
322
- break;
323
- }
324
- case 'PostfixExpr': {
325
- const postfixNode = node;
326
- if (postfixNode.primary && containsClosureCreation(postfixNode.primary))
327
- return true;
328
- break;
329
- }
330
- }
331
- return false;
204
+ },
205
+ exit() { },
206
+ });
207
+ return found;
332
208
  }
333
209
  /**
334
- * Check if a Block node contains an explicit capture statement ($ => $name).
210
+ * Check if a Block node contains an explicit capture statement ($ => $name)
211
+ * at the top level (closureDepth === 0). Captures inside nested closures
212
+ * are scoped to that closure and do not fix late binding for the each body.
335
213
  */
336
214
  function containsExplicitCapture(node) {
337
215
  if (node.type !== 'Block') {
338
216
  return false;
339
217
  }
340
- const blockNode = node;
341
- const statements = blockNode.statements;
342
- // Look for capture of $ into a named variable
343
- for (const stmt of statements) {
344
- if (stmt.type === 'Statement' &&
345
- stmt.expression &&
346
- stmt.expression.type === 'PipeChain') {
347
- const chain = stmt.expression;
348
- // Check if any pipe is a Capture
349
- if (chain.pipes && Array.isArray(chain.pipes)) {
350
- for (const pipe of chain.pipes) {
351
- if (pipe.type === 'Capture') {
352
- // Check if the head is bare $
353
- const head = chain.head;
354
- if (head && head.type === 'PostfixExpr') {
355
- const postfix = head;
356
- if (postfix.primary && postfix.primary.type === 'Variable') {
357
- const varNode = postfix.primary;
358
- if (varNode.isPipeVar) {
359
- return true;
360
- }
361
- }
362
- }
363
- }
218
+ let found = false;
219
+ let closureDepth = 0;
220
+ const ctx = {};
221
+ visitNode(node, ctx, {
222
+ enter(n) {
223
+ if (n.type === 'Closure') {
224
+ closureDepth++;
225
+ return;
226
+ }
227
+ if (closureDepth > 0)
228
+ return;
229
+ if (n.type !== 'PipeChain')
230
+ return;
231
+ const chain = n;
232
+ const head = chain.head;
233
+ if (!head || head.type !== 'PostfixExpr')
234
+ return;
235
+ const postfix = head;
236
+ if (!postfix.primary || postfix.primary.type !== 'Variable')
237
+ return;
238
+ if (!postfix.primary.isPipeVar)
239
+ return;
240
+ for (const pipe of chain.pipes) {
241
+ if (pipe.type === 'Capture') {
242
+ found = true;
364
243
  }
365
244
  }
366
- }
367
- }
368
- return false;
245
+ },
246
+ exit(n) {
247
+ if (n.type === 'Closure') {
248
+ closureDepth--;
249
+ }
250
+ },
251
+ });
252
+ return found;
369
253
  }
@@ -3,81 +3,26 @@
3
3
  * Enforces conventions for each, map, fold, and filter operators.
4
4
  */
5
5
  import { extractContextLine } from './helpers.js';
6
+ import { visitNode } from '../visitor.js';
6
7
  // ============================================================
7
8
  // HELPER FUNCTIONS
8
9
  // ============================================================
9
10
  /**
10
11
  * Check if an AST subtree contains a Break node.
11
- * Recursively traverses all node types.
12
+ * Uses visitNode for full AST traversal.
12
13
  */
13
14
  function containsBreak(node) {
14
- if (node.type === 'Break') {
15
- return true;
16
- }
17
- // Recursively check children based on node type
18
- switch (node.type) {
19
- case 'Block':
20
- return node.statements.some((stmt) => containsBreak(stmt));
21
- case 'Statement':
22
- return containsBreak(node.expression);
23
- case 'AnnotatedStatement':
24
- return containsBreak(node.statement);
25
- case 'PipeChain':
26
- if (containsBreak(node.head))
27
- return true;
28
- if (node.pipes.some((pipe) => containsBreak(pipe)))
29
- return true;
30
- if (node.terminator && node.terminator.type === 'Break')
31
- return true;
32
- return false;
33
- case 'PostfixExpr':
34
- if (containsBreak(node.primary))
35
- return true;
36
- return node.methods.some((method) => containsBreak(method));
37
- case 'BinaryExpr':
38
- return containsBreak(node.left) || containsBreak(node.right);
39
- case 'UnaryExpr':
40
- return containsBreak(node.operand);
41
- case 'GroupedExpr':
42
- return containsBreak(node.expression);
43
- case 'Conditional':
44
- if (node.input && containsBreak(node.input))
45
- return true;
46
- if (node.condition && containsBreak(node.condition))
47
- return true;
48
- if (containsBreak(node.thenBranch))
49
- return true;
50
- if (node.elseBranch && containsBreak(node.elseBranch))
51
- return true;
52
- return false;
53
- case 'WhileLoop':
54
- case 'DoWhileLoop':
55
- return containsBreak(node.body);
56
- case 'Closure':
57
- return containsBreak(node.body);
58
- case 'EachExpr':
59
- case 'MapExpr':
60
- case 'FoldExpr':
61
- case 'FilterExpr':
62
- return containsBreak(node.body);
63
- case 'HostCall':
64
- case 'ClosureCall':
65
- case 'MethodCall':
66
- case 'Invoke':
67
- case 'PipeInvoke':
68
- return node.args.some((arg) => containsBreak(arg));
69
- case 'StringLiteral':
70
- return node.parts.some((part) => typeof part !== 'string' && containsBreak(part));
71
- case 'TupleLiteral':
72
- return node.elements.some((elem) => containsBreak(elem));
73
- case 'Dict':
74
- return node.entries.some((entry) => containsBreak(entry));
75
- case 'DictEntry':
76
- return containsBreak(node.value);
77
- default:
78
- // Leaf nodes and other types don't contain breaks
79
- return false;
80
- }
15
+ let found = false;
16
+ const ctx = {};
17
+ visitNode(node, ctx, {
18
+ enter(n) {
19
+ if (n.type === 'Break') {
20
+ found = true;
21
+ }
22
+ },
23
+ exit() { },
24
+ });
25
+ return found;
81
26
  }
82
27
  /**
83
28
  * Check if a body is a simple method shorthand.
@@ -11,7 +11,7 @@ export { USE_DEFAULT_OPERATOR, CONDITION_TYPE } from './conditionals.js';
11
11
  export { CLOSURE_BARE_DOLLAR, CLOSURE_BRACES, CLOSURE_LATE_BINDING, } from './closures.js';
12
12
  export { UNNECESSARY_ASSERTION, VALIDATE_EXTERNAL } from './types.js';
13
13
  export { USE_EMPTY_METHOD } from './strings.js';
14
- export { AVOID_REASSIGNMENT, COMPLEX_CONDITION, LOOP_OUTER_CAPTURE, } from './anti-patterns.js';
14
+ export { AVOID_REASSIGNMENT, COMPLEX_CONDITION, LOOP_OUTER_CAPTURE, STREAM_PRE_ITERATION, } from './anti-patterns.js';
15
15
  export { SPACING_OPERATOR, SPACING_BRACES, SPACING_BRACKETS, SPACING_CLOSURE, INDENT_CONTINUATION, IMPLICIT_DOLLAR_METHOD, IMPLICIT_DOLLAR_FUNCTION, IMPLICIT_DOLLAR_CLOSURE, THROWAWAY_CAPTURE, } from './formatting.js';
16
16
  export { USE_DYNAMIC_IDENTIFIER, USE_UNTYPED_HOST_REF, } from './use-expressions.js';
17
17
  /**
@@ -10,7 +10,7 @@ import { USE_DEFAULT_OPERATOR, CONDITION_TYPE } from './conditionals.js';
10
10
  import { CLOSURE_BARE_DOLLAR, CLOSURE_BRACES, CLOSURE_LATE_BINDING, } from './closures.js';
11
11
  import { UNNECESSARY_ASSERTION, VALIDATE_EXTERNAL } from './types.js';
12
12
  import { USE_EMPTY_METHOD } from './strings.js';
13
- import { AVOID_REASSIGNMENT, COMPLEX_CONDITION, LOOP_OUTER_CAPTURE, } from './anti-patterns.js';
13
+ import { AVOID_REASSIGNMENT, COMPLEX_CONDITION, LOOP_OUTER_CAPTURE, STREAM_PRE_ITERATION, } from './anti-patterns.js';
14
14
  import { SPACING_OPERATOR, SPACING_BRACES, SPACING_BRACKETS, SPACING_CLOSURE, INDENT_CONTINUATION, IMPLICIT_DOLLAR_METHOD, IMPLICIT_DOLLAR_FUNCTION, IMPLICIT_DOLLAR_CLOSURE, THROWAWAY_CAPTURE, } from './formatting.js';
15
15
  import { USE_DYNAMIC_IDENTIFIER, USE_UNTYPED_HOST_REF, } from './use-expressions.js';
16
16
  // ============================================================
@@ -24,7 +24,7 @@ export { USE_DEFAULT_OPERATOR, CONDITION_TYPE } from './conditionals.js';
24
24
  export { CLOSURE_BARE_DOLLAR, CLOSURE_BRACES, CLOSURE_LATE_BINDING, } from './closures.js';
25
25
  export { UNNECESSARY_ASSERTION, VALIDATE_EXTERNAL } from './types.js';
26
26
  export { USE_EMPTY_METHOD } from './strings.js';
27
- export { AVOID_REASSIGNMENT, COMPLEX_CONDITION, LOOP_OUTER_CAPTURE, } from './anti-patterns.js';
27
+ export { AVOID_REASSIGNMENT, COMPLEX_CONDITION, LOOP_OUTER_CAPTURE, STREAM_PRE_ITERATION, } from './anti-patterns.js';
28
28
  export { SPACING_OPERATOR, SPACING_BRACES, SPACING_BRACKETS, SPACING_CLOSURE, INDENT_CONTINUATION, IMPLICIT_DOLLAR_METHOD, IMPLICIT_DOLLAR_FUNCTION, IMPLICIT_DOLLAR_CLOSURE, THROWAWAY_CAPTURE, } from './formatting.js';
29
29
  export { USE_DYNAMIC_IDENTIFIER, USE_UNTYPED_HOST_REF, } from './use-expressions.js';
30
30
  // ============================================================
@@ -66,6 +66,7 @@ export const VALIDATION_RULES = [
66
66
  AVOID_REASSIGNMENT,
67
67
  COMPLEX_CONDITION,
68
68
  LOOP_OUTER_CAPTURE,
69
+ STREAM_PRE_ITERATION,
69
70
  // Formatting
70
71
  SPACING_OPERATOR,
71
72
  SPACING_BRACES,
@@ -257,6 +257,7 @@ export function visitNode(node, context, visitor) {
257
257
  case 'Break':
258
258
  case 'Return':
259
259
  case 'Pass':
260
+ case 'Yield':
260
261
  // Leaf nodes - no children
261
262
  break;
262
263
  case 'RecoveryError':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcrsr/rill-cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "CLI tools for the rill scripting language",
5
5
  "license": "MIT",
6
6
  "author": "Andre Bremer",
@@ -21,8 +21,8 @@
21
21
  "dependencies": {
22
22
  "dotenv": "^16.0.0",
23
23
  "yaml": "^2.8.2",
24
- "@rcrsr/rill": "^0.17.0",
25
- "@rcrsr/rill-config": "^0.17.0"
24
+ "@rcrsr/rill": "^0.18.0",
25
+ "@rcrsr/rill-config": "^0.18.0"
26
26
  },
27
27
  "files": [
28
28
  "dist"