@taiga-ui/eslint-plugin-experience-next 0.468.0 → 0.470.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 +65 -12
- package/index.d.ts +3 -0
- package/index.esm.js +262 -66
- package/package.json +1 -1
- package/rules/no-side-effects-in-computed.d.ts +5 -0
- package/rules/utils/angular-signals.d.ts +2 -0
package/README.md
CHANGED
|
@@ -53,9 +53,10 @@ 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-
|
|
56
|
+
| no-side-effects-in-computed | Disallow observable side effects inside Angular `computed()` callbacks | ✅ | | |
|
|
57
|
+
| no-signal-reads-after-await-in-reactive-context | Disallow bare signal reads after `await` inside reactive callbacks | ✅ | | |
|
|
57
58
|
| no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
|
|
58
|
-
| no-untracked-outside-reactive-context | Disallow `untracked()` outside
|
|
59
|
+
| no-untracked-outside-reactive-context | Disallow `untracked()` outside reactive callbacks, except explicit post-`await` snapshots | ✅ | 🔧 | |
|
|
59
60
|
| no-useless-untracked | Disallow provably useless `untracked()` wrappers in reactive callbacks | ✅ | 🔧 | |
|
|
60
61
|
| object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
|
|
61
62
|
| prefer-deep-imports | Allow deep imports of Taiga UI packages | | 🔧 | |
|
|
@@ -566,12 +567,44 @@ 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>
|
|
572
604
|
|
|
573
|
-
Angular tracks signal reads only in synchronous code. If a reactive callback crosses an async boundary, any signal
|
|
574
|
-
after `await` will not become a dependency.
|
|
605
|
+
Angular tracks signal reads only in synchronous code. If a reactive callback crosses an async boundary, any bare signal
|
|
606
|
+
read after `await` will not become a dependency. Snapshot before `await` when you need the earlier value, or make an
|
|
607
|
+
intentional post-`await` current-value read explicit with `untracked(...)`.
|
|
575
608
|
|
|
576
609
|
```ts
|
|
577
610
|
// ❌ error
|
|
@@ -588,6 +621,14 @@ effect(async () => {
|
|
|
588
621
|
});
|
|
589
622
|
```
|
|
590
623
|
|
|
624
|
+
```ts
|
|
625
|
+
// ✅ ok — explicit current-value read after await
|
|
626
|
+
effect(async () => {
|
|
627
|
+
await this.fetchUser();
|
|
628
|
+
console.log(untracked(this.theme));
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
591
632
|
---
|
|
592
633
|
|
|
593
634
|
## no-untracked-outside-reactive-context
|
|
@@ -595,14 +636,15 @@ effect(async () => {
|
|
|
595
636
|
<sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
|
|
596
637
|
|
|
597
638
|
`untracked()` usually only affects signal reads that happen inside the synchronous body of a reactive callback. In
|
|
598
|
-
ordinary non-reactive code
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
`registerOnChange` including patched accessors such as
|
|
602
|
-
inside deferred scheduler / event-handler callbacks,
|
|
603
|
-
`InjectionToken({factory})` / `useFactory` when they guard creation of a
|
|
604
|
-
accidental ambient reactive context. For the narrow case
|
|
605
|
-
around a reactive call in ordinary code, autofix removes only
|
|
639
|
+
ordinary non-reactive code or nested callbacks it usually does not prevent dependency tracking and only adds noise. This
|
|
640
|
+
rule reports those cases, but intentionally allows a few explicit escape hatches: post-`await` reads inside a reactive
|
|
641
|
+
callback when `untracked()` is used to document an intentional current-value snapshot, imperative Angular hooks such as
|
|
642
|
+
`@Pipe().transform`, `ControlValueAccessor.writeValue`, `registerOnChange` including patched accessors such as
|
|
643
|
+
`accessor.writeValue = (...) => {}`, callback-form wrappers used inside deferred scheduler / event-handler callbacks,
|
|
644
|
+
and narrow lazy DI factory wrappers like `InjectionToken({factory})` / `useFactory` when they guard creation of a
|
|
645
|
+
reactive owner such as `effect()` against an accidental ambient reactive context. For the narrow case
|
|
646
|
+
`untracked(() => effect(...))` and similar outer wrappers around a reactive call in ordinary code, autofix removes only
|
|
647
|
+
the useless outer `untracked()` wrapper.
|
|
606
648
|
|
|
607
649
|
```ts
|
|
608
650
|
// ❌ error
|
|
@@ -624,6 +666,17 @@ effect(() => {
|
|
|
624
666
|
const snapshot = computed(() => untracked(this.user));
|
|
625
667
|
```
|
|
626
668
|
|
|
669
|
+
```ts
|
|
670
|
+
// ✅ ok — after await, untracked can mark an intentional current snapshot
|
|
671
|
+
effect(async () => {
|
|
672
|
+
await this.refresh();
|
|
673
|
+
|
|
674
|
+
if (untracked(this.user) !== previousUser) {
|
|
675
|
+
console.log('changed');
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
627
680
|
```ts
|
|
628
681
|
// ❌ error
|
|
629
682
|
untracked(() => {
|
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({
|
|
@@ -2396,6 +2397,16 @@ function isNodeInsideSynchronousReactiveScope(node, callback) {
|
|
|
2396
2397
|
});
|
|
2397
2398
|
return found;
|
|
2398
2399
|
}
|
|
2400
|
+
function isNodeAfterAsyncBoundaryInReactiveScope(node, callback) {
|
|
2401
|
+
let found = false;
|
|
2402
|
+
walkAfterAsyncBoundaryAst(callback, (inner) => {
|
|
2403
|
+
if (inner !== node) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
found = true;
|
|
2407
|
+
});
|
|
2408
|
+
return found;
|
|
2409
|
+
}
|
|
2399
2410
|
function findEnclosingReactiveScope(node, program) {
|
|
2400
2411
|
for (let current = node.parent; current; current = current.parent) {
|
|
2401
2412
|
if (current.type !== AST_NODE_TYPES.CallExpression) {
|
|
@@ -2409,6 +2420,19 @@ function findEnclosingReactiveScope(node, program) {
|
|
|
2409
2420
|
}
|
|
2410
2421
|
return null;
|
|
2411
2422
|
}
|
|
2423
|
+
function findEnclosingReactiveScopeAfterAsyncBoundary(node, program) {
|
|
2424
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2425
|
+
if (current.type !== AST_NODE_TYPES.CallExpression) {
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
for (const scope of getReactiveScopes(current, program)) {
|
|
2429
|
+
if (isNodeAfterAsyncBoundaryInReactiveScope(node, scope.callback)) {
|
|
2430
|
+
return scope;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2412
2436
|
/**
|
|
2413
2437
|
* Returns true when the TypeScript type at `node` is an Angular signal type.
|
|
2414
2438
|
* Uses duck-typing: callable type whose name contains "Signal", or whose
|
|
@@ -2555,7 +2579,7 @@ function collectReadsInsideUntracked(root, checker, esTreeNodeToTSNodeMap, progr
|
|
|
2555
2579
|
});
|
|
2556
2580
|
return reads;
|
|
2557
2581
|
}
|
|
2558
|
-
const rule$
|
|
2582
|
+
const rule$f = createUntrackedRule({
|
|
2559
2583
|
create(context) {
|
|
2560
2584
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
2561
2585
|
const checker = parserServices.program.getTypeChecker();
|
|
@@ -2641,8 +2665,8 @@ const config$1 = {
|
|
|
2641
2665
|
},
|
|
2642
2666
|
};
|
|
2643
2667
|
|
|
2644
|
-
const createRule$
|
|
2645
|
-
const rule$
|
|
2668
|
+
const createRule$b = ESLintUtils.RuleCreator((name) => name);
|
|
2669
|
+
const rule$e = createRule$b({
|
|
2646
2670
|
create(context) {
|
|
2647
2671
|
const checkImplicitPublic = (node) => {
|
|
2648
2672
|
const classRef = getClass(node);
|
|
@@ -2713,9 +2737,9 @@ function getClass(node) {
|
|
|
2713
2737
|
return getClass(node.parent);
|
|
2714
2738
|
}
|
|
2715
2739
|
|
|
2716
|
-
const createRule$
|
|
2740
|
+
const createRule$a = ESLintUtils.RuleCreator((name) => name);
|
|
2717
2741
|
const LEGACY_PEER_DEPS_PATTERN = /^legacy-peer-deps\s*=\s*true$/i;
|
|
2718
|
-
const rule$
|
|
2742
|
+
const rule$d = createRule$a({
|
|
2719
2743
|
create(context) {
|
|
2720
2744
|
return {
|
|
2721
2745
|
Program(node) {
|
|
@@ -2753,8 +2777,8 @@ const rule$c = createRule$9({
|
|
|
2753
2777
|
name: 'no-legacy-peer-deps',
|
|
2754
2778
|
});
|
|
2755
2779
|
|
|
2756
|
-
const createRule$
|
|
2757
|
-
const rule$
|
|
2780
|
+
const createRule$9 = ESLintUtils.RuleCreator((name) => name);
|
|
2781
|
+
const rule$c = createRule$9({
|
|
2758
2782
|
create(context) {
|
|
2759
2783
|
const services = ESLintUtils.getParserServices(context);
|
|
2760
2784
|
const checker = services.program.getTypeChecker();
|
|
@@ -2919,7 +2943,7 @@ const config = {
|
|
|
2919
2943
|
},
|
|
2920
2944
|
};
|
|
2921
2945
|
|
|
2922
|
-
const createRule$
|
|
2946
|
+
const createRule$8 = ESLintUtils.RuleCreator((name) => name);
|
|
2923
2947
|
function collectArrayExpressions(node) {
|
|
2924
2948
|
const result = [];
|
|
2925
2949
|
if (node.type === AST_NODE_TYPES.ArrayExpression) {
|
|
@@ -2945,7 +2969,7 @@ function collectArrayExpressions(node) {
|
|
|
2945
2969
|
}
|
|
2946
2970
|
return result;
|
|
2947
2971
|
}
|
|
2948
|
-
const rule$
|
|
2972
|
+
const rule$b = createRule$8({
|
|
2949
2973
|
create(context) {
|
|
2950
2974
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
2951
2975
|
const typeChecker = parserServices.program.getTypeChecker();
|
|
@@ -3041,6 +3065,207 @@ const rule$a = createRule$7({
|
|
|
3041
3065
|
name: 'no-redundant-type-annotation',
|
|
3042
3066
|
});
|
|
3043
3067
|
|
|
3068
|
+
/**
|
|
3069
|
+
* Strips TypeScript-only wrapper nodes that have no runtime meaning:
|
|
3070
|
+
* `as` casts, non-null assertions (`!`), type assertions (`<T>expr`), and
|
|
3071
|
+
* optional-chain wrappers. Iterates until no more wrappers are found.
|
|
3072
|
+
*/
|
|
3073
|
+
function unwrapExpression(expression) {
|
|
3074
|
+
let current = expression;
|
|
3075
|
+
let didUnwrap = true;
|
|
3076
|
+
while (didUnwrap) {
|
|
3077
|
+
didUnwrap = false;
|
|
3078
|
+
switch (current.type) {
|
|
3079
|
+
case AST_NODE_TYPES.ChainExpression:
|
|
3080
|
+
current = current.expression;
|
|
3081
|
+
didUnwrap = true;
|
|
3082
|
+
break;
|
|
3083
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
3084
|
+
current = current.expression;
|
|
3085
|
+
didUnwrap = true;
|
|
3086
|
+
break;
|
|
3087
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
3088
|
+
current = current.expression;
|
|
3089
|
+
didUnwrap = true;
|
|
3090
|
+
break;
|
|
3091
|
+
case AST_NODE_TYPES.TSTypeAssertion:
|
|
3092
|
+
current = current.expression;
|
|
3093
|
+
didUnwrap = true;
|
|
3094
|
+
break;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return current;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
const createRule$7 = ESLintUtils.RuleCreator((name) => name);
|
|
3101
|
+
function isReactiveCallback(node) {
|
|
3102
|
+
return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
3103
|
+
node?.type === AST_NODE_TYPES.FunctionExpression);
|
|
3104
|
+
}
|
|
3105
|
+
function unwrapMutationTarget(node) {
|
|
3106
|
+
let current = node;
|
|
3107
|
+
while (current.type === AST_NODE_TYPES.TSAsExpression ||
|
|
3108
|
+
current.type === AST_NODE_TYPES.TSNonNullExpression ||
|
|
3109
|
+
current.type === AST_NODE_TYPES.TSTypeAssertion) {
|
|
3110
|
+
current = current.expression;
|
|
3111
|
+
}
|
|
3112
|
+
return current;
|
|
3113
|
+
}
|
|
3114
|
+
function collectMutationTargets(node) {
|
|
3115
|
+
const current = unwrapMutationTarget(node);
|
|
3116
|
+
switch (current.type) {
|
|
3117
|
+
case AST_NODE_TYPES.ArrayPattern:
|
|
3118
|
+
return current.elements.flatMap((element) => element ? collectMutationTargets(element) : []);
|
|
3119
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
3120
|
+
return collectMutationTargets(current.left);
|
|
3121
|
+
case AST_NODE_TYPES.Identifier:
|
|
3122
|
+
return [current];
|
|
3123
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
3124
|
+
return [current];
|
|
3125
|
+
case AST_NODE_TYPES.ObjectPattern:
|
|
3126
|
+
return current.properties.flatMap((property) => {
|
|
3127
|
+
if (property.type === AST_NODE_TYPES.RestElement) {
|
|
3128
|
+
return collectMutationTargets(property.argument);
|
|
3129
|
+
}
|
|
3130
|
+
return collectMutationTargets(property.value);
|
|
3131
|
+
});
|
|
3132
|
+
case AST_NODE_TYPES.RestElement:
|
|
3133
|
+
return collectMutationTargets(current.argument);
|
|
3134
|
+
default:
|
|
3135
|
+
return [];
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
function getSymbolAtNode(node, checker, esTreeNodeToTSNodeMap) {
|
|
3139
|
+
const tsNode = esTreeNodeToTSNodeMap.get(node);
|
|
3140
|
+
if (!tsNode) {
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
return checker.getSymbolAtLocation(tsNode) ?? null;
|
|
3144
|
+
}
|
|
3145
|
+
function isLocalIdentifier(node, context) {
|
|
3146
|
+
const symbol = getSymbolAtNode(node, context.checker, context.esTreeNodeToTSNodeMap);
|
|
3147
|
+
if (!symbol) {
|
|
3148
|
+
return false;
|
|
3149
|
+
}
|
|
3150
|
+
return (symbol.declarations ?? []).some((declaration) => {
|
|
3151
|
+
const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
|
|
3152
|
+
return (!!estreeDeclaration &&
|
|
3153
|
+
isNodeInsideSynchronousReactiveScope(estreeDeclaration, context.callback));
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
function isLocallyCreatedExpression(node, context) {
|
|
3157
|
+
const expression = unwrapExpression(node);
|
|
3158
|
+
switch (expression.type) {
|
|
3159
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
3160
|
+
case AST_NODE_TYPES.NewExpression:
|
|
3161
|
+
case AST_NODE_TYPES.ObjectExpression:
|
|
3162
|
+
return true;
|
|
3163
|
+
case AST_NODE_TYPES.Identifier:
|
|
3164
|
+
return isLocalIdentifier(expression, context);
|
|
3165
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
3166
|
+
return isLocallyCreatedExpression(expression.object, context);
|
|
3167
|
+
default:
|
|
3168
|
+
return false;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function hasObservableMutationTarget(node, context) {
|
|
3172
|
+
return collectMutationTargets(node).some((target) => {
|
|
3173
|
+
if (target.type === AST_NODE_TYPES.Identifier) {
|
|
3174
|
+
return !isLocalIdentifier(target, context);
|
|
3175
|
+
}
|
|
3176
|
+
return !isLocallyCreatedExpression(target.object, context);
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
function reportSideEffect(node, inspectionContext, report) {
|
|
3180
|
+
const key = String(node.range);
|
|
3181
|
+
if (inspectionContext.reported.has(key)) {
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
inspectionContext.reported.add(key);
|
|
3185
|
+
report(node);
|
|
3186
|
+
}
|
|
3187
|
+
function inspectComputedBody(root, inspectionContext, report) {
|
|
3188
|
+
walkSynchronousAst(root, (node) => {
|
|
3189
|
+
if (node.type === AST_NODE_TYPES.CallExpression) {
|
|
3190
|
+
if (isWritableSignalWrite(node, inspectionContext.checker, inspectionContext.esTreeNodeToTSNodeMap)) {
|
|
3191
|
+
reportSideEffect(node, inspectionContext, report);
|
|
3192
|
+
}
|
|
3193
|
+
if (isAngularUntrackedCall(node, inspectionContext.program)) {
|
|
3194
|
+
const [arg] = node.arguments;
|
|
3195
|
+
if (isReactiveCallback(arg)) {
|
|
3196
|
+
inspectComputedBody(arg, inspectionContext, report);
|
|
3197
|
+
}
|
|
3198
|
+
return false;
|
|
3199
|
+
}
|
|
3200
|
+
if (node.callee.type === AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
3201
|
+
node.callee.type === AST_NODE_TYPES.FunctionExpression) {
|
|
3202
|
+
inspectComputedBody(node.callee, inspectionContext, report);
|
|
3203
|
+
return false;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
if (node.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
3207
|
+
hasObservableMutationTarget(node.left, inspectionContext)) {
|
|
3208
|
+
reportSideEffect(node.left, inspectionContext, report);
|
|
3209
|
+
}
|
|
3210
|
+
if (node.type === AST_NODE_TYPES.UpdateExpression &&
|
|
3211
|
+
hasObservableMutationTarget(node.argument, inspectionContext)) {
|
|
3212
|
+
reportSideEffect(node.argument, inspectionContext, report);
|
|
3213
|
+
}
|
|
3214
|
+
if (node.type === AST_NODE_TYPES.UnaryExpression &&
|
|
3215
|
+
node.operator === 'delete' &&
|
|
3216
|
+
hasObservableMutationTarget(node.argument, inspectionContext)) {
|
|
3217
|
+
reportSideEffect(node.argument, inspectionContext, report);
|
|
3218
|
+
}
|
|
3219
|
+
return;
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
const rule$a = createRule$7({
|
|
3223
|
+
create(context) {
|
|
3224
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
3225
|
+
const checker = parserServices.program.getTypeChecker();
|
|
3226
|
+
const esTreeNodeToTSNodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
3227
|
+
const tsNodeToESTreeNodeMap = parserServices.tsNodeToESTreeNodeMap;
|
|
3228
|
+
const { sourceCode } = context;
|
|
3229
|
+
const program = sourceCode.ast;
|
|
3230
|
+
return {
|
|
3231
|
+
CallExpression(node) {
|
|
3232
|
+
for (const scope of getReactiveScopes(node, program)) {
|
|
3233
|
+
if (scope.kind !== 'computed()') {
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
const inspectionContext = {
|
|
3237
|
+
callback: scope.callback,
|
|
3238
|
+
checker,
|
|
3239
|
+
esTreeNodeToTSNodeMap,
|
|
3240
|
+
program,
|
|
3241
|
+
reported: new Set(),
|
|
3242
|
+
tsNodeToESTreeNodeMap,
|
|
3243
|
+
};
|
|
3244
|
+
inspectComputedBody(scope.callback, inspectionContext, (reportNode) => {
|
|
3245
|
+
context.report({
|
|
3246
|
+
data: { expression: sourceCode.getText(reportNode) },
|
|
3247
|
+
messageId: 'sideEffectInComputed',
|
|
3248
|
+
node: reportNode,
|
|
3249
|
+
});
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
},
|
|
3253
|
+
};
|
|
3254
|
+
},
|
|
3255
|
+
meta: {
|
|
3256
|
+
docs: {
|
|
3257
|
+
description: 'Disallow observable side effects inside `computed()` callbacks so derivations stay pure',
|
|
3258
|
+
url: 'https://angular.dev/guide/signals',
|
|
3259
|
+
},
|
|
3260
|
+
messages: {
|
|
3261
|
+
sideEffectInComputed: '`{{ expression }}` causes a side effect inside `computed()`. Keep computed derivations pure and move state changes to `effect()` or outside the computation.',
|
|
3262
|
+
},
|
|
3263
|
+
schema: [],
|
|
3264
|
+
type: 'problem',
|
|
3265
|
+
},
|
|
3266
|
+
name: 'no-side-effects-in-computed',
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3044
3269
|
const rule$9 = createUntrackedRule({
|
|
3045
3270
|
create(context) {
|
|
3046
3271
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
@@ -3054,6 +3279,7 @@ const rule$9 = createUntrackedRule({
|
|
|
3054
3279
|
const reported = new Set();
|
|
3055
3280
|
walkAfterAsyncBoundaryAst(scope.callback, (inner) => {
|
|
3056
3281
|
if (inner.type !== AST_NODE_TYPES.CallExpression ||
|
|
3282
|
+
isAngularUntrackedCall(inner, program) ||
|
|
3057
3283
|
!isSignalReadCall(inner, checker, esTreeNodeToTSNodeMap)) {
|
|
3058
3284
|
return;
|
|
3059
3285
|
}
|
|
@@ -3077,11 +3303,11 @@ const rule$9 = createUntrackedRule({
|
|
|
3077
3303
|
},
|
|
3078
3304
|
meta: {
|
|
3079
3305
|
docs: {
|
|
3080
|
-
description: 'Disallow signal reads that occur after `await` inside reactive callbacks, because Angular no longer tracks them as dependencies',
|
|
3306
|
+
description: 'Disallow bare signal reads that occur after `await` inside reactive callbacks, because Angular no longer tracks them as dependencies',
|
|
3081
3307
|
url: ANGULAR_SIGNALS_ASYNC_GUIDE_URL,
|
|
3082
3308
|
},
|
|
3083
3309
|
messages: {
|
|
3084
|
-
readAfterAwait: '`{{ name }}` is read after `await` inside `{{ kind }}`. Angular only tracks synchronous signal reads, so this dependency will not be tracked. Read it before `await` and store the snapshot. See Angular guide: https://angular.dev/guide/signals#reactive-context-and-async-operations',
|
|
3310
|
+
readAfterAwait: '`{{ name }}` is read after `await` inside `{{ kind }}`. Angular only tracks synchronous signal reads, so this dependency will not be tracked. Read it before `await` and store the snapshot, or wrap the post-`await` read in `untracked(...)` when you intentionally need the current value at that point. See Angular guide: https://angular.dev/guide/signals#reactive-context-and-async-operations',
|
|
3085
3311
|
},
|
|
3086
3312
|
schema: [],
|
|
3087
3313
|
type: 'problem',
|
|
@@ -3600,7 +3826,8 @@ const rule$7 = createUntrackedRule({
|
|
|
3600
3826
|
if (!isAngularUntrackedCall(node, program)) {
|
|
3601
3827
|
return;
|
|
3602
3828
|
}
|
|
3603
|
-
if (findEnclosingReactiveScope(node, program)
|
|
3829
|
+
if (findEnclosingReactiveScope(node, program) ||
|
|
3830
|
+
findEnclosingReactiveScopeAfterAsyncBoundary(node, program)) {
|
|
3604
3831
|
return;
|
|
3605
3832
|
}
|
|
3606
3833
|
if (isAllowedImperativeAngularContext(node)) {
|
|
@@ -3638,12 +3865,12 @@ const rule$7 = createUntrackedRule({
|
|
|
3638
3865
|
},
|
|
3639
3866
|
meta: {
|
|
3640
3867
|
docs: {
|
|
3641
|
-
description: 'Disallow `untracked()` outside
|
|
3868
|
+
description: 'Disallow `untracked()` outside reactive callbacks, except for synchronous reactive reads, explicit post-`await` snapshot reads, and supported imperative/deferred/lazy-factory Angular escape hatches',
|
|
3642
3869
|
url: ANGULAR_SIGNALS_UNTRACKED_GUIDE_URL,
|
|
3643
3870
|
},
|
|
3644
3871
|
fixable: 'code',
|
|
3645
3872
|
messages: {
|
|
3646
|
-
outsideReactiveContext: '`untracked()` is used outside
|
|
3873
|
+
outsideReactiveContext: '`untracked()` is used outside a reactive callback and outside the supported post-`await` / imperative / deferred / lazy-factory exceptions, so it does not prevent dependency tracking and only adds noise. Remove it. See Angular guide: https://angular.dev/guide/signals#reading-without-tracking-dependencies',
|
|
3647
3874
|
},
|
|
3648
3875
|
schema: [],
|
|
3649
3876
|
type: 'problem',
|
|
@@ -3819,38 +4046,6 @@ const rule$6 = createUntrackedRule({
|
|
|
3819
4046
|
name: 'no-useless-untracked',
|
|
3820
4047
|
});
|
|
3821
4048
|
|
|
3822
|
-
/**
|
|
3823
|
-
* Strips TypeScript-only wrapper nodes that have no runtime meaning:
|
|
3824
|
-
* `as` casts, non-null assertions (`!`), type assertions (`<T>expr`), and
|
|
3825
|
-
* optional-chain wrappers. Iterates until no more wrappers are found.
|
|
3826
|
-
*/
|
|
3827
|
-
function unwrapExpression(expression) {
|
|
3828
|
-
let current = expression;
|
|
3829
|
-
let didUnwrap = true;
|
|
3830
|
-
while (didUnwrap) {
|
|
3831
|
-
didUnwrap = false;
|
|
3832
|
-
switch (current.type) {
|
|
3833
|
-
case AST_NODE_TYPES.ChainExpression:
|
|
3834
|
-
current = current.expression;
|
|
3835
|
-
didUnwrap = true;
|
|
3836
|
-
break;
|
|
3837
|
-
case AST_NODE_TYPES.TSAsExpression:
|
|
3838
|
-
current = current.expression;
|
|
3839
|
-
didUnwrap = true;
|
|
3840
|
-
break;
|
|
3841
|
-
case AST_NODE_TYPES.TSNonNullExpression:
|
|
3842
|
-
current = current.expression;
|
|
3843
|
-
didUnwrap = true;
|
|
3844
|
-
break;
|
|
3845
|
-
case AST_NODE_TYPES.TSTypeAssertion:
|
|
3846
|
-
current = current.expression;
|
|
3847
|
-
didUnwrap = true;
|
|
3848
|
-
break;
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
return current;
|
|
3852
|
-
}
|
|
3853
|
-
|
|
3854
4049
|
const createRule$5 = ESLintUtils.RuleCreator((name) => name);
|
|
3855
4050
|
const rule$5 = createRule$5({
|
|
3856
4051
|
create(context, [{ printWidth }]) {
|
|
@@ -5415,16 +5610,17 @@ const plugin = {
|
|
|
5415
5610
|
'decorator-key-sort': config$3,
|
|
5416
5611
|
'flat-exports': flatExports,
|
|
5417
5612
|
'html-logical-properties': config$2,
|
|
5418
|
-
'injection-token-description': rule$
|
|
5419
|
-
'no-deep-imports': rule$
|
|
5613
|
+
'injection-token-description': rule$h,
|
|
5614
|
+
'no-deep-imports': rule$g,
|
|
5420
5615
|
'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
|
|
5421
|
-
'no-fully-untracked-effect': rule$
|
|
5616
|
+
'no-fully-untracked-effect': rule$f,
|
|
5422
5617
|
'no-href-with-router-link': config$1,
|
|
5423
|
-
'no-implicit-public': rule$
|
|
5424
|
-
'no-legacy-peer-deps': rule$
|
|
5425
|
-
'no-playwright-empty-fill': rule$
|
|
5618
|
+
'no-implicit-public': rule$e,
|
|
5619
|
+
'no-legacy-peer-deps': rule$d,
|
|
5620
|
+
'no-playwright-empty-fill': rule$c,
|
|
5426
5621
|
'no-project-as-in-ng-template': config,
|
|
5427
|
-
'no-redundant-type-annotation': rule$
|
|
5622
|
+
'no-redundant-type-annotation': rule$b,
|
|
5623
|
+
'no-side-effects-in-computed': rule$a,
|
|
5428
5624
|
'no-signal-reads-after-await-in-reactive-context': rule$9,
|
|
5429
5625
|
'no-string-literal-concat': rule$8,
|
|
5430
5626
|
'no-untracked-outside-reactive-context': rule$7,
|
package/package.json
CHANGED
|
@@ -20,7 +20,9 @@ export declare function isAngularEffectCall(node: TSESTree.CallExpression, progr
|
|
|
20
20
|
export declare function isAngularUntrackedCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
21
21
|
export declare function getReactiveScopes(node: TSESTree.CallExpression, program: TSESTree.Program): ReactiveScope[];
|
|
22
22
|
export declare function isNodeInsideSynchronousReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;
|
|
23
|
+
export declare function isNodeAfterAsyncBoundaryInReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;
|
|
23
24
|
export declare function findEnclosingReactiveScope(node: TSESTree.Node, program: TSESTree.Program): ReactiveScope | null;
|
|
25
|
+
export declare function findEnclosingReactiveScopeAfterAsyncBoundary(node: TSESTree.Node, program: TSESTree.Program): ReactiveScope | null;
|
|
24
26
|
/**
|
|
25
27
|
* Returns true when the TypeScript type at `node` is an Angular signal type.
|
|
26
28
|
* Uses duck-typing: callable type whose name contains "Signal", or whose
|