@taiga-ui/eslint-plugin-experience-next 0.467.0 → 0.469.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 +37 -5
- package/index.d.ts +3 -0
- package/index.esm.js +263 -65
- package/package.json +1 -1
- package/rules/no-side-effects-in-computed.d.ts +5 -0
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ export default [
|
|
|
53
53
|
| no-playwright-empty-fill | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
|
|
54
54
|
| no-project-as-in-ng-template | `ngProjectAs` has no effect inside `<ng-template>` or dynamic outlets | ✅ | | |
|
|
55
55
|
| no-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
|
|
56
|
+
| no-side-effects-in-computed | Disallow observable side effects inside Angular `computed()` callbacks | ✅ | | |
|
|
56
57
|
| no-signal-reads-after-await-in-reactive-context | Disallow signal reads after `await` inside reactive callbacks | ✅ | | |
|
|
57
58
|
| no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
|
|
58
59
|
| no-untracked-outside-reactive-context | Disallow `untracked()` outside the synchronous body of reactive callbacks | ✅ | 🔧 | |
|
|
@@ -566,6 +567,37 @@ const doubled = computed(() => {
|
|
|
566
567
|
|
|
567
568
|
---
|
|
568
569
|
|
|
570
|
+
## no-side-effects-in-computed
|
|
571
|
+
|
|
572
|
+
<sup>`✅ Recommended`</sup>
|
|
573
|
+
|
|
574
|
+
`computed()` should only derive a value from its inputs. This rule reports observable side effects inside Angular
|
|
575
|
+
`computed()` callbacks, including signal writes (`.set()`, `.update()`, `.mutate()`), assignments to captured state,
|
|
576
|
+
`++/--`, `delete`, and property mutations on objects that were not created inside the computation itself.
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
// ❌ error
|
|
580
|
+
import {computed, signal} from '@angular/core';
|
|
581
|
+
|
|
582
|
+
const source = signal(0);
|
|
583
|
+
const target = signal(0);
|
|
584
|
+
|
|
585
|
+
const derived = computed(() => {
|
|
586
|
+
target.set(source() + 1);
|
|
587
|
+
return target();
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
// ✅ ok
|
|
593
|
+
import {computed, signal} from '@angular/core';
|
|
594
|
+
|
|
595
|
+
const source = signal(0);
|
|
596
|
+
const derived = computed(() => source() + 1);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
569
601
|
## no-signal-reads-after-await-in-reactive-context
|
|
570
602
|
|
|
571
603
|
<sup>`✅ Recommended`</sup>
|
|
@@ -598,11 +630,11 @@ effect(async () => {
|
|
|
598
630
|
ordinary non-reactive code, nested callbacks, or code that runs after `await`, it usually does not prevent dependency
|
|
599
631
|
tracking and only adds noise. This rule reports those cases, but intentionally allows a few imperative Angular escape
|
|
600
632
|
hatches where `untracked()` can still be useful: `@Pipe().transform`, `ControlValueAccessor.writeValue`,
|
|
601
|
-
`registerOnChange
|
|
602
|
-
|
|
603
|
-
`
|
|
604
|
-
|
|
605
|
-
wrapper.
|
|
633
|
+
`registerOnChange` including patched accessors such as `accessor.writeValue = (...) => {}`, callback-form wrappers used
|
|
634
|
+
inside deferred scheduler / event-handler callbacks, and narrow lazy DI factory wrappers like
|
|
635
|
+
`InjectionToken({factory})` / `useFactory` when they guard creation of a reactive owner such as `effect()` against an
|
|
636
|
+
accidental ambient reactive context. For the narrow case `untracked(() => effect(...))` and similar outer wrappers
|
|
637
|
+
around a reactive call in ordinary code, autofix removes only the useless outer `untracked()` wrapper.
|
|
606
638
|
|
|
607
639
|
```ts
|
|
608
640
|
// ❌ error
|
package/index.d.ts
CHANGED
|
@@ -53,6 +53,9 @@ declare const plugin: {
|
|
|
53
53
|
} | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
54
54
|
name: string;
|
|
55
55
|
};
|
|
56
|
+
'no-side-effects-in-computed': import("@typescript-eslint/utils/ts-eslint").RuleModule<"sideEffectInComputed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
57
|
+
name: string;
|
|
58
|
+
};
|
|
56
59
|
'no-signal-reads-after-await-in-reactive-context': import("@typescript-eslint/utils/ts-eslint").RuleModule<"readAfterAwait", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
57
60
|
name: string;
|
|
58
61
|
};
|
package/index.esm.js
CHANGED
|
@@ -908,6 +908,7 @@ var recommended = defineConfig([
|
|
|
908
908
|
'@taiga-ui/experience-next/no-fully-untracked-effect': 'error',
|
|
909
909
|
'@taiga-ui/experience-next/no-implicit-public': 'error',
|
|
910
910
|
'@taiga-ui/experience-next/no-redundant-type-annotation': 'error',
|
|
911
|
+
'@taiga-ui/experience-next/no-side-effects-in-computed': 'error',
|
|
911
912
|
'@taiga-ui/experience-next/no-signal-reads-after-await-in-reactive-context': 'error',
|
|
912
913
|
'@taiga-ui/experience-next/no-untracked-outside-reactive-context': 'error',
|
|
913
914
|
'@taiga-ui/experience-next/no-useless-untracked': 'error',
|
|
@@ -1333,8 +1334,8 @@ function intersect(a, b) {
|
|
|
1333
1334
|
return a.some((type) => origin.has(type));
|
|
1334
1335
|
}
|
|
1335
1336
|
|
|
1336
|
-
const createRule$
|
|
1337
|
-
var classPropertyNaming = createRule$
|
|
1337
|
+
const createRule$g = ESLintUtils.RuleCreator((name) => name);
|
|
1338
|
+
var classPropertyNaming = createRule$g({
|
|
1338
1339
|
create(context, [configs]) {
|
|
1339
1340
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
1340
1341
|
const typeChecker = parserServices.program.getTypeChecker();
|
|
@@ -1503,9 +1504,9 @@ function isExternalPureTuple(typeChecker, type) {
|
|
|
1503
1504
|
return typeArgs.every((item) => isClassType(item));
|
|
1504
1505
|
}
|
|
1505
1506
|
|
|
1506
|
-
const createRule$
|
|
1507
|
+
const createRule$f = ESLintUtils.RuleCreator((name) => name);
|
|
1507
1508
|
const MESSAGE_ID$7 = 'spreadArrays';
|
|
1508
|
-
var flatExports = createRule$
|
|
1509
|
+
var flatExports = createRule$f({
|
|
1509
1510
|
create(context) {
|
|
1510
1511
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
1511
1512
|
const typeChecker = parserServices.program.getTypeChecker();
|
|
@@ -1711,8 +1712,8 @@ const config$2 = {
|
|
|
1711
1712
|
|
|
1712
1713
|
const MESSAGE_ID$5 = 'invalid-injection-token-description';
|
|
1713
1714
|
const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
|
|
1714
|
-
const createRule$
|
|
1715
|
-
const rule$
|
|
1715
|
+
const createRule$e = ESLintUtils.RuleCreator((name) => name);
|
|
1716
|
+
const rule$h = createRule$e({
|
|
1716
1717
|
create(context) {
|
|
1717
1718
|
return {
|
|
1718
1719
|
'NewExpression[callee.name="InjectionToken"]'(node) {
|
|
@@ -1779,8 +1780,8 @@ const DEFAULT_OPTIONS = {
|
|
|
1779
1780
|
importDeclaration: '^@taiga-ui*',
|
|
1780
1781
|
projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
|
|
1781
1782
|
};
|
|
1782
|
-
const createRule$
|
|
1783
|
-
const rule$
|
|
1783
|
+
const createRule$d = ESLintUtils.RuleCreator((name) => name);
|
|
1784
|
+
const rule$g = createRule$d({
|
|
1784
1785
|
create(context) {
|
|
1785
1786
|
const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
|
|
1786
1787
|
const hasNonCodeExtension = (source) => {
|
|
@@ -1867,13 +1868,13 @@ const rule$f = createRule$c({
|
|
|
1867
1868
|
name: 'no-deep-imports',
|
|
1868
1869
|
});
|
|
1869
1870
|
|
|
1870
|
-
const createRule$
|
|
1871
|
+
const createRule$c = ESLintUtils.RuleCreator((name) => name);
|
|
1871
1872
|
const resolveCacheByOptions = new WeakMap();
|
|
1872
1873
|
const nearestFileUpCache = new Map();
|
|
1873
1874
|
const markerCache = new Map();
|
|
1874
1875
|
const indexFileCache = new Map();
|
|
1875
1876
|
const indexExportsCache = new Map();
|
|
1876
|
-
var noDeepImportsToIndexedPackages = createRule$
|
|
1877
|
+
var noDeepImportsToIndexedPackages = createRule$c({
|
|
1877
1878
|
create(context) {
|
|
1878
1879
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
1879
1880
|
const program = parserServices.program;
|
|
@@ -2298,7 +2299,7 @@ const AFTER_RENDER_EFFECT_PHASES = new Map([
|
|
|
2298
2299
|
['read', 'afterRenderEffect().read'],
|
|
2299
2300
|
['write', 'afterRenderEffect().write'],
|
|
2300
2301
|
]);
|
|
2301
|
-
function isReactiveCallback(node) {
|
|
2302
|
+
function isReactiveCallback$1(node) {
|
|
2302
2303
|
return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
2303
2304
|
node?.type === AST_NODE_TYPES.FunctionExpression);
|
|
2304
2305
|
}
|
|
@@ -2322,7 +2323,7 @@ function isAngularCoreCall(node, program, exportedName) {
|
|
|
2322
2323
|
}
|
|
2323
2324
|
function appendFirstArgReactiveScope(scopes, call, kind) {
|
|
2324
2325
|
const [arg] = call.arguments;
|
|
2325
|
-
if (!isReactiveCallback(arg)) {
|
|
2326
|
+
if (!isReactiveCallback$1(arg)) {
|
|
2326
2327
|
return;
|
|
2327
2328
|
}
|
|
2328
2329
|
scopes.push({ callback: arg, kind, owner: call, reportNode: call });
|
|
@@ -2334,7 +2335,7 @@ function appendObjectPropertyReactiveScopes(scopes, call, object, labels) {
|
|
|
2334
2335
|
}
|
|
2335
2336
|
const name = getPropertyName(property);
|
|
2336
2337
|
const label = name ? labels.get(name) : null;
|
|
2337
|
-
if (!label || !isReactiveCallback(property.value)) {
|
|
2338
|
+
if (!label || !isReactiveCallback$1(property.value)) {
|
|
2338
2339
|
continue;
|
|
2339
2340
|
}
|
|
2340
2341
|
scopes.push({
|
|
@@ -2555,7 +2556,7 @@ function collectReadsInsideUntracked(root, checker, esTreeNodeToTSNodeMap, progr
|
|
|
2555
2556
|
});
|
|
2556
2557
|
return reads;
|
|
2557
2558
|
}
|
|
2558
|
-
const rule$
|
|
2559
|
+
const rule$f = createUntrackedRule({
|
|
2559
2560
|
create(context) {
|
|
2560
2561
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
2561
2562
|
const checker = parserServices.program.getTypeChecker();
|
|
@@ -2641,8 +2642,8 @@ const config$1 = {
|
|
|
2641
2642
|
},
|
|
2642
2643
|
};
|
|
2643
2644
|
|
|
2644
|
-
const createRule$
|
|
2645
|
-
const rule$
|
|
2645
|
+
const createRule$b = ESLintUtils.RuleCreator((name) => name);
|
|
2646
|
+
const rule$e = createRule$b({
|
|
2646
2647
|
create(context) {
|
|
2647
2648
|
const checkImplicitPublic = (node) => {
|
|
2648
2649
|
const classRef = getClass(node);
|
|
@@ -2713,9 +2714,9 @@ function getClass(node) {
|
|
|
2713
2714
|
return getClass(node.parent);
|
|
2714
2715
|
}
|
|
2715
2716
|
|
|
2716
|
-
const createRule$
|
|
2717
|
+
const createRule$a = ESLintUtils.RuleCreator((name) => name);
|
|
2717
2718
|
const LEGACY_PEER_DEPS_PATTERN = /^legacy-peer-deps\s*=\s*true$/i;
|
|
2718
|
-
const rule$
|
|
2719
|
+
const rule$d = createRule$a({
|
|
2719
2720
|
create(context) {
|
|
2720
2721
|
return {
|
|
2721
2722
|
Program(node) {
|
|
@@ -2753,8 +2754,8 @@ const rule$c = createRule$9({
|
|
|
2753
2754
|
name: 'no-legacy-peer-deps',
|
|
2754
2755
|
});
|
|
2755
2756
|
|
|
2756
|
-
const createRule$
|
|
2757
|
-
const rule$
|
|
2757
|
+
const createRule$9 = ESLintUtils.RuleCreator((name) => name);
|
|
2758
|
+
const rule$c = createRule$9({
|
|
2758
2759
|
create(context) {
|
|
2759
2760
|
const services = ESLintUtils.getParserServices(context);
|
|
2760
2761
|
const checker = services.program.getTypeChecker();
|
|
@@ -2919,7 +2920,7 @@ const config = {
|
|
|
2919
2920
|
},
|
|
2920
2921
|
};
|
|
2921
2922
|
|
|
2922
|
-
const createRule$
|
|
2923
|
+
const createRule$8 = ESLintUtils.RuleCreator((name) => name);
|
|
2923
2924
|
function collectArrayExpressions(node) {
|
|
2924
2925
|
const result = [];
|
|
2925
2926
|
if (node.type === AST_NODE_TYPES.ArrayExpression) {
|
|
@@ -2945,7 +2946,7 @@ function collectArrayExpressions(node) {
|
|
|
2945
2946
|
}
|
|
2946
2947
|
return result;
|
|
2947
2948
|
}
|
|
2948
|
-
const rule$
|
|
2949
|
+
const rule$b = createRule$8({
|
|
2949
2950
|
create(context) {
|
|
2950
2951
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
2951
2952
|
const typeChecker = parserServices.program.getTypeChecker();
|
|
@@ -3041,6 +3042,207 @@ const rule$a = createRule$7({
|
|
|
3041
3042
|
name: 'no-redundant-type-annotation',
|
|
3042
3043
|
});
|
|
3043
3044
|
|
|
3045
|
+
/**
|
|
3046
|
+
* Strips TypeScript-only wrapper nodes that have no runtime meaning:
|
|
3047
|
+
* `as` casts, non-null assertions (`!`), type assertions (`<T>expr`), and
|
|
3048
|
+
* optional-chain wrappers. Iterates until no more wrappers are found.
|
|
3049
|
+
*/
|
|
3050
|
+
function unwrapExpression(expression) {
|
|
3051
|
+
let current = expression;
|
|
3052
|
+
let didUnwrap = true;
|
|
3053
|
+
while (didUnwrap) {
|
|
3054
|
+
didUnwrap = false;
|
|
3055
|
+
switch (current.type) {
|
|
3056
|
+
case AST_NODE_TYPES.ChainExpression:
|
|
3057
|
+
current = current.expression;
|
|
3058
|
+
didUnwrap = true;
|
|
3059
|
+
break;
|
|
3060
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
3061
|
+
current = current.expression;
|
|
3062
|
+
didUnwrap = true;
|
|
3063
|
+
break;
|
|
3064
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
3065
|
+
current = current.expression;
|
|
3066
|
+
didUnwrap = true;
|
|
3067
|
+
break;
|
|
3068
|
+
case AST_NODE_TYPES.TSTypeAssertion:
|
|
3069
|
+
current = current.expression;
|
|
3070
|
+
didUnwrap = true;
|
|
3071
|
+
break;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
return current;
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
const createRule$7 = ESLintUtils.RuleCreator((name) => name);
|
|
3078
|
+
function isReactiveCallback(node) {
|
|
3079
|
+
return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
3080
|
+
node?.type === AST_NODE_TYPES.FunctionExpression);
|
|
3081
|
+
}
|
|
3082
|
+
function unwrapMutationTarget(node) {
|
|
3083
|
+
let current = node;
|
|
3084
|
+
while (current.type === AST_NODE_TYPES.TSAsExpression ||
|
|
3085
|
+
current.type === AST_NODE_TYPES.TSNonNullExpression ||
|
|
3086
|
+
current.type === AST_NODE_TYPES.TSTypeAssertion) {
|
|
3087
|
+
current = current.expression;
|
|
3088
|
+
}
|
|
3089
|
+
return current;
|
|
3090
|
+
}
|
|
3091
|
+
function collectMutationTargets(node) {
|
|
3092
|
+
const current = unwrapMutationTarget(node);
|
|
3093
|
+
switch (current.type) {
|
|
3094
|
+
case AST_NODE_TYPES.ArrayPattern:
|
|
3095
|
+
return current.elements.flatMap((element) => element ? collectMutationTargets(element) : []);
|
|
3096
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
3097
|
+
return collectMutationTargets(current.left);
|
|
3098
|
+
case AST_NODE_TYPES.Identifier:
|
|
3099
|
+
return [current];
|
|
3100
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
3101
|
+
return [current];
|
|
3102
|
+
case AST_NODE_TYPES.ObjectPattern:
|
|
3103
|
+
return current.properties.flatMap((property) => {
|
|
3104
|
+
if (property.type === AST_NODE_TYPES.RestElement) {
|
|
3105
|
+
return collectMutationTargets(property.argument);
|
|
3106
|
+
}
|
|
3107
|
+
return collectMutationTargets(property.value);
|
|
3108
|
+
});
|
|
3109
|
+
case AST_NODE_TYPES.RestElement:
|
|
3110
|
+
return collectMutationTargets(current.argument);
|
|
3111
|
+
default:
|
|
3112
|
+
return [];
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
function getSymbolAtNode(node, checker, esTreeNodeToTSNodeMap) {
|
|
3116
|
+
const tsNode = esTreeNodeToTSNodeMap.get(node);
|
|
3117
|
+
if (!tsNode) {
|
|
3118
|
+
return null;
|
|
3119
|
+
}
|
|
3120
|
+
return checker.getSymbolAtLocation(tsNode) ?? null;
|
|
3121
|
+
}
|
|
3122
|
+
function isLocalIdentifier(node, context) {
|
|
3123
|
+
const symbol = getSymbolAtNode(node, context.checker, context.esTreeNodeToTSNodeMap);
|
|
3124
|
+
if (!symbol) {
|
|
3125
|
+
return false;
|
|
3126
|
+
}
|
|
3127
|
+
return (symbol.declarations ?? []).some((declaration) => {
|
|
3128
|
+
const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
|
|
3129
|
+
return (!!estreeDeclaration &&
|
|
3130
|
+
isNodeInsideSynchronousReactiveScope(estreeDeclaration, context.callback));
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
function isLocallyCreatedExpression(node, context) {
|
|
3134
|
+
const expression = unwrapExpression(node);
|
|
3135
|
+
switch (expression.type) {
|
|
3136
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
3137
|
+
case AST_NODE_TYPES.NewExpression:
|
|
3138
|
+
case AST_NODE_TYPES.ObjectExpression:
|
|
3139
|
+
return true;
|
|
3140
|
+
case AST_NODE_TYPES.Identifier:
|
|
3141
|
+
return isLocalIdentifier(expression, context);
|
|
3142
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
3143
|
+
return isLocallyCreatedExpression(expression.object, context);
|
|
3144
|
+
default:
|
|
3145
|
+
return false;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
function hasObservableMutationTarget(node, context) {
|
|
3149
|
+
return collectMutationTargets(node).some((target) => {
|
|
3150
|
+
if (target.type === AST_NODE_TYPES.Identifier) {
|
|
3151
|
+
return !isLocalIdentifier(target, context);
|
|
3152
|
+
}
|
|
3153
|
+
return !isLocallyCreatedExpression(target.object, context);
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
function reportSideEffect(node, inspectionContext, report) {
|
|
3157
|
+
const key = String(node.range);
|
|
3158
|
+
if (inspectionContext.reported.has(key)) {
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
inspectionContext.reported.add(key);
|
|
3162
|
+
report(node);
|
|
3163
|
+
}
|
|
3164
|
+
function inspectComputedBody(root, inspectionContext, report) {
|
|
3165
|
+
walkSynchronousAst(root, (node) => {
|
|
3166
|
+
if (node.type === AST_NODE_TYPES.CallExpression) {
|
|
3167
|
+
if (isWritableSignalWrite(node, inspectionContext.checker, inspectionContext.esTreeNodeToTSNodeMap)) {
|
|
3168
|
+
reportSideEffect(node, inspectionContext, report);
|
|
3169
|
+
}
|
|
3170
|
+
if (isAngularUntrackedCall(node, inspectionContext.program)) {
|
|
3171
|
+
const [arg] = node.arguments;
|
|
3172
|
+
if (isReactiveCallback(arg)) {
|
|
3173
|
+
inspectComputedBody(arg, inspectionContext, report);
|
|
3174
|
+
}
|
|
3175
|
+
return false;
|
|
3176
|
+
}
|
|
3177
|
+
if (node.callee.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
3178
|
+
node.callee.type === AST_NODE_TYPES.FunctionExpression) {
|
|
3179
|
+
inspectComputedBody(node.callee, inspectionContext, report);
|
|
3180
|
+
return false;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
if (node.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
3184
|
+
hasObservableMutationTarget(node.left, inspectionContext)) {
|
|
3185
|
+
reportSideEffect(node.left, inspectionContext, report);
|
|
3186
|
+
}
|
|
3187
|
+
if (node.type === AST_NODE_TYPES.UpdateExpression &&
|
|
3188
|
+
hasObservableMutationTarget(node.argument, inspectionContext)) {
|
|
3189
|
+
reportSideEffect(node.argument, inspectionContext, report);
|
|
3190
|
+
}
|
|
3191
|
+
if (node.type === AST_NODE_TYPES.UnaryExpression &&
|
|
3192
|
+
node.operator === 'delete' &&
|
|
3193
|
+
hasObservableMutationTarget(node.argument, inspectionContext)) {
|
|
3194
|
+
reportSideEffect(node.argument, inspectionContext, report);
|
|
3195
|
+
}
|
|
3196
|
+
return;
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
const rule$a = createRule$7({
|
|
3200
|
+
create(context) {
|
|
3201
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
3202
|
+
const checker = parserServices.program.getTypeChecker();
|
|
3203
|
+
const esTreeNodeToTSNodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
3204
|
+
const tsNodeToESTreeNodeMap = parserServices.tsNodeToESTreeNodeMap;
|
|
3205
|
+
const { sourceCode } = context;
|
|
3206
|
+
const program = sourceCode.ast;
|
|
3207
|
+
return {
|
|
3208
|
+
CallExpression(node) {
|
|
3209
|
+
for (const scope of getReactiveScopes(node, program)) {
|
|
3210
|
+
if (scope.kind !== 'computed()') {
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
const inspectionContext = {
|
|
3214
|
+
callback: scope.callback,
|
|
3215
|
+
checker,
|
|
3216
|
+
esTreeNodeToTSNodeMap,
|
|
3217
|
+
program,
|
|
3218
|
+
reported: new Set(),
|
|
3219
|
+
tsNodeToESTreeNodeMap,
|
|
3220
|
+
};
|
|
3221
|
+
inspectComputedBody(scope.callback, inspectionContext, (reportNode) => {
|
|
3222
|
+
context.report({
|
|
3223
|
+
data: { expression: sourceCode.getText(reportNode) },
|
|
3224
|
+
messageId: 'sideEffectInComputed',
|
|
3225
|
+
node: reportNode,
|
|
3226
|
+
});
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
},
|
|
3230
|
+
};
|
|
3231
|
+
},
|
|
3232
|
+
meta: {
|
|
3233
|
+
docs: {
|
|
3234
|
+
description: 'Disallow observable side effects inside `computed()` callbacks so derivations stay pure',
|
|
3235
|
+
url: 'https://angular.dev/guide/signals',
|
|
3236
|
+
},
|
|
3237
|
+
messages: {
|
|
3238
|
+
sideEffectInComputed: '`{{ expression }}` causes a side effect inside `computed()`. Keep computed derivations pure and move state changes to `effect()` or outside the computation.',
|
|
3239
|
+
},
|
|
3240
|
+
schema: [],
|
|
3241
|
+
type: 'problem',
|
|
3242
|
+
},
|
|
3243
|
+
name: 'no-side-effects-in-computed',
|
|
3244
|
+
});
|
|
3245
|
+
|
|
3044
3246
|
const rule$9 = createUntrackedRule({
|
|
3045
3247
|
create(context) {
|
|
3046
3248
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
@@ -3347,6 +3549,15 @@ function getObjectPropertyName(node) {
|
|
|
3347
3549
|
}
|
|
3348
3550
|
return typeof node.key.value === 'string' ? node.key.value : null;
|
|
3349
3551
|
}
|
|
3552
|
+
function getMemberExpressionPropertyName(node) {
|
|
3553
|
+
if (!node.computed && node.property.type === AST_NODE_TYPES.Identifier) {
|
|
3554
|
+
return node.property.name;
|
|
3555
|
+
}
|
|
3556
|
+
return node.property.type === AST_NODE_TYPES.Literal &&
|
|
3557
|
+
typeof node.property.value === 'string'
|
|
3558
|
+
? node.property.value
|
|
3559
|
+
: null;
|
|
3560
|
+
}
|
|
3350
3561
|
function getEnclosingClassMember(node) {
|
|
3351
3562
|
for (let current = node.parent; current; current = current.parent) {
|
|
3352
3563
|
if (current.type === AST_NODE_TYPES.MethodDefinition ||
|
|
@@ -3391,13 +3602,31 @@ function isPipeTransformMember(member) {
|
|
|
3391
3602
|
}
|
|
3392
3603
|
return hasNamedDecorator(member.parent.parent, 'Pipe');
|
|
3393
3604
|
}
|
|
3605
|
+
function hasAllowedImperativeAssignment(node) {
|
|
3606
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
3607
|
+
if (!isFunctionLike(current)) {
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
const parent = current.parent;
|
|
3611
|
+
if (parent.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
3612
|
+
parent.right === current &&
|
|
3613
|
+
parent.left.type === AST_NODE_TYPES.MemberExpression) {
|
|
3614
|
+
const memberName = getMemberExpressionPropertyName(parent.left);
|
|
3615
|
+
if (memberName && IMPERATIVE_UNTRACKED_METHODS.has(memberName)) {
|
|
3616
|
+
return true;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return false;
|
|
3621
|
+
}
|
|
3394
3622
|
function isAllowedImperativeAngularContext(node) {
|
|
3395
3623
|
const member = getEnclosingClassMember(node);
|
|
3396
3624
|
const memberName = member ? getClassMemberName(member) : null;
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3625
|
+
return ((!!member &&
|
|
3626
|
+
!!memberName &&
|
|
3627
|
+
(IMPERATIVE_UNTRACKED_METHODS.has(memberName) ||
|
|
3628
|
+
isPipeTransformMember(member))) ||
|
|
3629
|
+
hasAllowedImperativeAssignment(node));
|
|
3401
3630
|
}
|
|
3402
3631
|
function isDirectCallbackArgument(fn) {
|
|
3403
3632
|
const parent = fn.parent;
|
|
@@ -3792,38 +4021,6 @@ const rule$6 = createUntrackedRule({
|
|
|
3792
4021
|
name: 'no-useless-untracked',
|
|
3793
4022
|
});
|
|
3794
4023
|
|
|
3795
|
-
/**
|
|
3796
|
-
* Strips TypeScript-only wrapper nodes that have no runtime meaning:
|
|
3797
|
-
* `as` casts, non-null assertions (`!`), type assertions (`<T>expr`), and
|
|
3798
|
-
* optional-chain wrappers. Iterates until no more wrappers are found.
|
|
3799
|
-
*/
|
|
3800
|
-
function unwrapExpression(expression) {
|
|
3801
|
-
let current = expression;
|
|
3802
|
-
let didUnwrap = true;
|
|
3803
|
-
while (didUnwrap) {
|
|
3804
|
-
didUnwrap = false;
|
|
3805
|
-
switch (current.type) {
|
|
3806
|
-
case AST_NODE_TYPES.ChainExpression:
|
|
3807
|
-
current = current.expression;
|
|
3808
|
-
didUnwrap = true;
|
|
3809
|
-
break;
|
|
3810
|
-
case AST_NODE_TYPES.TSAsExpression:
|
|
3811
|
-
current = current.expression;
|
|
3812
|
-
didUnwrap = true;
|
|
3813
|
-
break;
|
|
3814
|
-
case AST_NODE_TYPES.TSNonNullExpression:
|
|
3815
|
-
current = current.expression;
|
|
3816
|
-
didUnwrap = true;
|
|
3817
|
-
break;
|
|
3818
|
-
case AST_NODE_TYPES.TSTypeAssertion:
|
|
3819
|
-
current = current.expression;
|
|
3820
|
-
didUnwrap = true;
|
|
3821
|
-
break;
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
|
-
return current;
|
|
3825
|
-
}
|
|
3826
|
-
|
|
3827
4024
|
const createRule$5 = ESLintUtils.RuleCreator((name) => name);
|
|
3828
4025
|
const rule$5 = createRule$5({
|
|
3829
4026
|
create(context, [{ printWidth }]) {
|
|
@@ -5388,16 +5585,17 @@ const plugin = {
|
|
|
5388
5585
|
'decorator-key-sort': config$3,
|
|
5389
5586
|
'flat-exports': flatExports,
|
|
5390
5587
|
'html-logical-properties': config$2,
|
|
5391
|
-
'injection-token-description': rule$
|
|
5392
|
-
'no-deep-imports': rule$
|
|
5588
|
+
'injection-token-description': rule$h,
|
|
5589
|
+
'no-deep-imports': rule$g,
|
|
5393
5590
|
'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
|
|
5394
|
-
'no-fully-untracked-effect': rule$
|
|
5591
|
+
'no-fully-untracked-effect': rule$f,
|
|
5395
5592
|
'no-href-with-router-link': config$1,
|
|
5396
|
-
'no-implicit-public': rule$
|
|
5397
|
-
'no-legacy-peer-deps': rule$
|
|
5398
|
-
'no-playwright-empty-fill': rule$
|
|
5593
|
+
'no-implicit-public': rule$e,
|
|
5594
|
+
'no-legacy-peer-deps': rule$d,
|
|
5595
|
+
'no-playwright-empty-fill': rule$c,
|
|
5399
5596
|
'no-project-as-in-ng-template': config,
|
|
5400
|
-
'no-redundant-type-annotation': rule$
|
|
5597
|
+
'no-redundant-type-annotation': rule$b,
|
|
5598
|
+
'no-side-effects-in-computed': rule$a,
|
|
5401
5599
|
'no-signal-reads-after-await-in-reactive-context': rule$9,
|
|
5402
5600
|
'no-string-literal-concat': rule$8,
|
|
5403
5601
|
'no-untracked-outside-reactive-context': rule$7,
|
package/package.json
CHANGED