@taiga-ui/eslint-plugin-experience-next 0.473.0 → 0.474.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/README.md +9 -4
- package/index.esm.js +211 -28
- package/package.json +1 -1
- package/rules/utils/angular-signals.d.ts +1 -0
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ export default [
|
|
|
54
54
|
| no-playwright-empty-fill | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
|
|
55
55
|
| no-project-as-in-ng-template | `ngProjectAs` has no effect inside `<ng-template>` or dynamic outlets | ✅ | | |
|
|
56
56
|
| no-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
|
|
57
|
-
| no-side-effects-in-computed | Disallow
|
|
57
|
+
| no-side-effects-in-computed | Disallow side effects and effectful helper calls inside Angular `computed()` callbacks | ✅ | | |
|
|
58
58
|
| no-signal-reads-after-await-in-reactive-context | Disallow bare signal reads after `await` inside reactive callbacks | ✅ | | |
|
|
59
59
|
| no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
|
|
60
60
|
| no-untracked-outside-reactive-context | Disallow `untracked()` outside reactive callbacks, except explicit post-`await` snapshots | ✅ | 🔧 | |
|
|
@@ -681,8 +681,9 @@ const doubled = computed(() => {
|
|
|
681
681
|
<sup>`✅ Recommended`</sup>
|
|
682
682
|
|
|
683
683
|
`computed()` should only derive a value from its inputs. This rule reports observable side effects inside Angular
|
|
684
|
-
`computed()` callbacks, including signal writes (`.set()`, `.update()`, `.mutate()`),
|
|
685
|
-
`++/--`, `delete`,
|
|
684
|
+
`computed()` callbacks, including signal writes (`.set()`, `.update()`, `.mutate()`), `effect()`, `inject()`,
|
|
685
|
+
assignments to captured state, `++/--`, `delete`, property mutations on objects that were not created inside the
|
|
686
|
+
computation itself, and calls to local helper functions or methods when their bodies perform those operations.
|
|
686
687
|
|
|
687
688
|
```ts
|
|
688
689
|
// ❌ error
|
|
@@ -691,8 +692,12 @@ import {computed, signal} from '@angular/core';
|
|
|
691
692
|
const source = signal(0);
|
|
692
693
|
const target = signal(0);
|
|
693
694
|
|
|
694
|
-
|
|
695
|
+
function syncTarget(): void {
|
|
695
696
|
target.set(source() + 1);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const derived = computed(() => {
|
|
700
|
+
syncTarget();
|
|
696
701
|
return target();
|
|
697
702
|
});
|
|
698
703
|
```
|
package/index.esm.js
CHANGED
|
@@ -2718,6 +2718,9 @@ function appendObjectPropertyReactiveScopes(scopes, call, object, labels) {
|
|
|
2718
2718
|
function isAngularEffectCall(node, program) {
|
|
2719
2719
|
return isAngularCoreCall(node, program, 'effect');
|
|
2720
2720
|
}
|
|
2721
|
+
function isAngularInjectCall(node, program) {
|
|
2722
|
+
return isAngularCoreCall(node, program, 'inject');
|
|
2723
|
+
}
|
|
2721
2724
|
function isAngularUntrackedCall(node, program) {
|
|
2722
2725
|
return isAngularCoreCall(node, program, 'untracked');
|
|
2723
2726
|
}
|
|
@@ -3463,6 +3466,11 @@ function unwrapExpression(expression) {
|
|
|
3463
3466
|
}
|
|
3464
3467
|
|
|
3465
3468
|
const createRule$8 = ESLintUtils.RuleCreator((name) => name);
|
|
3469
|
+
function isFunctionLikeScope(node) {
|
|
3470
|
+
return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
3471
|
+
node?.type === AST_NODE_TYPES.FunctionDeclaration ||
|
|
3472
|
+
node?.type === AST_NODE_TYPES.FunctionExpression);
|
|
3473
|
+
}
|
|
3466
3474
|
function isReactiveCallback(node) {
|
|
3467
3475
|
return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
3468
3476
|
node?.type === AST_NODE_TYPES.FunctionExpression);
|
|
@@ -3507,7 +3515,7 @@ function getSymbolAtNode(node, checker, esTreeNodeToTSNodeMap) {
|
|
|
3507
3515
|
}
|
|
3508
3516
|
return checker.getSymbolAtLocation(tsNode) ?? null;
|
|
3509
3517
|
}
|
|
3510
|
-
function isLocalIdentifier(node, context) {
|
|
3518
|
+
function isLocalIdentifier(node, context, localScopes) {
|
|
3511
3519
|
const symbol = getSymbolAtNode(node, context.checker, context.esTreeNodeToTSNodeMap);
|
|
3512
3520
|
if (!symbol) {
|
|
3513
3521
|
return false;
|
|
@@ -3515,10 +3523,24 @@ function isLocalIdentifier(node, context) {
|
|
|
3515
3523
|
return (symbol.declarations ?? []).some((declaration) => {
|
|
3516
3524
|
const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
|
|
3517
3525
|
return (!!estreeDeclaration &&
|
|
3518
|
-
|
|
3526
|
+
isDeclaredInsideLocalScope(estreeDeclaration, localScopes));
|
|
3519
3527
|
});
|
|
3520
3528
|
}
|
|
3521
|
-
function
|
|
3529
|
+
function isDeclaredInsideLocalScope(node, localScopes) {
|
|
3530
|
+
return localScopes.some((scope) => isNodeInsideFunctionScope(node, scope));
|
|
3531
|
+
}
|
|
3532
|
+
function isNodeInsideFunctionScope(node, scope) {
|
|
3533
|
+
let found = false;
|
|
3534
|
+
walkSynchronousAst(scope, (inner) => {
|
|
3535
|
+
if (inner !== node) {
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
found = true;
|
|
3539
|
+
return false;
|
|
3540
|
+
});
|
|
3541
|
+
return found;
|
|
3542
|
+
}
|
|
3543
|
+
function isLocallyCreatedExpression(node, context, localScopes) {
|
|
3522
3544
|
const expression = unwrapExpression(node);
|
|
3523
3545
|
switch (expression.type) {
|
|
3524
3546
|
case AST_NODE_TYPES.ArrayExpression:
|
|
@@ -3526,60 +3548,222 @@ function isLocallyCreatedExpression(node, context) {
|
|
|
3526
3548
|
case AST_NODE_TYPES.ObjectExpression:
|
|
3527
3549
|
return true;
|
|
3528
3550
|
case AST_NODE_TYPES.Identifier:
|
|
3529
|
-
return isLocalIdentifier(expression, context);
|
|
3551
|
+
return isLocalIdentifier(expression, context, localScopes);
|
|
3530
3552
|
case AST_NODE_TYPES.MemberExpression:
|
|
3531
|
-
return isLocallyCreatedExpression(expression.object, context);
|
|
3553
|
+
return isLocallyCreatedExpression(expression.object, context, localScopes);
|
|
3532
3554
|
default:
|
|
3533
3555
|
return false;
|
|
3534
3556
|
}
|
|
3535
3557
|
}
|
|
3536
|
-
function hasObservableMutationTarget(node, context) {
|
|
3558
|
+
function hasObservableMutationTarget(node, context, localScopes) {
|
|
3537
3559
|
return collectMutationTargets(node).some((target) => {
|
|
3538
3560
|
if (target.type === AST_NODE_TYPES.Identifier) {
|
|
3539
|
-
return !isLocalIdentifier(target, context);
|
|
3561
|
+
return !isLocalIdentifier(target, context, localScopes);
|
|
3540
3562
|
}
|
|
3541
|
-
return !isLocallyCreatedExpression(target.object, context);
|
|
3563
|
+
return !isLocallyCreatedExpression(target.object, context, localScopes);
|
|
3542
3564
|
});
|
|
3543
3565
|
}
|
|
3544
|
-
function reportSideEffect(node,
|
|
3566
|
+
function reportSideEffect(node, context, report) {
|
|
3545
3567
|
const key = String(node.range);
|
|
3546
|
-
if (
|
|
3568
|
+
if (context.reported.has(key)) {
|
|
3547
3569
|
return;
|
|
3548
3570
|
}
|
|
3549
|
-
|
|
3571
|
+
context.reported.add(key);
|
|
3550
3572
|
report(node);
|
|
3551
3573
|
}
|
|
3552
|
-
function
|
|
3574
|
+
function isDirectAngularSideEffectCall(node, context) {
|
|
3575
|
+
return (isWritableSignalWrite(node, context.checker, context.esTreeNodeToTSNodeMap) ||
|
|
3576
|
+
isAngularEffectCall(node, context.program) ||
|
|
3577
|
+
isAngularInjectCall(node, context.program));
|
|
3578
|
+
}
|
|
3579
|
+
function isInspectableFunctionContainer(node) {
|
|
3580
|
+
return (!!node &&
|
|
3581
|
+
(isFunctionLikeScope(node) ||
|
|
3582
|
+
node.type === AST_NODE_TYPES.MethodDefinition ||
|
|
3583
|
+
node.type === AST_NODE_TYPES.Property ||
|
|
3584
|
+
node.type === AST_NODE_TYPES.PropertyDefinition ||
|
|
3585
|
+
node.type === AST_NODE_TYPES.VariableDeclarator));
|
|
3586
|
+
}
|
|
3587
|
+
function resolveFunctionLikeFromContainer(node, context, seenSymbols = new Set()) {
|
|
3588
|
+
if (isFunctionLikeScope(node)) {
|
|
3589
|
+
return [node];
|
|
3590
|
+
}
|
|
3591
|
+
if ((node.type === AST_NODE_TYPES.MethodDefinition &&
|
|
3592
|
+
isFunctionLikeScope(node.value)) ||
|
|
3593
|
+
(node.type === AST_NODE_TYPES.Property && isFunctionLikeScope(node.value))) {
|
|
3594
|
+
return [node.value];
|
|
3595
|
+
}
|
|
3596
|
+
if (node.type === AST_NODE_TYPES.Property &&
|
|
3597
|
+
node.value.type === AST_NODE_TYPES.Identifier) {
|
|
3598
|
+
return resolveFunctionLikeFromIdentifier(node.value, context, seenSymbols);
|
|
3599
|
+
}
|
|
3600
|
+
if (node.type === AST_NODE_TYPES.PropertyDefinition &&
|
|
3601
|
+
node.value &&
|
|
3602
|
+
isFunctionLikeScope(node.value)) {
|
|
3603
|
+
return [node.value];
|
|
3604
|
+
}
|
|
3605
|
+
if (node.type === AST_NODE_TYPES.PropertyDefinition &&
|
|
3606
|
+
node.value?.type === AST_NODE_TYPES.Identifier) {
|
|
3607
|
+
return resolveFunctionLikeFromIdentifier(node.value, context, seenSymbols);
|
|
3608
|
+
}
|
|
3609
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator) {
|
|
3610
|
+
const { init } = node;
|
|
3611
|
+
if (init && isFunctionLikeScope(init)) {
|
|
3612
|
+
return [init];
|
|
3613
|
+
}
|
|
3614
|
+
if (init?.type === AST_NODE_TYPES.Identifier) {
|
|
3615
|
+
return resolveFunctionLikeFromIdentifier(init, context, seenSymbols);
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return [];
|
|
3619
|
+
}
|
|
3620
|
+
function resolveFunctionLikeFromIdentifier(node, context, seenSymbols = new Set()) {
|
|
3621
|
+
const symbol = getSymbolAtNode(node, context.checker, context.esTreeNodeToTSNodeMap);
|
|
3622
|
+
if (!symbol) {
|
|
3623
|
+
return [];
|
|
3624
|
+
}
|
|
3625
|
+
const symbolId = `${symbol.name}:${symbol.declarations?.[0]?.pos ?? -1}`;
|
|
3626
|
+
if (seenSymbols.has(symbolId)) {
|
|
3627
|
+
return [];
|
|
3628
|
+
}
|
|
3629
|
+
seenSymbols.add(symbolId);
|
|
3630
|
+
return (symbol.declarations ?? []).flatMap((declaration) => {
|
|
3631
|
+
const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
|
|
3632
|
+
if (!estreeDeclaration || !isInspectableFunctionContainer(estreeDeclaration)) {
|
|
3633
|
+
return [];
|
|
3634
|
+
}
|
|
3635
|
+
return resolveFunctionLikeFromContainer(estreeDeclaration, context, seenSymbols);
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
function resolveCalledFunctions(node, context) {
|
|
3639
|
+
const resolved = new Map();
|
|
3640
|
+
const tsNode = context.esTreeNodeToTSNodeMap.get(node);
|
|
3641
|
+
const signature = tsNode ? context.checker.getResolvedSignature(tsNode) : undefined;
|
|
3642
|
+
const declarations = new Set();
|
|
3643
|
+
if (signature?.declaration) {
|
|
3644
|
+
declarations.add(signature.declaration);
|
|
3645
|
+
}
|
|
3646
|
+
const callee = unwrapExpression(node.callee);
|
|
3647
|
+
if (callee.type === AST_NODE_TYPES.Identifier ||
|
|
3648
|
+
callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
3649
|
+
const symbol = getSymbolAtNode(callee, context.checker, context.esTreeNodeToTSNodeMap);
|
|
3650
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
3651
|
+
declarations.add(declaration);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
for (const declaration of declarations) {
|
|
3655
|
+
const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
|
|
3656
|
+
if (!estreeDeclaration || !isInspectableFunctionContainer(estreeDeclaration)) {
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
for (const fn of resolveFunctionLikeFromContainer(estreeDeclaration, context)) {
|
|
3660
|
+
resolved.set(String(fn.range), fn);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
return [...resolved.values()];
|
|
3664
|
+
}
|
|
3665
|
+
function functionHasObservableSideEffects(root, context, localScopes, visitedFunctions) {
|
|
3666
|
+
let hasSideEffect = false;
|
|
3553
3667
|
walkSynchronousAst(root, (node) => {
|
|
3554
3668
|
if (node.type === AST_NODE_TYPES.CallExpression) {
|
|
3555
|
-
if (
|
|
3556
|
-
|
|
3669
|
+
if (isDirectAngularSideEffectCall(node, context)) {
|
|
3670
|
+
hasSideEffect = true;
|
|
3671
|
+
return false;
|
|
3672
|
+
}
|
|
3673
|
+
if (isAngularUntrackedCall(node, context.program)) {
|
|
3674
|
+
const [arg] = node.arguments;
|
|
3675
|
+
if (isReactiveCallback(arg) &&
|
|
3676
|
+
functionHasObservableSideEffects(arg, context, [...localScopes, arg], visitedFunctions)) {
|
|
3677
|
+
hasSideEffect = true;
|
|
3678
|
+
}
|
|
3679
|
+
return false;
|
|
3680
|
+
}
|
|
3681
|
+
if (isReactiveCallback(node.callee)) {
|
|
3682
|
+
if (functionHasObservableSideEffects(node.callee, context, [...localScopes, node.callee], visitedFunctions)) {
|
|
3683
|
+
hasSideEffect = true;
|
|
3684
|
+
}
|
|
3685
|
+
return false;
|
|
3686
|
+
}
|
|
3687
|
+
for (const calledFunction of resolveCalledFunctions(node, context)) {
|
|
3688
|
+
const key = String(calledFunction.range);
|
|
3689
|
+
if (visitedFunctions.has(key)) {
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
visitedFunctions.add(key);
|
|
3693
|
+
const calledFunctionHasSideEffects = functionHasObservableSideEffects(calledFunction, context, [...localScopes, calledFunction], visitedFunctions);
|
|
3694
|
+
visitedFunctions.delete(key);
|
|
3695
|
+
if (!calledFunctionHasSideEffects) {
|
|
3696
|
+
continue;
|
|
3697
|
+
}
|
|
3698
|
+
hasSideEffect = true;
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
if (node.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
3703
|
+
hasObservableMutationTarget(node.left, context, localScopes)) {
|
|
3704
|
+
hasSideEffect = true;
|
|
3705
|
+
return false;
|
|
3706
|
+
}
|
|
3707
|
+
if (node.type === AST_NODE_TYPES.UpdateExpression &&
|
|
3708
|
+
hasObservableMutationTarget(node.argument, context, localScopes)) {
|
|
3709
|
+
hasSideEffect = true;
|
|
3710
|
+
return false;
|
|
3711
|
+
}
|
|
3712
|
+
if (node.type === AST_NODE_TYPES.UnaryExpression &&
|
|
3713
|
+
node.operator === 'delete' &&
|
|
3714
|
+
hasObservableMutationTarget(node.argument, context, localScopes)) {
|
|
3715
|
+
hasSideEffect = true;
|
|
3716
|
+
return false;
|
|
3717
|
+
}
|
|
3718
|
+
return;
|
|
3719
|
+
});
|
|
3720
|
+
return hasSideEffect;
|
|
3721
|
+
}
|
|
3722
|
+
function inspectComputedBody(root, context, localScopes, visitedFunctions, report) {
|
|
3723
|
+
walkSynchronousAst(root, (node) => {
|
|
3724
|
+
if (node.type === AST_NODE_TYPES.CallExpression) {
|
|
3725
|
+
if (isDirectAngularSideEffectCall(node, context)) {
|
|
3726
|
+
reportSideEffect(node, context, report);
|
|
3727
|
+
return false;
|
|
3557
3728
|
}
|
|
3558
|
-
if (isAngularUntrackedCall(node,
|
|
3729
|
+
if (isAngularUntrackedCall(node, context.program)) {
|
|
3559
3730
|
const [arg] = node.arguments;
|
|
3560
3731
|
if (isReactiveCallback(arg)) {
|
|
3561
|
-
inspectComputedBody(arg,
|
|
3732
|
+
inspectComputedBody(arg, context, [...localScopes, arg], visitedFunctions, report);
|
|
3562
3733
|
}
|
|
3563
3734
|
return false;
|
|
3564
3735
|
}
|
|
3565
|
-
if (node.callee
|
|
3566
|
-
node.callee.
|
|
3567
|
-
|
|
3736
|
+
if (isReactiveCallback(node.callee)) {
|
|
3737
|
+
inspectComputedBody(node.callee, context, [...localScopes, node.callee], visitedFunctions, report);
|
|
3738
|
+
return false;
|
|
3739
|
+
}
|
|
3740
|
+
for (const calledFunction of resolveCalledFunctions(node, context)) {
|
|
3741
|
+
const key = String(calledFunction.range);
|
|
3742
|
+
if (visitedFunctions.has(key)) {
|
|
3743
|
+
continue;
|
|
3744
|
+
}
|
|
3745
|
+
visitedFunctions.add(key);
|
|
3746
|
+
const calledFunctionHasSideEffects = functionHasObservableSideEffects(calledFunction, context, [...localScopes, calledFunction], visitedFunctions);
|
|
3747
|
+
visitedFunctions.delete(key);
|
|
3748
|
+
if (!calledFunctionHasSideEffects) {
|
|
3749
|
+
continue;
|
|
3750
|
+
}
|
|
3751
|
+
reportSideEffect(node, context, report);
|
|
3568
3752
|
return false;
|
|
3569
3753
|
}
|
|
3570
3754
|
}
|
|
3571
3755
|
if (node.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
3572
|
-
hasObservableMutationTarget(node.left,
|
|
3573
|
-
reportSideEffect(node.left,
|
|
3756
|
+
hasObservableMutationTarget(node.left, context, localScopes)) {
|
|
3757
|
+
reportSideEffect(node.left, context, report);
|
|
3574
3758
|
}
|
|
3575
3759
|
if (node.type === AST_NODE_TYPES.UpdateExpression &&
|
|
3576
|
-
hasObservableMutationTarget(node.argument,
|
|
3577
|
-
reportSideEffect(node.argument,
|
|
3760
|
+
hasObservableMutationTarget(node.argument, context, localScopes)) {
|
|
3761
|
+
reportSideEffect(node.argument, context, report);
|
|
3578
3762
|
}
|
|
3579
3763
|
if (node.type === AST_NODE_TYPES.UnaryExpression &&
|
|
3580
3764
|
node.operator === 'delete' &&
|
|
3581
|
-
hasObservableMutationTarget(node.argument,
|
|
3582
|
-
reportSideEffect(node.argument,
|
|
3765
|
+
hasObservableMutationTarget(node.argument, context, localScopes)) {
|
|
3766
|
+
reportSideEffect(node.argument, context, report);
|
|
3583
3767
|
}
|
|
3584
3768
|
return;
|
|
3585
3769
|
});
|
|
@@ -3598,15 +3782,14 @@ const rule$b = createRule$8({
|
|
|
3598
3782
|
if (scope.kind !== 'computed()') {
|
|
3599
3783
|
continue;
|
|
3600
3784
|
}
|
|
3601
|
-
const
|
|
3602
|
-
callback: scope.callback,
|
|
3785
|
+
const analysisContext = {
|
|
3603
3786
|
checker,
|
|
3604
3787
|
esTreeNodeToTSNodeMap,
|
|
3605
3788
|
program,
|
|
3606
3789
|
reported: new Set(),
|
|
3607
3790
|
tsNodeToESTreeNodeMap,
|
|
3608
3791
|
};
|
|
3609
|
-
inspectComputedBody(scope.callback,
|
|
3792
|
+
inspectComputedBody(scope.callback, analysisContext, [scope.callback], new Set([String(scope.callback.range)]), (reportNode) => {
|
|
3610
3793
|
context.report({
|
|
3611
3794
|
data: { expression: sourceCode.getText(reportNode) },
|
|
3612
3795
|
messageId: 'sideEffectInComputed',
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ export interface SignalUsage {
|
|
|
17
17
|
readonly writes: TSESTree.CallExpression[];
|
|
18
18
|
}
|
|
19
19
|
export declare function isAngularEffectCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
20
|
+
export declare function isAngularInjectCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
20
21
|
export declare function isAngularUntrackedCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
21
22
|
export declare function getReactiveScopes(node: TSESTree.CallExpression, program: TSESTree.Program): ReactiveScope[];
|
|
22
23
|
export declare function isNodeInsideSynchronousReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;
|