@depup/angular-eslint__eslint-plugin 22.1.0-depup.1 → 22.3.0-depup.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 +2 -2
- package/changes.json +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/rules/component-class-suffix.js +5 -5
- package/dist/rules/component-max-inline-declarations.js +7 -7
- package/dist/rules/computed-must-return.js +1 -1
- package/dist/rules/consistent-component-styles.js +1 -1
- package/dist/rules/contextual-decorator.js +1 -1
- package/dist/rules/contextual-lifecycle.js +1 -1
- package/dist/rules/directive-class-suffix.js +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/no-async-lifecycle-method.js +1 -1
- package/dist/rules/no-attribute-decorator.js +1 -1
- package/dist/rules/no-developer-preview.js +1 -1
- package/dist/rules/no-duplicates-in-metadata-arrays.d.ts.map +1 -1
- package/dist/rules/no-duplicates-in-metadata-arrays.js +11 -6
- package/dist/rules/no-empty-lifecycle-method.js +1 -1
- package/dist/rules/no-experimental.js +1 -1
- package/dist/rules/no-forward-ref.js +1 -1
- package/dist/rules/no-implicit-take-until-destroyed.d.ts.map +1 -1
- package/dist/rules/no-implicit-take-until-destroyed.js +16 -6
- package/dist/rules/no-input-prefix.js +1 -1
- package/dist/rules/no-input-rename.js +1 -1
- package/dist/rules/no-inputs-metadata-property.js +1 -1
- package/dist/rules/no-lifecycle-call.js +1 -1
- package/dist/rules/no-output-native.js +1 -1
- package/dist/rules/no-output-on-prefix.js +1 -1
- package/dist/rules/no-output-rename.js +1 -1
- package/dist/rules/no-outputs-metadata-property.js +1 -1
- package/dist/rules/no-pipe-impure.js +1 -1
- package/dist/rules/no-queries-metadata-property.js +1 -1
- package/dist/rules/no-uncalled-signals.js +1 -1
- package/dist/rules/pipe-prefix.js +5 -5
- package/dist/rules/prefer-host-metadata-property.js +1 -1
- package/dist/rules/prefer-inject.js +1 -1
- package/dist/rules/prefer-on-push-component-change-detection.d.ts +6 -3
- package/dist/rules/prefer-on-push-component-change-detection.d.ts.map +1 -1
- package/dist/rules/prefer-on-push-component-change-detection.js +50 -20
- package/dist/rules/prefer-output-emitter-ref.js +1 -1
- package/dist/rules/prefer-output-readonly.js +1 -1
- package/dist/rules/prefer-service-decorator.js +1 -1
- 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 +118 -40
- package/dist/rules/prefer-signals.js +1 -1
- package/dist/rules/prefer-standalone.js +1 -1
- package/dist/rules/reactive-context-must-read-signal.d.ts +16 -0
- package/dist/rules/reactive-context-must-read-signal.d.ts.map +1 -0
- package/dist/rules/reactive-context-must-read-signal.js +357 -0
- package/dist/rules/relative-url-prefix.js +1 -1
- package/dist/rules/require-lifecycle-on-prototype.js +1 -1
- package/dist/rules/require-localize-metadata.js +1 -1
- package/dist/rules/runtime-localize.js +1 -1
- package/dist/rules/sort-keys-in-type-decorator.js +1 -1
- package/dist/rules/sort-lifecycle-methods.js +1 -1
- package/dist/rules/use-component-selector.js +1 -1
- package/dist/rules/use-component-view-encapsulation.js +1 -1
- package/dist/rules/use-injectable-provided-in.d.ts +6 -3
- package/dist/rules/use-injectable-provided-in.d.ts.map +1 -1
- package/dist/rules/use-injectable-provided-in.js +14 -5
- package/dist/rules/use-lifecycle-interface.js +1 -1
- package/dist/rules/use-pipe-transform-interface.js +1 -1
- package/dist/utils/create-eslint-rule.d.ts.map +1 -1
- package/dist/utils/create-eslint-rule.js +1 -24
- package/package.json +7 -7
|
@@ -30,12 +30,12 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
30
30
|
pipePrefix: '@Pipes should be prefixed with {{prefixes}}',
|
|
31
31
|
selectorAfterPrefixFailure: '@Pipes should have a selector after the {{prefixes}} prefix',
|
|
32
32
|
},
|
|
33
|
+
defaultOptions: [
|
|
34
|
+
{
|
|
35
|
+
prefixes: [],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
33
38
|
},
|
|
34
|
-
defaultOptions: [
|
|
35
|
-
{
|
|
36
|
-
prefixes: [],
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
39
|
create(context, [{ prefixes }]) {
|
|
40
40
|
function checkValidOption(prefixes) {
|
|
41
41
|
return Array.isArray(prefixes) && prefixes.length > 0;
|
|
@@ -16,8 +16,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
16
16
|
preferHostMetadataPropertyForBinding: 'Prefer using the `host` metadata property in the `@Component` decorator for host bindings instead of the `@HostBinding` decorator.',
|
|
17
17
|
preferHostMetadataPropertyForListener: 'Prefer using the `host` metadata property in the `@Component` decorator for host listeners instead of the `@HostListener` decorator.',
|
|
18
18
|
},
|
|
19
|
+
defaultOptions: [],
|
|
19
20
|
},
|
|
20
|
-
defaultOptions: [],
|
|
21
21
|
create(context) {
|
|
22
22
|
return {
|
|
23
23
|
[utils_1.Selectors.HOST_BINDING_DECORATOR]: (node) => {
|
|
@@ -17,8 +17,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
17
17
|
messages: {
|
|
18
18
|
preferInject: "Prefer using the inject() function over constructor parameter injection. Use Angular's migration schematic to automatically refactor: ng generate @angular/core:inject",
|
|
19
19
|
},
|
|
20
|
+
defaultOptions: [],
|
|
20
21
|
},
|
|
21
|
-
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
23
|
const angularDecoratorsPattern = (0, utils_1.toPattern)([
|
|
24
24
|
'Component',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export type Options = [{
|
|
3
|
+
readonly allowExplicitOnPush: boolean;
|
|
4
|
+
}];
|
|
5
|
+
export type MessageIds = 'preferOnPushComponentChangeDetection' | 'suggestRemoveChangeDetection' | 'redundantOnPushComponentChangeDetection';
|
|
3
6
|
export declare const RULE_NAME = "prefer-on-push-component-change-detection";
|
|
4
|
-
declare const _default:
|
|
7
|
+
declare const _default: TSESLint.RuleModule<MessageIds, Options, import("../utils/create-eslint-rule").RuleDocs, TSESLint.RuleListener> & {
|
|
5
8
|
name: string;
|
|
6
9
|
};
|
|
7
10
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-on-push-component-change-detection.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-on-push-component-change-detection.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefer-on-push-component-change-detection.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-on-push-component-change-detection.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,0BAA0B,CAAC;AAGnE,MAAM,MAAM,OAAO,GAAG,CAAC;IAAE,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAClE,MAAM,MAAM,UAAU,GAClB,sCAAsC,GACtC,8BAA8B,GAC9B,yCAAyC,CAAC;AAC9C,eAAO,MAAM,SAAS,8CAA8C,CAAC;;;;AAMrE,wBAoGG;AAEH,eAAO,MAAM,mBAAmB;;CAQ/B,CAAC"}
|
|
@@ -6,6 +6,7 @@ const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
|
6
6
|
exports.RULE_NAME = 'prefer-on-push-component-change-detection';
|
|
7
7
|
const METADATA_PROPERTY_NAME = 'changeDetection';
|
|
8
8
|
const STRATEGY_ON_PUSH = 'ChangeDetectionStrategy.OnPush';
|
|
9
|
+
const DEFAULT_OPTIONS = { allowExplicitOnPush: true };
|
|
9
10
|
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
10
11
|
name: exports.RULE_NAME,
|
|
11
12
|
meta: {
|
|
@@ -14,26 +15,41 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
14
15
|
description: `Ensures components do not opt out of the default \`${STRATEGY_ON_PUSH}\` change detection strategy`,
|
|
15
16
|
recommended: 'recommended',
|
|
16
17
|
},
|
|
18
|
+
fixable: 'code',
|
|
17
19
|
hasSuggestions: true,
|
|
18
|
-
schema: [
|
|
20
|
+
schema: [
|
|
21
|
+
{
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
allowExplicitOnPush: {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: DEFAULT_OPTIONS.allowExplicitOnPush,
|
|
27
|
+
description: `Whether to allow a component to set \`${METADATA_PROPERTY_NAME}: ${STRATEGY_ON_PUSH}\` explicitly even though it is now the default.`,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
19
33
|
messages: {
|
|
20
34
|
preferOnPushComponentChangeDetection: `Components should not opt out of the default \`${STRATEGY_ON_PUSH}\` change detection strategy`,
|
|
21
35
|
suggestRemoveChangeDetection: `Remove \`${METADATA_PROPERTY_NAME}\` to use the default (\`${STRATEGY_ON_PUSH}\`)`,
|
|
36
|
+
redundantOnPushComponentChangeDetection: `\`${METADATA_PROPERTY_NAME}: ${STRATEGY_ON_PUSH}\` is redundant because \`${STRATEGY_ON_PUSH}\` is the default change detection strategy`,
|
|
22
37
|
},
|
|
38
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
23
39
|
},
|
|
24
|
-
|
|
25
|
-
create(context) {
|
|
40
|
+
create(context, [{ allowExplicitOnPush }]) {
|
|
26
41
|
const sourceCode = context.sourceCode;
|
|
27
42
|
const changeDetectionMetadataProperty = utils_1.Selectors.metadataProperty(METADATA_PROPERTY_NAME);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
const changeDetectionStrategyProperty = `${utils_1.Selectors.COMPONENT_CLASS_DECORATOR} > CallExpression > ObjectExpression > ${changeDetectionMetadataProperty}[value.object.name='ChangeDetectionStrategy']`;
|
|
44
|
+
const onPushOptOutProperty = `${changeDetectionStrategyProperty}[value.property.name!='OnPush']`;
|
|
45
|
+
const redundantOnPushProperty = `${changeDetectionStrategyProperty}[value.property.name='OnPush']`;
|
|
46
|
+
function removeChangeDetectionProperty(node, fixer) {
|
|
47
|
+
const importDeclarations = utils_1.ASTUtils.getImportDeclarations(node, '@angular/core') ?? [];
|
|
48
|
+
return [
|
|
49
|
+
utils_1.RuleFixes.getNodeToCommaRemoveFix(sourceCode, node, fixer),
|
|
50
|
+
utils_1.RuleFixes.getImportRemoveFix(sourceCode, importDeclarations, 'ChangeDetectionStrategy', fixer),
|
|
51
|
+
].filter(utils_1.isNotNullOrUndefined);
|
|
52
|
+
}
|
|
37
53
|
return {
|
|
38
54
|
[onPushOptOutProperty](node) {
|
|
39
55
|
const { value } = node;
|
|
@@ -48,20 +64,34 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
48
64
|
suggest: [
|
|
49
65
|
{
|
|
50
66
|
messageId: 'suggestRemoveChangeDetection',
|
|
51
|
-
fix: (fixer) =>
|
|
52
|
-
const importDeclarations = utils_1.ASTUtils.getImportDeclarations(node, '@angular/core') ?? [];
|
|
53
|
-
return [
|
|
54
|
-
utils_1.RuleFixes.getNodeToCommaRemoveFix(sourceCode, node, fixer),
|
|
55
|
-
utils_1.RuleFixes.getImportRemoveFix(sourceCode, importDeclarations, 'ChangeDetectionStrategy', fixer),
|
|
56
|
-
].filter(utils_1.isNotNullOrUndefined);
|
|
57
|
-
},
|
|
67
|
+
fix: (fixer) => removeChangeDetectionProperty(node, fixer),
|
|
58
68
|
},
|
|
59
69
|
],
|
|
60
70
|
});
|
|
61
71
|
},
|
|
72
|
+
[redundantOnPushProperty](node) {
|
|
73
|
+
if (allowExplicitOnPush) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { value } = node;
|
|
77
|
+
if (!utils_1.ASTUtils.isMemberExpression(value)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
context.report({
|
|
81
|
+
node: value.property,
|
|
82
|
+
messageId: 'redundantOnPushComponentChangeDetection',
|
|
83
|
+
fix: (fixer) => removeChangeDetectionProperty(node, fixer),
|
|
84
|
+
});
|
|
85
|
+
},
|
|
62
86
|
};
|
|
63
87
|
},
|
|
64
88
|
});
|
|
65
89
|
exports.RULE_DOCS_EXTENSION = {
|
|
66
|
-
rationale: `As of Angular v22, \`${STRATEGY_ON_PUSH}\` is the default change detection strategy: a component that does not specify \`${METADATA_PROPERTY_NAME}\` is checked using OnPush.
|
|
90
|
+
rationale: `As of Angular v22, \`${STRATEGY_ON_PUSH}\` is the default change detection strategy: a component that does not specify \`${METADATA_PROPERTY_NAME}\` is checked using OnPush. \
|
|
91
|
+
This brings new code in line with zoneless being the default and with Angular's goal of performance by default, and means it is no longer necessary to set \`${STRATEGY_ON_PUSH}\` explicitly. \
|
|
92
|
+
The previous default, \`ChangeDetectionStrategy.Default\`, has been renamed to \`ChangeDetectionStrategy.Eager\`. \
|
|
93
|
+
When you run \`ng update\`, the v22 migration adds an explicit \`ChangeDetectionStrategy.Eager\` to existing components that relied on the old implicit default, so that they keep behaving as before.\n\n\
|
|
94
|
+
By default the rule reports components that explicitly opt out of OnPush by setting \`${METADATA_PROPERTY_NAME}\` to \`ChangeDetectionStrategy.Eager\` (or the deprecated \`ChangeDetectionStrategy.Default\`) — including the components the migration marked as \`Eager\` — so you can review them and adopt OnPush where it is safe to do so. \
|
|
95
|
+
Because omitting \`${METADATA_PROPERTY_NAME}\` already gives you OnPush, declaring \`${STRATEGY_ON_PUSH}\` explicitly is redundant; set \`allowExplicitOnPush\` to \`false\` to have the rule flag and autofix that too. \
|
|
96
|
+
Note that switching a component from eager checking to OnPush can change its runtime behaviour, so apply the suggestion deliberately and make sure the component uses immutable data patterns (creating new object references when data changes).`,
|
|
67
97
|
};
|
|
@@ -15,8 +15,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
15
15
|
messages: {
|
|
16
16
|
preferOutputEmitterRef: 'Use `OutputEmitterRef` via `output()` for Component and Directive outputs rather than the legacy `@Output()` decorator',
|
|
17
17
|
},
|
|
18
|
+
defaultOptions: [],
|
|
18
19
|
},
|
|
19
|
-
defaultOptions: [],
|
|
20
20
|
create(context) {
|
|
21
21
|
return {
|
|
22
22
|
[utils_1.Selectors.OUTPUT_DECORATOR]: (node) => {
|
|
@@ -17,8 +17,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
17
17
|
preferOutputReadonly: 'Prefer to declare `{{type}}` as `readonly` since they are not supposed to be reassigned',
|
|
18
18
|
suggestAddReadonlyModifier: 'Add `readonly` modifier',
|
|
19
19
|
},
|
|
20
|
+
defaultOptions: [],
|
|
20
21
|
},
|
|
21
|
-
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
23
|
return {
|
|
24
24
|
[`PropertyDefinition:not([readonly=true]) > ${utils_1.Selectors.OUTPUT_DECORATOR}`]({ parent: { key }, }) {
|
|
@@ -17,8 +17,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
17
17
|
messages: {
|
|
18
18
|
preferServiceDecorator: "Use the `@Service()` decorator instead of `@Injectable({ providedIn: 'root' })`",
|
|
19
19
|
},
|
|
20
|
+
defaultOptions: [],
|
|
20
21
|
},
|
|
21
|
-
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
23
|
return {
|
|
24
24
|
[utils_1.Selectors.INJECTABLE_CLASS_DECORATOR](node) {
|
|
@@ -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,54 +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
|
},
|
|
46
|
+
defaultOptions: [{ ...DEFAULT_OPTIONS }],
|
|
19
47
|
},
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
create(context, [{ useTypeChecking = DEFAULT_OPTIONS.useTypeChecking }]) {
|
|
49
|
+
const { sourceCode } = context;
|
|
22
50
|
const inputs = new Map();
|
|
23
51
|
const outputs = new Map();
|
|
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.
|
|
60
|
+
// https://github.com/angular/angular/issues/55166#issuecomment-2032150999
|
|
61
|
+
if (hasTransformOption(options)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
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
|
+
}
|
|
24
113
|
return {
|
|
25
|
-
"PropertyDefinition > CallExpression[callee.name='input']"(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
inputs.set(propertyName, propertyDef);
|
|
29
|
-
},
|
|
30
|
-
"PropertyDefinition > CallExpression[callee.name='output']"(node) {
|
|
31
|
-
const propertyDef = node.parent;
|
|
32
|
-
const propertyName = utils_1.ASTUtils.getPropertyDefinitionName(propertyDef);
|
|
33
|
-
outputs.set(propertyName, propertyDef);
|
|
34
|
-
},
|
|
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 }),
|
|
35
117
|
'ClassDeclaration:exit'() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
}
|
|
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
|
+
});
|
|
61
140
|
}
|
|
62
|
-
// Clear the maps for the next class
|
|
63
141
|
inputs.clear();
|
|
64
142
|
outputs.clear();
|
|
65
143
|
},
|
|
@@ -67,5 +145,5 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
67
145
|
},
|
|
68
146
|
});
|
|
69
147
|
exports.RULE_DOCS_EXTENSION = {
|
|
70
|
-
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.",
|
|
71
149
|
};
|
|
@@ -67,8 +67,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
67
67
|
preferQuerySignals: 'Use the `{{function}}` function instead of the `{{decorator}}` decorator',
|
|
68
68
|
preferReadonlySignalProperties: 'Properties declared using signals should be marked as `readonly` since they should not be reassigned',
|
|
69
69
|
},
|
|
70
|
+
defaultOptions: [{ ...DEFAULT_OPTIONS }],
|
|
70
71
|
},
|
|
71
|
-
defaultOptions: [{ ...DEFAULT_OPTIONS }],
|
|
72
72
|
create(context, [{ preferReadonlySignalProperties = DEFAULT_OPTIONS.preferReadonlySignalProperties, preferInputSignals = DEFAULT_OPTIONS.preferInputSignals, preferQuerySignals = DEFAULT_OPTIONS.preferQuerySignals, additionalSignalCreationFunctions = DEFAULT_OPTIONS.additionalSignalCreationFunctions, useTypeChecking = DEFAULT_OPTIONS.useTypeChecking, },]) {
|
|
73
73
|
let services;
|
|
74
74
|
const listener = {};
|
|
@@ -19,8 +19,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
19
19
|
preferStandalone: `Components, Directives and Pipes should not opt out of standalone. Following this guide is highly recommended: ${RECOMMENDED_GUIDE_URL}`,
|
|
20
20
|
removeStandaloneFalse: `Quickly remove 'standalone: false'. NOTE - Following this guide is highly recommended: ${RECOMMENDED_GUIDE_URL}`,
|
|
21
21
|
},
|
|
22
|
+
defaultOptions: [],
|
|
22
23
|
},
|
|
23
|
-
defaultOptions: [],
|
|
24
24
|
create(context) {
|
|
25
25
|
const standaloneRuleFactory = (type) => (node) => {
|
|
26
26
|
const standalone = utils_1.ASTUtils.getDecoratorPropertyValue(node, 'standalone');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
export type Options = [
|
|
3
|
+
{
|
|
4
|
+
readonly checkResources?: boolean;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export type MessageIds = 'mustReadSignal';
|
|
8
|
+
export declare const RULE_NAME = "reactive-context-must-read-signal";
|
|
9
|
+
declare const _default: ESLintUtils.RuleModule<"mustReadSignal", Options, import("../utils/create-eslint-rule").RuleDocs, ESLintUtils.RuleListener> & {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export default _default;
|
|
13
|
+
export declare const RULE_DOCS_EXTENSION: {
|
|
14
|
+
rationale: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=reactive-context-must-read-signal.d.ts.map
|
|
@@ -0,0 +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,wBA2VG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|