@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.
- package/dist/check/rules/anti-patterns.d.ts +20 -0
- package/dist/check/rules/anti-patterns.js +225 -68
- package/dist/check/rules/closures.js +59 -175
- package/dist/check/rules/collections.js +13 -68
- package/dist/check/rules/index.d.ts +1 -1
- package/dist/check/rules/index.js +3 -2
- package/dist/check/visitor.js +1 -0
- package/package.json +3 -3
|
@@ -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
|
-
*
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
*
|
|
58
|
+
* Uses visitNode for full AST traversal, detecting VariableNode with isPipeVar=true.
|
|
58
59
|
*/
|
|
59
60
|
function containsBareReference(node) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
12
|
+
* Uses visitNode for full AST traversal.
|
|
12
13
|
*/
|
|
13
14
|
function containsBreak(node) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
package/dist/check/visitor.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rcrsr/rill-cli",
|
|
3
|
-
"version": "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.
|
|
25
|
-
"@rcrsr/rill-config": "^0.
|
|
24
|
+
"@rcrsr/rill": "^0.18.0",
|
|
25
|
+
"@rcrsr/rill-config": "^0.18.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"dist"
|