@angular-eslint/eslint-plugin 22.6.1-alpha.1 → 22.6.1-alpha.3
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/index.d.ts +1 -1
- package/dist/rules/no-experimental.d.ts +6 -2
- package/dist/rules/no-experimental.d.ts.map +1 -1
- package/dist/rules/no-experimental.js +67 -3
- package/dist/rules/no-input-prefix.d.ts.map +1 -1
- package/dist/rules/no-input-prefix.js +14 -0
- package/dist/rules/reactive-context-must-read-signal.d.ts.map +1 -1
- package/dist/rules/reactive-context-must-read-signal.js +6 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ declare const _default: {
|
|
|
45
45
|
"no-empty-lifecycle-method": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/no-empty-lifecycle-method").MessageIds, [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
46
46
|
name: string;
|
|
47
47
|
};
|
|
48
|
-
"no-experimental": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noExperimental",
|
|
48
|
+
"no-experimental": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noExperimental", import("./rules/no-experimental").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
49
49
|
name: string;
|
|
50
50
|
};
|
|
51
51
|
"no-forward-ref": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noForwardRef", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
export type Options = [
|
|
1
|
+
export type Options = [
|
|
2
|
+
{
|
|
3
|
+
readonly ignorePackages?: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
2
6
|
export type MessageIds = 'noExperimental';
|
|
3
7
|
export declare const RULE_NAME = "no-experimental";
|
|
4
|
-
declare const _default: import("@typescript-eslint/utils/eslint-utils").RuleModule<"noExperimental",
|
|
8
|
+
declare const _default: import("@typescript-eslint/utils/eslint-utils").RuleModule<"noExperimental", Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/eslint-utils").RuleListener> & {
|
|
5
9
|
name: string;
|
|
6
10
|
};
|
|
7
11
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-experimental.d.ts","sourceRoot":"","sources":["../../src/rules/no-experimental.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"no-experimental.d.ts","sourceRoot":"","sources":["../../src/rules/no-experimental.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KAC7C;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAC1C,eAAO,MAAM,SAAS,oBAAoB,CAAC;;;;AA6E3C,wBA0DG;AAEH,eAAO,MAAM,mBAAmB;;CAE/B,CAAC"}
|
|
@@ -5,6 +5,52 @@ const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
|
5
5
|
const eslint_utils_1 = require("@typescript-eslint/utils/eslint-utils");
|
|
6
6
|
const jsdoc_1 = require("../utils/jsdoc");
|
|
7
7
|
exports.RULE_NAME = 'no-experimental';
|
|
8
|
+
const NODE_MODULES_SEGMENT = '/node_modules/';
|
|
9
|
+
/**
|
|
10
|
+
* Returns the name of the npm package that owns the given file, based on the
|
|
11
|
+
* segment(s) after the last `node_modules` directory in its path, or
|
|
12
|
+
* `undefined` if the file is not inside `node_modules`.
|
|
13
|
+
*
|
|
14
|
+
* Using the last `node_modules` directory means that nested installs
|
|
15
|
+
* (`node_modules/a/node_modules/b/...`) and pnpm's layout
|
|
16
|
+
* (`node_modules/.pnpm/b@1.0.0/node_modules/b/...`) both resolve to `b`.
|
|
17
|
+
*/
|
|
18
|
+
function getOwningPackageName(fileName) {
|
|
19
|
+
const normalizedFileName = `/${fileName.replace(/\\/g, '/')}`;
|
|
20
|
+
const index = normalizedFileName.lastIndexOf(NODE_MODULES_SEGMENT);
|
|
21
|
+
if (index === -1) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const [first, second] = normalizedFileName
|
|
25
|
+
.slice(index + NODE_MODULES_SEGMENT.length)
|
|
26
|
+
.split('/');
|
|
27
|
+
if (!first) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (first.startsWith('@')) {
|
|
31
|
+
return second ? `${first}/${second}` : undefined;
|
|
32
|
+
}
|
|
33
|
+
return first;
|
|
34
|
+
}
|
|
35
|
+
function isFileFromIgnoredPackage(fileName, ignorePackages) {
|
|
36
|
+
const packageName = getOwningPackageName(fileName);
|
|
37
|
+
return packageName !== undefined && ignorePackages.includes(packageName);
|
|
38
|
+
}
|
|
39
|
+
function shouldIgnoreDueToPackages(symbols, ignorePackages) {
|
|
40
|
+
if (ignorePackages.length === 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const experimentalSymbols = symbols.filter((symbol) => symbol?.getJsDocTags().some((tag) => tag.name === 'experimental'));
|
|
44
|
+
return (experimentalSymbols.length > 0 &&
|
|
45
|
+
experimentalSymbols.every((symbol) => {
|
|
46
|
+
if (symbol === undefined) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const declarations = symbol.getDeclarations() ?? [];
|
|
50
|
+
return (declarations.length > 0 &&
|
|
51
|
+
declarations.every((declaration) => isFileFromIgnoredPackage(declaration.getSourceFile().fileName, ignorePackages)));
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
8
54
|
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
9
55
|
name: exports.RULE_NAME,
|
|
10
56
|
meta: {
|
|
@@ -12,13 +58,28 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
12
58
|
docs: {
|
|
13
59
|
description: `Disallow using code which is marked as experimental`,
|
|
14
60
|
},
|
|
15
|
-
schema: [
|
|
61
|
+
schema: [
|
|
62
|
+
{
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
ignorePackages: {
|
|
66
|
+
type: 'array',
|
|
67
|
+
items: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
},
|
|
70
|
+
description: 'npm package names whose experimental APIs should not be reported. Each entry must be the name of the package that declares the experimental API, wherever it is installed (including nested `node_modules` and pnpm layouts). An API that one package re-exports from another is only ignored when the declaring package is listed, for example `@vitest/browser` (not `vitest`) for Vitest browser APIs imported from `vitest/browser`. Type-only packages such as `@types/foo` must be listed by their own name.',
|
|
71
|
+
uniqueItems: true,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
16
77
|
messages: {
|
|
17
78
|
noExperimental: '`{{name}}` is experimental',
|
|
18
79
|
},
|
|
19
|
-
defaultOptions: [],
|
|
80
|
+
defaultOptions: [{ ignorePackages: [] }],
|
|
20
81
|
},
|
|
21
|
-
create(context) {
|
|
82
|
+
create(context, [{ ignorePackages = [] }]) {
|
|
22
83
|
const services = (0, eslint_utils_1.getParserServices)(context);
|
|
23
84
|
const checker = services.program.getTypeChecker();
|
|
24
85
|
return {
|
|
@@ -30,6 +91,9 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
30
91
|
if (!(0, jsdoc_1.hasJsDocTag)(symbols, 'experimental')) {
|
|
31
92
|
return;
|
|
32
93
|
}
|
|
94
|
+
if (shouldIgnoreDueToPackages(symbols, ignorePackages)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
33
97
|
const { name } = node;
|
|
34
98
|
context.report({
|
|
35
99
|
node,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-input-prefix.d.ts","sourceRoot":"","sources":["../../src/rules/no-input-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,OAAO,GAAG,CAAC;IAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC;AACzC,eAAO,MAAM,SAAS,oBAAoB,CAAC;;;;AAE3C,
|
|
1
|
+
{"version":3,"file":"no-input-prefix.d.ts","sourceRoot":"","sources":["../../src/rules/no-input-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,OAAO,GAAG,CAAC;IAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC;AACzC,eAAO,MAAM,SAAS,oBAAoB,CAAC;;;;AAE3C,wBA4JG;AAWH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|
|
@@ -90,6 +90,20 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
|
+
[utils_1.Selectors.SIGNAL_INPUT_OR_MODEL_ALIAS](node) {
|
|
94
|
+
const aliasName = utils_1.ASTUtils.getRawText(node);
|
|
95
|
+
const hasDisallowedPrefix = prefixes.some((prefix) => isDisallowedPrefix(prefix, aliasName));
|
|
96
|
+
if (!hasDisallowedPrefix) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
context.report({
|
|
100
|
+
node,
|
|
101
|
+
messageId: 'noInputPrefix',
|
|
102
|
+
data: {
|
|
103
|
+
prefixes: (0, utils_1.toHumanReadableText)(prefixes),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
},
|
|
93
107
|
[utils_1.Selectors.INPUTS_METADATA_PROPERTY_LITERAL](node) {
|
|
94
108
|
const [propertyName, aliasName] = utils_1.ASTUtils.getRawText(node)
|
|
95
109
|
.replace(/\s/g, '')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive-context-must-read-signal.d.ts","sourceRoot":"","sources":["../../src/rules/reactive-context-must-read-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,WAAW,EAGZ,MAAM,0BAA0B,CAAC;AAMlC,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;KACnC;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAC1C,eAAO,MAAM,SAAS,sCAAsC,CAAC;;;;AA8D7D,
|
|
1
|
+
{"version":3,"file":"reactive-context-must-read-signal.d.ts","sourceRoot":"","sources":["../../src/rules/reactive-context-must-read-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,WAAW,EAGZ,MAAM,0BAA0B,CAAC;AAMlC,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;KACnC;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAC1C,eAAO,MAAM,SAAS,sCAAsC,CAAC;;;;AA8D7D,wBAgWG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|
|
@@ -186,6 +186,11 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
186
186
|
}
|
|
187
187
|
if (!symbol ||
|
|
188
188
|
!isDeclaredInDefaultLib(symbol) ||
|
|
189
|
+
// `fn.call()` and `fn.apply()` run `fn`, which can read a signal.
|
|
190
|
+
// Any other standard library `call`/`apply`, such as `Reflect.apply()`,
|
|
191
|
+
// also runs a function it is given.
|
|
192
|
+
symbol.name === 'call' ||
|
|
193
|
+
symbol.name === 'apply' ||
|
|
189
194
|
args.some(isOpaqueFunctionValue)) {
|
|
190
195
|
analysis.hasUnknown = true;
|
|
191
196
|
}
|
|
@@ -351,5 +356,5 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
351
356
|
},
|
|
352
357
|
});
|
|
353
358
|
exports.RULE_DOCS_EXTENSION = {
|
|
354
|
-
rationale: 'Reactive contexts like `computed()`, `linkedSignal()`, `effect()` and `afterRenderEffect()` re-run whenever a signal they read changes. If the relevant function never reads a signal, the context runs once and can never react to anything, so it adds overhead without providing reactivity. This is usually a mistake: either the developer forgot to call a signal (e.g. wrote `firstName` instead of `firstName()`), or the value is actually static and should be a plain constant, a `signal()`, or an `afterNextRender()` in the case of `afterRenderEffect()`. To avoid false positives, the rule only reports when everything the tracked function does is known not to read a signal. Calls into the TypeScript standard library (arrays, strings, `Math`, `JSON`, `console`, DOM APIs, etc.) are known to be safe
|
|
359
|
+
rationale: 'Reactive contexts like `computed()`, `linkedSignal()`, `effect()` and `afterRenderEffect()` re-run whenever a signal they read changes. If the relevant function never reads a signal, the context runs once and can never react to anything, so it adds overhead without providing reactivity. This is usually a mistake: either the developer forgot to call a signal (e.g. wrote `firstName` instead of `firstName()`), or the value is actually static and should be a plain constant, a `signal()`, or an `afterNextRender()` in the case of `afterRenderEffect()`. To avoid false positives, the rule only reports when everything the tracked function does is known not to read a signal. Calls into the TypeScript standard library (arrays, strings, `Math`, `JSON`, `console`, DOM APIs, etc.) are known to be safe, except `call()` and `apply()`, which run the function they are called on. Anything else keeps the rule silent, including a helper function, a service method, a getter, an unresolved symbol, and any Angular API such as `untracked()`. One known limitation: a standard library call that reaches user code through a protocol method, such as `toJSON()` in `JSON.stringify()` or `toString()` in a template literal, is still treated as safe, so a signal read inside such a method is not detected. For `resource()`/`rxResource()` only the `params` function defines the dependencies, and these are opt-in via the `checkResources` option.',
|
|
355
360
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-eslint/eslint-plugin",
|
|
3
|
-
"version": "22.6.1-alpha.
|
|
3
|
+
"version": "22.6.1-alpha.3",
|
|
4
4
|
"description": "ESLint plugin for Angular applications, following https://angular.dev/style-guide",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@angular-eslint/bundled-angular-compiler": "22.6.1-alpha.
|
|
23
|
-
"@angular-eslint/utils": "22.6.1-alpha.
|
|
22
|
+
"@angular-eslint/bundled-angular-compiler": "22.6.1-alpha.3",
|
|
23
|
+
"@angular-eslint/utils": "22.6.1-alpha.3",
|
|
24
24
|
"ts-api-utils": "^2.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@angular-eslint/test-utils": "22.6.1-alpha.
|
|
27
|
+
"@angular-eslint/test-utils": "22.6.1-alpha.3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@typescript-eslint/utils": "^8.0.0",
|