@angular-eslint/eslint-plugin 22.2.1-alpha.10 → 22.2.1-alpha.12
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 +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/rules/inject-at-top.d.ts +6 -2
- package/dist/rules/inject-at-top.d.ts.map +1 -1
- package/dist/rules/inject-at-top.js +53 -22
- package/dist/rules/prefer-signal-model.d.ts +7 -1
- package/dist/rules/prefer-signal-model.d.ts.map +1 -1
- package/dist/rules/prefer-signal-model.js +116 -50
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ declare const _default: {
|
|
|
27
27
|
"directive-selector": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/directive-selector").MessageIds, import("./rules/directive-selector").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
28
28
|
name: string;
|
|
29
29
|
};
|
|
30
|
-
"inject-at-top": import("@typescript-eslint/utils/ts-eslint").RuleModule<"injectAtTop",
|
|
30
|
+
"inject-at-top": import("@typescript-eslint/utils/ts-eslint").RuleModule<"injectAtTop", import("./rules/inject-at-top").Options, import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
31
31
|
name: string;
|
|
32
32
|
};
|
|
33
33
|
"no-async-lifecycle-method": import("@typescript-eslint/utils/ts-eslint").RuleModule<"noAsyncLifecycleMethod", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
@@ -108,7 +108,9 @@ declare const _default: {
|
|
|
108
108
|
"prefer-service-decorator": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferServiceDecorator", [], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
109
109
|
name: string;
|
|
110
110
|
};
|
|
111
|
-
"prefer-signal-model": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferSignalModel", [
|
|
111
|
+
"prefer-signal-model": import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferSignalModel", [{
|
|
112
|
+
useTypeChecking: boolean;
|
|
113
|
+
}], import("./utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
112
114
|
name: string;
|
|
113
115
|
};
|
|
114
116
|
"prefer-signals": import("@typescript-eslint/utils/ts-eslint").RuleModule<import("./rules/prefer-signals").MessageIds, [{
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuJA,kBAsDE"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
export type Options = [
|
|
1
|
+
export type Options = [
|
|
2
|
+
{
|
|
3
|
+
readonly additionalInjectFunctions?: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
2
6
|
export type MessageIds = 'injectAtTop';
|
|
3
7
|
export declare const RULE_NAME = "inject-at-top";
|
|
4
|
-
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"injectAtTop",
|
|
8
|
+
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"injectAtTop", Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
5
9
|
name: string;
|
|
6
10
|
};
|
|
7
11
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inject-at-top.d.ts","sourceRoot":"","sources":["../../src/rules/inject-at-top.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"inject-at-top.d.ts","sourceRoot":"","sources":["../../src/rules/inject-at-top.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;KACxD;CACF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,aAAa,CAAC;AACvC,eAAO,MAAM,SAAS,kBAAkB,CAAC;;;;AAIzC,wBAyHG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|
|
@@ -5,6 +5,7 @@ const utils_1 = require("@angular-eslint/utils");
|
|
|
5
5
|
const utils_2 = require("@typescript-eslint/utils");
|
|
6
6
|
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
7
7
|
exports.RULE_NAME = 'inject-at-top';
|
|
8
|
+
const DEFAULT_OPTIONS = { additionalInjectFunctions: [] };
|
|
8
9
|
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
9
10
|
name: exports.RULE_NAME,
|
|
10
11
|
meta: {
|
|
@@ -12,13 +13,28 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
12
13
|
docs: {
|
|
13
14
|
description: 'Requires inject() calls to be declared at the top of the class, before any other member',
|
|
14
15
|
},
|
|
15
|
-
schema: [
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
additionalInjectFunctions: {
|
|
21
|
+
type: 'array',
|
|
22
|
+
items: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
},
|
|
25
|
+
description: 'A list of additional functions which perform injection, such as your own `injectTranslations()` helper. Each entry is a regular expression which must match the whole function name, so plain names such as `injectTranslations` work as well as patterns such as `inject[A-Z].*`.',
|
|
26
|
+
uniqueItems: true,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
16
32
|
messages: {
|
|
17
|
-
injectAtTop: 'Move this
|
|
33
|
+
injectAtTop: 'Move this {{functionName}}() to the top of the class. Class fields are initialized in the order they are written, so anything declared above this line cannot safely use the service yet.',
|
|
18
34
|
},
|
|
19
|
-
defaultOptions: [],
|
|
35
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
20
36
|
},
|
|
21
|
-
create(context) {
|
|
37
|
+
create(context, [{ additionalInjectFunctions = [] }]) {
|
|
22
38
|
const angularDecoratorsPattern = (0, utils_1.toPattern)([
|
|
23
39
|
'Component',
|
|
24
40
|
'Directive',
|
|
@@ -26,14 +42,18 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
26
42
|
'Pipe',
|
|
27
43
|
'Service',
|
|
28
44
|
]);
|
|
29
|
-
|
|
45
|
+
const injectFunctionsPattern = (0, utils_1.toPattern)([
|
|
46
|
+
'inject',
|
|
47
|
+
...additionalInjectFunctions,
|
|
48
|
+
]);
|
|
49
|
+
function findEagerInjectName(node) {
|
|
30
50
|
if (!node) {
|
|
31
|
-
return
|
|
51
|
+
return null;
|
|
32
52
|
}
|
|
33
53
|
if (node.type === utils_2.AST_NODE_TYPES.CallExpression &&
|
|
34
54
|
node.callee.type === utils_2.AST_NODE_TYPES.Identifier &&
|
|
35
|
-
node.callee.name
|
|
36
|
-
return
|
|
55
|
+
injectFunctionsPattern.test(node.callee.name)) {
|
|
56
|
+
return node.callee.name;
|
|
37
57
|
}
|
|
38
58
|
const isFn = node.type === utils_2.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
39
59
|
node.type === utils_2.AST_NODE_TYPES.FunctionExpression;
|
|
@@ -41,15 +61,20 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
41
61
|
node.parent?.type === utils_2.AST_NODE_TYPES.CallExpression &&
|
|
42
62
|
node.parent.callee === node;
|
|
43
63
|
if (node.type === utils_2.AST_NODE_TYPES.ClassExpression || (isFn && !isIIFE)) {
|
|
44
|
-
return
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
for (const child of Object.values(node).flat()) {
|
|
67
|
+
if (child &&
|
|
68
|
+
typeof child === 'object' &&
|
|
69
|
+
'type' in child &&
|
|
70
|
+
child !== node.parent) {
|
|
71
|
+
const name = findEagerInjectName(child);
|
|
72
|
+
if (name) {
|
|
73
|
+
return name;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
45
76
|
}
|
|
46
|
-
return
|
|
47
|
-
.flat()
|
|
48
|
-
.some((child) => child &&
|
|
49
|
-
typeof child === 'object' &&
|
|
50
|
-
'type' in child &&
|
|
51
|
-
child !== node.parent &&
|
|
52
|
-
hasEagerInject(child));
|
|
77
|
+
return null;
|
|
53
78
|
}
|
|
54
79
|
return {
|
|
55
80
|
[`${utils_1.Selectors.decoratorDefinition(angularDecoratorsPattern)} > ClassBody`](node) {
|
|
@@ -58,12 +83,18 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
58
83
|
if ('static' in member && member.static) {
|
|
59
84
|
continue;
|
|
60
85
|
}
|
|
61
|
-
if (member.type === utils_2.AST_NODE_TYPES.PropertyDefinition
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
86
|
+
if (member.type === utils_2.AST_NODE_TYPES.PropertyDefinition) {
|
|
87
|
+
const functionName = findEagerInjectName(member.value);
|
|
88
|
+
if (functionName) {
|
|
89
|
+
if (seenNonInject) {
|
|
90
|
+
context.report({
|
|
91
|
+
node: member,
|
|
92
|
+
messageId: 'injectAtTop',
|
|
93
|
+
data: { functionName },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
65
97
|
}
|
|
66
|
-
continue;
|
|
67
98
|
}
|
|
68
99
|
seenNonInject = true;
|
|
69
100
|
}
|
|
@@ -72,5 +103,5 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
72
103
|
},
|
|
73
104
|
});
|
|
74
105
|
exports.RULE_DOCS_EXTENSION = {
|
|
75
|
-
rationale: "Class fields are initialized in the order they are declared, so a field that calls inject() only creates its value when the initializer runs. Anything declared above that line which tries to read the injected service will see undefined, and the error message (usually something like 'Cannot read properties of undefined') points at the consumer, not at the field that was initialized out of order. TypeScript catches the obvious shape of this bug when a later-declared field is referenced directly from an earlier initializer, but it does not trace through a getter body or a method call, so the moment the read of this.someService happens behind that indirection the compiler lets it through and the failure only shows up at runtime. The scenario is deceptively easy to introduce: a getter that reads an injected service is defined below the inject() call, someone later adds a small convenience field above the getter that references it, and the class breaks at construction time even though nothing about the change looks suspicious. Keeping every inject() call at the very top of the class removes the ordering concern entirely, because every dependency is guaranteed to exist before any other field, getter, method, or constructor statement runs.",
|
|
106
|
+
rationale: "Class fields are initialized in the order they are declared, so a field that calls inject() only creates its value when the initializer runs. Anything declared above that line which tries to read the injected service will see undefined, and the error message (usually something like 'Cannot read properties of undefined') points at the consumer, not at the field that was initialized out of order. TypeScript catches the obvious shape of this bug when a later-declared field is referenced directly from an earlier initializer, but it does not trace through a getter body or a method call, so the moment the read of this.someService happens behind that indirection the compiler lets it through and the failure only shows up at runtime. The scenario is deceptively easy to introduce: a getter that reads an injected service is defined below the inject() call, someone later adds a small convenience field above the getter that references it, and the class breaks at construction time even though nothing about the change looks suspicious. Keeping every inject() call at the very top of the class removes the ordering concern entirely, because every dependency is guaranteed to exist before any other field, getter, method, or constructor statement runs. Codebases often wrap inject() in their own helpers, for example an injectTranslations() function that injects a translation service and derives a scoped accessor from it; such a helper has exactly the same ordering constraint, so it can be declared through the additionalInjectFunctions option to be treated like inject() itself.",
|
|
76
107
|
};
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
useTypeChecking: boolean;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
1
7
|
export type MessageIds = 'preferSignalModel';
|
|
2
8
|
export declare const RULE_NAME = "prefer-signal-model";
|
|
3
|
-
declare const _default:
|
|
9
|
+
declare const _default: ESLintUtils.RuleModule<"preferSignalModel", Options, import("../utils/create-eslint-rule").RuleDocs, ESLintUtils.RuleListener> & {
|
|
4
10
|
name: string;
|
|
5
11
|
};
|
|
6
12
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-signal-model.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-signal-model.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefer-signal-model.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-signal-model.ts"],"names":[],"mappings":"AASA,OAAO,EAAkB,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIvE,KAAK,OAAO,GAAG;IACb;QACE,eAAe,EAAE,OAAO,CAAC;KAC1B;CACF,CAAC;AAMF,MAAM,MAAM,UAAU,GAAG,mBAAmB,CAAC;AAC7C,eAAO,MAAM,SAAS,wBAAwB,CAAC;;;;AA8B/C,wBAuKG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.RULE_DOCS_EXTENSION = exports.RULE_NAME = void 0;
|
|
4
7
|
const utils_1 = require("@angular-eslint/utils");
|
|
8
|
+
const utils_2 = require("@typescript-eslint/utils");
|
|
9
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
5
10
|
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
11
|
+
const DEFAULT_OPTIONS = {
|
|
12
|
+
useTypeChecking: false,
|
|
13
|
+
};
|
|
6
14
|
exports.RULE_NAME = 'prefer-signal-model';
|
|
15
|
+
function hasTransformOption(options) {
|
|
16
|
+
return (options?.type === utils_2.AST_NODE_TYPES.ObjectExpression &&
|
|
17
|
+
options.properties.some((property) => property.type === utils_2.AST_NODE_TYPES.Property &&
|
|
18
|
+
((property.key.type === utils_2.AST_NODE_TYPES.Identifier &&
|
|
19
|
+
property.key.name === 'transform') ||
|
|
20
|
+
(property.key.type === utils_2.AST_NODE_TYPES.Literal &&
|
|
21
|
+
property.key.value === 'transform'))));
|
|
22
|
+
}
|
|
7
23
|
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
8
24
|
name: exports.RULE_NAME,
|
|
9
25
|
meta: {
|
|
@@ -12,66 +28,116 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
12
28
|
description: 'Use `model` instead of `input` and `output` for two-way bindings',
|
|
13
29
|
},
|
|
14
30
|
fixable: 'code',
|
|
15
|
-
schema: [
|
|
31
|
+
schema: [
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
useTypeChecking: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
default: DEFAULT_OPTIONS.useTypeChecking,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
16
43
|
messages: {
|
|
17
44
|
preferSignalModel: 'Use `model` for two-way bindings instead of `input()` and `output()`',
|
|
18
45
|
},
|
|
19
|
-
defaultOptions: [],
|
|
46
|
+
defaultOptions: [{ ...DEFAULT_OPTIONS }],
|
|
20
47
|
},
|
|
21
|
-
create(context) {
|
|
48
|
+
create(context, [{ useTypeChecking = DEFAULT_OPTIONS.useTypeChecking }]) {
|
|
49
|
+
const { sourceCode } = context;
|
|
22
50
|
const inputs = new Map();
|
|
23
51
|
const outputs = new Map();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
let services;
|
|
53
|
+
function getTypeServices() {
|
|
54
|
+
return (services ??= utils_2.ESLintUtils.getParserServices(context));
|
|
55
|
+
}
|
|
56
|
+
function createSignalCollector(signals, { hasInitialValueArgument }) {
|
|
57
|
+
return (node) => {
|
|
58
|
+
const options = node.arguments[hasInitialValueArgument ? 1 : 0];
|
|
59
|
+
// `model()` has no transform support.
|
|
27
60
|
// https://github.com/angular/angular/issues/55166#issuecomment-2032150999
|
|
28
|
-
|
|
29
|
-
const hasTransformOption = options?.type === 'ObjectExpression' &&
|
|
30
|
-
options.properties.some((prop) => prop.type === 'Property' &&
|
|
31
|
-
((prop.key.type === 'Identifier' &&
|
|
32
|
-
prop.key.name === 'transform') ||
|
|
33
|
-
(prop.key.type === 'Literal' &&
|
|
34
|
-
prop.key.value === 'transform')));
|
|
35
|
-
if (hasTransformOption) {
|
|
61
|
+
if (hasTransformOption(options)) {
|
|
36
62
|
return;
|
|
37
63
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
const property = node.parent;
|
|
65
|
+
signals.set(utils_1.ASTUtils.getPropertyDefinitionName(property), {
|
|
66
|
+
property,
|
|
67
|
+
callee: (node.callee.type === utils_2.AST_NODE_TYPES.MemberExpression
|
|
68
|
+
? node.callee.object
|
|
69
|
+
: node.callee),
|
|
70
|
+
typeArgument: node.typeArguments?.params[0],
|
|
71
|
+
initialValue: hasInitialValueArgument ? node.arguments[0] : undefined,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function getValueType({ typeArgument, initialValue }) {
|
|
76
|
+
const typeServices = getTypeServices();
|
|
77
|
+
if (typeArgument) {
|
|
78
|
+
return typeServices.getTypeAtLocation(typeArgument);
|
|
79
|
+
}
|
|
80
|
+
if (initialValue) {
|
|
81
|
+
return typeServices.program
|
|
82
|
+
.getTypeChecker()
|
|
83
|
+
.getBaseTypeOfLiteralType(typeServices.getTypeAtLocation(initialValue));
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
function haveEquallyWrittenTypes(input, output) {
|
|
88
|
+
return (!input.typeArgument ||
|
|
89
|
+
!output.typeArgument ||
|
|
90
|
+
sourceCode.getText(input.typeArgument) ===
|
|
91
|
+
sourceCode.getText(output.typeArgument));
|
|
92
|
+
}
|
|
93
|
+
function haveMergeableTypes(input, output) {
|
|
94
|
+
if (!useTypeChecking) {
|
|
95
|
+
return haveEquallyWrittenTypes(input, output);
|
|
96
|
+
}
|
|
97
|
+
const inputType = getValueType(input);
|
|
98
|
+
const outputType = getValueType(output);
|
|
99
|
+
if (!inputType || !outputType) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// A type argument that does not resolve becomes the error type, which is
|
|
103
|
+
// assignable in both directions, so only the written text is trustworthy.
|
|
104
|
+
if ((inputType.flags | outputType.flags) & typescript_1.default.TypeFlags.Any) {
|
|
105
|
+
return haveEquallyWrittenTypes(input, output);
|
|
106
|
+
}
|
|
107
|
+
// Mutual assignability stands in for the internal `isTypeIdenticalTo`;
|
|
108
|
+
// the same type written twice yields two distinct `ts.Type` objects.
|
|
109
|
+
const checker = getTypeServices().program.getTypeChecker();
|
|
110
|
+
return (checker.isTypeAssignableTo(inputType, outputType) &&
|
|
111
|
+
checker.isTypeAssignableTo(outputType, inputType));
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
"PropertyDefinition > CallExpression[callee.name='input']": createSignalCollector(inputs, { hasInitialValueArgument: true }),
|
|
115
|
+
"PropertyDefinition > CallExpression[callee.object.name='input'][callee.property.name='required']": createSignalCollector(inputs, { hasInitialValueArgument: false }),
|
|
116
|
+
"PropertyDefinition > CallExpression[callee.name='output']": createSignalCollector(outputs, { hasInitialValueArgument: false }),
|
|
47
117
|
'ClassDeclaration:exit'() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
118
|
+
const twoWayBindings = [...inputs]
|
|
119
|
+
.map(([name, input]) => ({
|
|
120
|
+
input,
|
|
121
|
+
output: outputs.get(`${name}Change`),
|
|
122
|
+
}))
|
|
123
|
+
.filter((binding) => binding.output !== undefined &&
|
|
124
|
+
haveMergeableTypes(binding.input, binding.output));
|
|
125
|
+
for (const { input, output } of twoWayBindings) {
|
|
126
|
+
context.report({
|
|
127
|
+
node: input.property,
|
|
128
|
+
messageId: 'preferSignalModel',
|
|
129
|
+
fix: (fixer) => [
|
|
130
|
+
utils_1.RuleFixes.getImportAddFix({
|
|
131
|
+
fixer,
|
|
132
|
+
importName: 'model',
|
|
133
|
+
moduleName: '@angular/core',
|
|
134
|
+
node: input.property,
|
|
135
|
+
}),
|
|
136
|
+
fixer.replaceText(input.callee, 'model'),
|
|
137
|
+
fixer.remove(output.property),
|
|
138
|
+
].filter(utils_1.isNotNullOrUndefined),
|
|
139
|
+
});
|
|
73
140
|
}
|
|
74
|
-
// Clear the maps for the next class
|
|
75
141
|
inputs.clear();
|
|
76
142
|
outputs.clear();
|
|
77
143
|
},
|
|
@@ -79,5 +145,5 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
79
145
|
},
|
|
80
146
|
});
|
|
81
147
|
exports.RULE_DOCS_EXTENSION = {
|
|
82
|
-
rationale: "The model() function is Angular's modern API for two-way bindings, combining both input and output into a single signal. When you have an input property paired with an output property that follows the naming pattern of `propertyChange` (e.g., `enabled` input with `enabledChange` output), this is the traditional pattern for two-way binding. The model() function provides a cleaner, more concise way to express this pattern with better type safety and integration with Angular's signal ecosystem. It eliminates the boilerplate of managing separate input and output properties while maintaining the same two-way binding functionality.",
|
|
148
|
+
rationale: "The model() function is Angular's modern API for two-way bindings, combining both input and output into a single signal. When you have an input property paired with an output property that follows the naming pattern of `propertyChange` (e.g., `enabled` input with `enabledChange` output), this is the traditional pattern for two-way binding. The model() function provides a cleaner, more concise way to express this pattern with better type safety and integration with Angular's signal ecosystem. It eliminates the boilerplate of managing separate input and output properties while maintaining the same two-way binding functionality.\n\nBecause `model()` exposes a single type for both directions, only pairs whose `input` and `output` types match are reported. By default the written type arguments are compared as text, and a pair is assumed compatible when either side has no type argument. Enabling the `useTypeChecking` option compares the types semantically instead, and infers an `input`'s type from its initial value, so `input('')` paired with `output<number>()` is left alone. A type argument that does not resolve falls back to the text comparison.",
|
|
83
149
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-eslint/eslint-plugin",
|
|
3
|
-
"version": "22.2.1-alpha.
|
|
3
|
+
"version": "22.2.1-alpha.12",
|
|
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.2.1-alpha.
|
|
23
|
-
"@angular-eslint/utils": "22.2.1-alpha.
|
|
22
|
+
"@angular-eslint/bundled-angular-compiler": "22.2.1-alpha.12",
|
|
23
|
+
"@angular-eslint/utils": "22.2.1-alpha.12",
|
|
24
24
|
"ts-api-utils": "^2.1.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@angular-eslint/test-utils": "22.2.1-alpha.
|
|
27
|
+
"@angular-eslint/test-utils": "22.2.1-alpha.12"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@typescript-eslint/utils": "^8.0.0",
|