@taiga-ui/eslint-plugin-experience-next 0.466.0 → 0.467.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 +385 -21
- package/index.d.ts +18 -0
- package/index.esm.js +1591 -66
- package/package.json +1 -1
- package/rules/no-fully-untracked-effect.d.ts +5 -0
- package/rules/no-signal-reads-after-await-in-reactive-context.d.ts +5 -0
- package/rules/no-untracked-outside-reactive-context.d.ts +5 -0
- package/rules/no-useless-untracked.d.ts +5 -0
- package/rules/prefer-untracked-incidental-signal-reads.d.ts +30 -0
- package/rules/prefer-untracked-signal-getter.d.ts +5 -0
- package/rules/utils/angular-imports.d.ts +14 -0
- package/rules/utils/angular-signals.d.ts +45 -0
- package/rules/utils/ast-expressions.d.ts +7 -0
- package/rules/utils/ast-walk.d.ts +30 -0
- package/rules/utils/import-fix-helpers.d.ts +14 -0
- package/rules/utils/untracked-docs.d.ts +7 -0
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prefer-untracked-incidental-signal-reads
|
|
3
|
+
*
|
|
4
|
+
* Conservatively flags signal reads inside reactive callbacks that appear to be
|
|
5
|
+
* incidental — they provide only a snapshot value passed to a side-effecting
|
|
6
|
+
* consumer such as a signal write or DOM imperative call — and therefore
|
|
7
|
+
* probably should be wrapped in `untracked(...)`.
|
|
8
|
+
*
|
|
9
|
+
* Design principle: prefer false-negatives over false-positives.
|
|
10
|
+
* This rule only reports patterns it can confidently identify as suspicious.
|
|
11
|
+
*
|
|
12
|
+
* Limitations
|
|
13
|
+
* -----------
|
|
14
|
+
* Whether a signal read *should* be tracked is a question of developer intent,
|
|
15
|
+
* not syntax. The rule relies on heuristics (type-based signal detection,
|
|
16
|
+
* structural pattern matching) and will inevitably produce:
|
|
17
|
+
*
|
|
18
|
+
* - False negatives: incidental reads that do not match the heuristics.
|
|
19
|
+
* - False positives (rare): reads that look incidental but are intentional
|
|
20
|
+
* dependencies (e.g. the developer intentionally re-runs the effect when
|
|
21
|
+
* `mousePosition` changes to keep `position` in sync).
|
|
22
|
+
*
|
|
23
|
+
* Always review suggestions before accepting the fix. If the reported read
|
|
24
|
+
* IS meant to be a tracked dependency, disable the rule for that line.
|
|
25
|
+
*/
|
|
26
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
27
|
+
export declare const rule: ESLintUtils.RuleModule<"incidentalRead", [], unknown, ESLintUtils.RuleListener> & {
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
export default rule;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the local name bound to a named import from a given source.
|
|
4
|
+
* Handles aliased imports: `import { untracked as ngUntracked } from '@angular/core'`
|
|
5
|
+
* returns `'ngUntracked'` for `exportedName = 'untracked'`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getLocalNameForImport(program: TSESTree.Program, source: string, exportedName: string): string | null;
|
|
8
|
+
export declare function findAngularCoreImports(program: TSESTree.Program): TSESTree.ImportDeclaration[];
|
|
9
|
+
export declare function findAngularCoreImport(program: TSESTree.Program): TSESTree.ImportDeclaration | null;
|
|
10
|
+
export declare function findRuntimeAngularCoreImport(program: TSESTree.Program): TSESTree.ImportDeclaration | null;
|
|
11
|
+
export declare function findAngularCoreImportSpecifier(program: TSESTree.Program, exportedName: string): {
|
|
12
|
+
readonly importDecl: TSESTree.ImportDeclaration;
|
|
13
|
+
readonly specifier: TSESTree.ImportSpecifier;
|
|
14
|
+
} | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
export { findAngularCoreImport, findAngularCoreImports, findAngularCoreImportSpecifier, findRuntimeAngularCoreImport, getLocalNameForImport, } from './angular-imports';
|
|
4
|
+
export { walkAfterAsyncBoundaryAst, walkAst, walkSynchronousAst } from './ast-walk';
|
|
5
|
+
export interface NodeMap {
|
|
6
|
+
get(node: TSESTree.Node): ts.Node | undefined;
|
|
7
|
+
}
|
|
8
|
+
type ReactiveCallback = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression;
|
|
9
|
+
export interface ReactiveScope {
|
|
10
|
+
readonly callback: ReactiveCallback;
|
|
11
|
+
readonly kind: string;
|
|
12
|
+
readonly owner: TSESTree.CallExpression;
|
|
13
|
+
readonly reportNode: TSESTree.Node;
|
|
14
|
+
}
|
|
15
|
+
export interface SignalUsage {
|
|
16
|
+
readonly reads: TSESTree.CallExpression[];
|
|
17
|
+
readonly writes: TSESTree.CallExpression[];
|
|
18
|
+
}
|
|
19
|
+
export declare function isAngularEffectCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
20
|
+
export declare function isAngularUntrackedCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
21
|
+
export declare function getReactiveScopes(node: TSESTree.CallExpression, program: TSESTree.Program): ReactiveScope[];
|
|
22
|
+
export declare function isNodeInsideSynchronousReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;
|
|
23
|
+
export declare function findEnclosingReactiveScope(node: TSESTree.Node, program: TSESTree.Program): ReactiveScope | null;
|
|
24
|
+
/**
|
|
25
|
+
* Returns true when the TypeScript type at `node` is an Angular signal type.
|
|
26
|
+
* Uses duck-typing: callable type whose name contains "Signal", or whose
|
|
27
|
+
* string-keyed properties include the Angular `ɵ` brand.
|
|
28
|
+
*
|
|
29
|
+
* Falls back to `false` on any error.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSignalType(node: TSESTree.Node, checker: ts.TypeChecker, esTreeNodeToTSNodeMap: NodeMap): boolean;
|
|
32
|
+
export declare function isSignalReadCall(node: TSESTree.CallExpression, checker: ts.TypeChecker, esTreeNodeToTSNodeMap: NodeMap): boolean;
|
|
33
|
+
export declare function isWritableSignalWrite(node: TSESTree.CallExpression, checker: ts.TypeChecker, esTreeNodeToTSNodeMap: NodeMap): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when `node` is a member expression `foo.bar` where `bar` is a
|
|
36
|
+
* TypeScript getter. Getter accesses are opaque — the getter body can read
|
|
37
|
+
* signals, so wrapping them in `untracked()` is justified.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isGetterMemberAccess(node: TSESTree.MemberExpression, checker: ts.TypeChecker, esTreeNodeToTSNodeMap: NodeMap): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Walks `scopeNode` and collects all signal reads and writes within it,
|
|
42
|
+
* NOT recursing into nested `untracked(...)` calls (their contents are
|
|
43
|
+
* already hidden from tracking).
|
|
44
|
+
*/
|
|
45
|
+
export declare function collectSignalUsages(scopeNode: TSESTree.Node, checker: ts.TypeChecker, esTreeNodeToTSNodeMap: NodeMap, program: TSESTree.Program): SignalUsage;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Strips TypeScript-only wrapper nodes that have no runtime meaning:
|
|
4
|
+
* `as` casts, non-null assertions (`!`), type assertions (`<T>expr`), and
|
|
5
|
+
* optional-chain wrappers. Iterates until no more wrappers are found.
|
|
6
|
+
*/
|
|
7
|
+
export declare function unwrapExpression(expression: TSESTree.Expression): TSESTree.Expression;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Walks the synchronous portion of a reactive scope.
|
|
4
|
+
*
|
|
5
|
+
* - Stops descending into nested function boundaries, since they run in their
|
|
6
|
+
* own call context and should not be treated as reads/writes of the current
|
|
7
|
+
* reactive scope.
|
|
8
|
+
* - Traverses the argument of `await`, because it is evaluated synchronously,
|
|
9
|
+
* then stops visiting subsequent sibling nodes in the current execution path.
|
|
10
|
+
*/
|
|
11
|
+
export declare function walkSynchronousAst(root: TSESTree.Node, visitor: (node: TSESTree.Node) => false | void): void;
|
|
12
|
+
/**
|
|
13
|
+
* Walks nodes that run after an async boundary inside a reactive callback.
|
|
14
|
+
*
|
|
15
|
+
* The traversal is intentionally conservative:
|
|
16
|
+
* - it never descends into nested function boundaries
|
|
17
|
+
* - it propagates async state through straight-line code
|
|
18
|
+
* - it does not propagate branch-local `await` from optional control-flow
|
|
19
|
+
* bodies into later siblings, which avoids noisy false positives
|
|
20
|
+
*/
|
|
21
|
+
export declare function walkAfterAsyncBoundaryAst(root: TSESTree.Node, visitor: (node: TSESTree.Node) => void): void;
|
|
22
|
+
/**
|
|
23
|
+
* Shallow AST walker. Visits `root` and every descendant, skipping the
|
|
24
|
+
* synthetic `parent` back-pointer to avoid cycles.
|
|
25
|
+
*
|
|
26
|
+
* If the visitor returns `false` for a node, that node's children are NOT
|
|
27
|
+
* visited (prune / stop-descend). Any other return value (including `void`)
|
|
28
|
+
* continues the walk.
|
|
29
|
+
*/
|
|
30
|
+
export declare function walkAst(root: TSESTree.Node, visitor: (node: TSESTree.Node) => false | void): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { type RuleFixer, type SourceCode } from '@typescript-eslint/utils/ts-eslint';
|
|
3
|
+
/** Returns the local alias for `untracked` from `@angular/core`, or null if not imported. */
|
|
4
|
+
export declare function findUntrackedAlias(program: TSESTree.Program): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Builds fixer actions that add `untracked` to an existing `@angular/core` import,
|
|
7
|
+
* or insert a new import declaration when none exists.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildUntrackedImportFixes(program: TSESTree.Program, fixer: RuleFixer): Array<ReturnType<RuleFixer['insertTextBefore']>>;
|
|
10
|
+
/**
|
|
11
|
+
* Removes the `untracked` import specifier from `@angular/core`.
|
|
12
|
+
* When it is the last specifier, removes the entire declaration.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildImportRemovalFixes(program: TSESTree.Program, fixer: RuleFixer, sourceCode: SourceCode): Array<ReturnType<RuleFixer['remove']>>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const ANGULAR_SIGNALS_UNTRACKED_GUIDE_URL = "https://angular.dev/guide/signals#reading-without-tracking-dependencies";
|
|
3
|
+
export declare const ANGULAR_SIGNALS_ASYNC_GUIDE_URL = "https://angular.dev/guide/signals#reactive-context-and-async-operations";
|
|
4
|
+
export declare const UNTRACKED_RULES_README_URL = "https://github.com/taiga-family/taiga-ui/blob/main/projects/eslint-plugin-experience-next/README.md";
|
|
5
|
+
export declare const createUntrackedRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|