Skip to content

Commit c9f97f0

Browse files
authored
feat(eslint-plugin): add rule prefer-host-metadata-property (#2615)
1 parent fd595c2 commit c9f97f0

File tree

9 files changed

+289
-0
lines changed

9 files changed

+289
-0
lines changed

packages/angular-eslint/src/configs/ts-all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export default (
4444
'@angular-eslint/no-queries-metadata-property': 'error',
4545
'@angular-eslint/no-uncalled-signals': 'error',
4646
'@angular-eslint/pipe-prefix': 'error',
47+
'@angular-eslint/prefer-host-metadata-property': 'error',
4748
'@angular-eslint/prefer-inject': 'error',
4849
'@angular-eslint/prefer-on-push-component-change-detection': 'error',
4950
'@angular-eslint/prefer-output-emitter-ref': 'error',

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr
6868
| [`no-queries-metadata-property`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/no-queries-metadata-property.md) | Disallows usage of the `queries` metadata property. See more at https://angular.dev/style-guide#style-05-12. | | | |
6969
| [`no-uncalled-signals`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/no-uncalled-signals.md) | Warns user about unintentionally doing logic on the signal, rather than the signal's value | | | :bulb: |
7070
| [`pipe-prefix`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/pipe-prefix.md) | Enforce consistent prefix for pipes. | | | |
71+
| [`prefer-host-metadata-property`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-host-metadata-property.md) | Use `host` metadata property instead of `@HostBinding` and `HostListener` | | | |
7172
| [`prefer-inject`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-inject.md) | Prefer using the inject() function over constructor parameter injection | :white_check_mark: | | |
7273
| [`prefer-on-push-component-change-detection`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-on-push-component-change-detection.md) | Ensures component's `changeDetection` is set to `ChangeDetectionStrategy.OnPush` | | | :bulb: |
7374
| [`prefer-output-emitter-ref`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-output-emitter-ref.md) | Use `OutputEmitterRef` instead of `@Output()` | | | |
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<!--
2+
3+
DO NOT EDIT.
4+
5+
This markdown file was autogenerated using a mixture of the following files as the source of truth for its data:
6+
- ../../src/rules/prefer-host-metadata-property.ts
7+
- ../../tests/rules/prefer-host-metadata-property/cases.ts
8+
9+
In order to update this file, it is therefore those files which need to be updated, as well as potentially the generator script:
10+
- ../../../../tools/scripts/generate-rule-docs.ts
11+
12+
-->
13+
14+
<br>
15+
16+
# `@angular-eslint/prefer-host-metadata-property`
17+
18+
Use `host` metadata property instead of `@HostBinding` and `HostListener`
19+
20+
- Type: suggestion
21+
22+
<br>
23+
24+
## Rule Options
25+
26+
The rule does not have any configuration options.
27+
28+
<br>
29+
30+
## Usage Examples
31+
32+
> The following examples are generated automatically from the actual unit tests within the plugin, so you can be assured that their behavior is accurate based on the current commit.
33+
34+
<br>
35+
36+
<details>
37+
<summary>❌ - Toggle examples of <strong>incorrect</strong> code for this rule</summary>
38+
39+
<br>
40+
41+
#### Default Config
42+
43+
```json
44+
{
45+
"rules": {
46+
"@angular-eslint/prefer-host-metadata-property": [
47+
"error"
48+
]
49+
}
50+
}
51+
```
52+
53+
<br>
54+
55+
#### ❌ Invalid Code
56+
57+
```ts
58+
class Test {
59+
@HostBinding('class')
60+
~~~~~~~~~~~~~~~~~~~~~
61+
readonly cssClass = 'my-class';
62+
}
63+
```
64+
65+
<br>
66+
67+
---
68+
69+
<br>
70+
71+
#### Default Config
72+
73+
```json
74+
{
75+
"rules": {
76+
"@angular-eslint/prefer-host-metadata-property": [
77+
"error"
78+
]
79+
}
80+
}
81+
```
82+
83+
<br>
84+
85+
#### ❌ Invalid Code
86+
87+
```ts
88+
class Test {
89+
@HostListener('click')
90+
~~~~~~~~~~~~~~~~~~~~~~
91+
bar() {}
92+
}
93+
```
94+
95+
</details>
96+
97+
<br>
98+
99+
---
100+
101+
<br>
102+
103+
<details>
104+
<summary>✅ - Toggle examples of <strong>correct</strong> code for this rule</summary>
105+
106+
<br>
107+
108+
#### Default Config
109+
110+
```json
111+
{
112+
"rules": {
113+
"@angular-eslint/prefer-host-metadata-property": [
114+
"error"
115+
]
116+
}
117+
}
118+
```
119+
120+
<br>
121+
122+
#### ✅ Valid Code
123+
124+
```ts
125+
@Component({
126+
host: {
127+
class: 'my-class',
128+
},
129+
})
130+
class Test {}
131+
```
132+
133+
<br>
134+
135+
---
136+
137+
<br>
138+
139+
#### Default Config
140+
141+
```json
142+
{
143+
"rules": {
144+
"@angular-eslint/prefer-host-metadata-property": [
145+
"error"
146+
]
147+
}
148+
}
149+
```
150+
151+
<br>
152+
153+
#### ✅ Valid Code
154+
155+
```ts
156+
@Component({
157+
host: {
158+
'(click)': 'bar()'
159+
},
160+
})
161+
class Test {
162+
bar() {}
163+
}
164+
```
165+
166+
</details>
167+
168+
<br>

packages/eslint-plugin/src/configs/all.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@angular-eslint/no-queries-metadata-property": "error",
3131
"@angular-eslint/no-uncalled-signals": "error",
3232
"@angular-eslint/pipe-prefix": "error",
33+
"@angular-eslint/prefer-host-metadata-property": "error",
3334
"@angular-eslint/prefer-inject": "error",
3435
"@angular-eslint/prefer-on-push-component-change-detection": "error",
3536
"@angular-eslint/prefer-output-emitter-ref": "error",

packages/eslint-plugin/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ import noUncalledSignals, {
7979
import pipePrefix, {
8080
RULE_NAME as pipePrefixRuleName,
8181
} from './rules/pipe-prefix';
82+
import preferHostMetadataProperty, {
83+
RULE_NAME as preferHostMetadataPropertyRuleName,
84+
} from './rules/prefer-host-metadata-property';
8285
import preferOnPushComponentChangeDetection, {
8386
RULE_NAME as preferOnPushComponentChangeDetectionRuleName,
8487
} from './rules/prefer-on-push-component-change-detection';
@@ -169,6 +172,7 @@ export = {
169172
[noPipeImpureRuleName]: noPipeImpure,
170173
[noQueriesMetadataPropertyRuleName]: noQueriesMetadataProperty,
171174
[pipePrefixRuleName]: pipePrefix,
175+
[preferHostMetadataPropertyRuleName]: preferHostMetadataProperty,
172176
[preferOnPushComponentChangeDetectionRuleName]:
173177
preferOnPushComponentChangeDetection,
174178
[preferSignalsRuleName]: preferSignals,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Selectors } from '@angular-eslint/utils';
2+
import { createESLintRule } from '../utils/create-eslint-rule';
3+
4+
type Options = [];
5+
6+
export type MessageIds =
7+
| 'preferHostMetadataPropertyForBinding'
8+
| 'preferHostMetadataPropertyForListener';
9+
export const RULE_NAME = 'prefer-host-metadata-property';
10+
11+
export default createESLintRule<Options, MessageIds>({
12+
name: RULE_NAME,
13+
meta: {
14+
type: 'suggestion',
15+
docs: {
16+
description:
17+
'Use `host` metadata property instead of `@HostBinding` and `HostListener`',
18+
},
19+
schema: [],
20+
messages: {
21+
preferHostMetadataPropertyForBinding:
22+
'Prefer using the `host` metadata property in the `@Component` decorator for host bindings instead of the `@HostBinding` decorator.',
23+
preferHostMetadataPropertyForListener:
24+
'Prefer using the `host` metadata property in the `@Component` decorator for host listeners instead of the `@HostListener` decorator.',
25+
},
26+
},
27+
defaultOptions: [],
28+
create(context) {
29+
return {
30+
[Selectors.HOST_BINDING_DECORATOR]: (node) => {
31+
context.report({
32+
node,
33+
messageId: 'preferHostMetadataPropertyForBinding',
34+
});
35+
},
36+
[Selectors.HOST_LISTENER_DECORATOR]: (node) => {
37+
context.report({
38+
node,
39+
messageId: 'preferHostMetadataPropertyForListener',
40+
});
41+
},
42+
};
43+
},
44+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';
2+
import type { MessageIds } from '../../../src/rules/prefer-host-metadata-property';
3+
4+
const messageIdPreferHostMetadataPropertyForBinding: MessageIds =
5+
'preferHostMetadataPropertyForBinding';
6+
const messageIdPreferHostMetadataPropertyForListener: MessageIds =
7+
'preferHostMetadataPropertyForListener';
8+
9+
export const valid = [
10+
`
11+
@Component({
12+
host: {
13+
class: 'my-class',
14+
},
15+
})
16+
class Test {}
17+
`,
18+
`
19+
@Component({
20+
host: {
21+
'(click)': 'bar()'
22+
},
23+
})
24+
class Test {
25+
bar() {}
26+
}
27+
`,
28+
];
29+
30+
export const invalid = [
31+
convertAnnotatedSourceToFailureCase({
32+
description: 'should fail when a @HostBinding is used',
33+
annotatedSource: `
34+
class Test {
35+
@HostBinding('class')
36+
~~~~~~~~~~~~~~~~~~~~~
37+
readonly cssClass = 'my-class';
38+
}
39+
`,
40+
messageId: messageIdPreferHostMetadataPropertyForBinding,
41+
}),
42+
convertAnnotatedSourceToFailureCase({
43+
description: 'should fail when a @HostListener is used',
44+
annotatedSource: `
45+
class Test {
46+
@HostListener('click')
47+
~~~~~~~~~~~~~~~~~~~~~~
48+
bar() {}
49+
}
50+
`,
51+
messageId: messageIdPreferHostMetadataPropertyForListener,
52+
}),
53+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from '@angular-eslint/test-utils';
2+
import rule, {
3+
RULE_NAME,
4+
} from '../../../src/rules/prefer-host-metadata-property';
5+
import { invalid, valid } from './cases';
6+
7+
const ruleTester = new RuleTester();
8+
9+
ruleTester.run(RULE_NAME, rule, {
10+
valid,
11+
invalid,
12+
});

packages/utils/src/eslint-plugin/selectors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export const INPUT_DECORATOR = 'Decorator[expression.callee.name="Input"]';
2020

2121
export const OUTPUT_DECORATOR = 'Decorator[expression.callee.name="Output"]';
2222

23+
export const HOST_BINDING_DECORATOR =
24+
'Decorator[expression.callee.name="HostBinding"]';
25+
export const HOST_LISTENER_DECORATOR =
26+
'Decorator[expression.callee.name="HostListener"]';
27+
2328
export const LITERAL_OR_TEMPLATE_ELEMENT = ':matches(Literal, TemplateElement)';
2429

2530
export const ALIAS_PROPERTY_VALUE = `ObjectExpression > Property[key.name='alias'] ${LITERAL_OR_TEMPLATE_ELEMENT}`;

0 commit comments

Comments
 (0)