Skip to content

Commit 6d516a8

Browse files
committed
fix(eslint-plugin): no-input-prefix and no-input-rename work with alias metadata
1 parent f79b3cb commit 6d516a8

File tree

9 files changed

+427
-67
lines changed

9 files changed

+427
-67
lines changed

packages/eslint-plugin/docs/rules/no-input-prefix.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,41 @@ class Test {
315315

316316
#### ❌ Invalid Code
317317

318+
```ts
319+
@Component()
320+
class Test {
321+
@Custom() @Input({ required: true, alias: `on` }) _on = getInput();
322+
~~~~
323+
}
324+
```
325+
326+
<br>
327+
328+
---
329+
330+
<br>
331+
332+
#### Custom Config
333+
334+
```json
335+
{
336+
"rules": {
337+
"@angular-eslint/no-input-prefix": [
338+
"error",
339+
{
340+
"prefixes": [
341+
"on"
342+
]
343+
}
344+
]
345+
}
346+
}
347+
```
348+
349+
<br>
350+
351+
#### ❌ Invalid Code
352+
318353
```ts
319354
@Directive()
320355
class Test {
@@ -350,6 +385,41 @@ class Test {
350385

351386
#### ❌ Invalid Code
352387

388+
```ts
389+
@Directive()
390+
class Test {
391+
@Input({ alias: 'onPrefix', required: true }) _on = (this.subject$ as Subject<{on: boolean}>).pipe();
392+
~~~~~~~~~~
393+
}
394+
```
395+
396+
<br>
397+
398+
---
399+
400+
<br>
401+
402+
#### Custom Config
403+
404+
```json
405+
{
406+
"rules": {
407+
"@angular-eslint/no-input-prefix": [
408+
"error",
409+
{
410+
"prefixes": [
411+
"on"
412+
]
413+
}
414+
]
415+
}
416+
}
417+
```
418+
419+
<br>
420+
421+
#### ❌ Invalid Code
422+
353423
```ts
354424
@Component()
355425
class Test {
@@ -630,6 +700,40 @@ class Test {
630700

631701
#### ✅ Valid Code
632702

703+
```ts
704+
@Directive()
705+
class Test {
706+
@Input({ alias: \`one\` }) ontype = new EventEmitter<{ bar: string, on: boolean }>();
707+
}
708+
```
709+
710+
<br>
711+
712+
---
713+
714+
<br>
715+
716+
#### Custom Config
717+
718+
```json
719+
{
720+
"rules": {
721+
"@angular-eslint/no-input-prefix": [
722+
"error",
723+
{
724+
"prefixes": [
725+
"on"
726+
]
727+
}
728+
]
729+
}
730+
}
731+
```
732+
733+
<br>
734+
735+
#### ✅ Valid Code
736+
633737
```ts
634738
@Component()
635739
class Test {
@@ -664,6 +768,40 @@ class Test {
664768

665769
#### ✅ Valid Code
666770

771+
```ts
772+
@Component()
773+
class Test {
774+
@Input({ alias: 'oneProp' }) common = new EventEmitter<ComplextOn>();
775+
}
776+
```
777+
778+
<br>
779+
780+
---
781+
782+
<br>
783+
784+
#### Custom Config
785+
786+
```json
787+
{
788+
"rules": {
789+
"@angular-eslint/no-input-prefix": [
790+
"error",
791+
{
792+
"prefixes": [
793+
"on"
794+
]
795+
}
796+
]
797+
}
798+
}
799+
```
800+
801+
<br>
802+
803+
#### ✅ Valid Code
804+
667805
```ts
668806
@Directive()
669807
class Test<On> {

packages/eslint-plugin/docs/rules/no-input-rename.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,36 @@ class Test {
256256

257257
#### ❌ Invalid Code
258258

259+
```ts
260+
@Directive()
261+
class Test {
262+
@Input({ alias: 'change' }) change = (this.subject$ as Subject<{blur: boolean}>).pipe();
263+
~~~~~~~~
264+
}
265+
```
266+
267+
<br>
268+
269+
---
270+
271+
<br>
272+
273+
#### Default Config
274+
275+
```json
276+
{
277+
"rules": {
278+
"@angular-eslint/no-input-rename": [
279+
"error"
280+
]
281+
}
282+
}
283+
```
284+
285+
<br>
286+
287+
#### ❌ Invalid Code
288+
259289
```ts
260290
@Directive({
261291
selector: 'foo'
@@ -1080,6 +1110,37 @@ class Test {
10801110
}
10811111
```
10821112

1113+
<br>
1114+
1115+
---
1116+
1117+
<br>
1118+
1119+
#### Default Config
1120+
1121+
```json
1122+
{
1123+
"rules": {
1124+
"@angular-eslint/no-input-rename": [
1125+
"error"
1126+
]
1127+
}
1128+
}
1129+
```
1130+
1131+
<br>
1132+
1133+
#### ✅ Valid Code
1134+
1135+
```ts
1136+
@Component({
1137+
selector: 'foo'
1138+
})
1139+
class Test {
1140+
@Input({ required: true }) name: string;
1141+
}
1142+
```
1143+
10831144
</details>
10841145

10851146
<br>

packages/eslint-plugin/src/rules/no-input-prefix.ts

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,76 @@ export default createESLintRule<Options, MessageIds>({
4040
},
4141
defaultOptions: [{ prefixes: [] }],
4242
create(context, [{ prefixes }]) {
43-
const selectors = [
44-
Selectors.INPUTS_METADATA_PROPERTY_LITERAL,
45-
Selectors.INPUT_ALIAS,
46-
Selectors.INPUT_PROPERTY_OR_SETTER,
47-
].join(',');
48-
4943
return {
50-
[selectors](
44+
[Selectors.INPUT_PROPERTY_OR_SETTER](
45+
node: TSESTree.Identifier | TSESTree.Literal | TSESTree.TemplateElement,
46+
) {
47+
const rawPropertyName = ASTUtils.getRawText(node);
48+
const hasDisallowedPrefix = prefixes.some((prefix) =>
49+
isDisallowedPrefix(prefix, rawPropertyName),
50+
);
51+
52+
// Direct violation on the property name
53+
if (hasDisallowedPrefix) {
54+
context.report({
55+
node,
56+
messageId: 'noInputPrefix',
57+
data: {
58+
prefixes: toHumanReadableText(prefixes),
59+
},
60+
});
61+
}
62+
63+
// Check if decorator alias has a violation
64+
let aliasProperty: TSESTree.Node | undefined;
65+
if (!node.parent) {
66+
return;
67+
}
68+
const inputDecorator = ASTUtils.getDecorator(
69+
node.parent as any,
70+
'Input',
71+
);
72+
73+
if (
74+
inputDecorator &&
75+
ASTUtils.isCallExpression(inputDecorator.expression)
76+
) {
77+
// Angular 16+ alias property syntax
78+
aliasProperty = ASTUtils.getDecoratorPropertyValue(
79+
inputDecorator,
80+
'alias',
81+
);
82+
let aliasValue = '';
83+
let aliasArg: TSESTree.Node | undefined;
84+
if (aliasProperty) {
85+
aliasValue = ASTUtils.getRawText(aliasProperty);
86+
} else if (
87+
inputDecorator.expression.arguments.length > 0 &&
88+
(ASTUtils.isLiteral(inputDecorator.expression.arguments[0]) ||
89+
ASTUtils.isTemplateLiteral(
90+
inputDecorator.expression.arguments[0],
91+
))
92+
) {
93+
aliasArg = inputDecorator.expression.arguments[0];
94+
aliasValue = ASTUtils.getRawText(aliasArg);
95+
}
96+
const hasDisallowedPrefix = prefixes.some((prefix) =>
97+
isDisallowedPrefix(prefix, aliasValue),
98+
);
99+
if (!hasDisallowedPrefix) {
100+
return;
101+
}
102+
103+
return context.report({
104+
node: aliasProperty || aliasArg || node,
105+
messageId: 'noInputPrefix',
106+
data: {
107+
prefixes: toHumanReadableText(prefixes),
108+
},
109+
});
110+
}
111+
},
112+
[Selectors.INPUTS_METADATA_PROPERTY_LITERAL](
51113
node: TSESTree.Identifier | TSESTree.Literal | TSESTree.TemplateElement,
52114
) {
53115
const [propertyName, aliasName] = ASTUtils.getRawText(node)
@@ -76,8 +138,8 @@ export default createESLintRule<Options, MessageIds>({
76138
function isDisallowedPrefix(
77139
prefix: string,
78140
propertyName: string,
79-
aliasName: string,
141+
aliasName = '',
80142
): boolean {
81-
const prefixPattern = RegExp(`^${prefix}(([^a-z])|(?=$))`);
143+
const prefixPattern = new RegExp(`^${prefix}(([^a-z])|(?=$))`);
82144
return prefixPattern.test(propertyName) || prefixPattern.test(aliasName);
83145
}

packages/eslint-plugin/src/rules/no-input-rename.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ export default createESLintRule<Options, MessageIds>({
8787
return;
8888
}
8989

90+
let isAliasMetadataProperty = false;
91+
92+
if (node.parent && ASTUtils.isProperty(node.parent)) {
93+
if (ASTUtils.getRawText(node.parent.key) !== 'alias') {
94+
// We're within an Input decorator metadata object, but it is not the alias property
95+
return;
96+
}
97+
isAliasMetadataProperty = true;
98+
}
99+
90100
const aliasName = ASTUtils.getRawText(node);
91101
const propertyName = ASTUtils.getRawText(
92102
propertyOrMethodDefinition.key,
@@ -104,7 +114,12 @@ export default createESLintRule<Options, MessageIds>({
104114
context.report({
105115
node,
106116
messageId: 'noInputRename',
107-
fix: (fixer) => fixer.remove(node),
117+
fix: (fixer) => {
118+
if (node.parent && isAliasMetadataProperty) {
119+
return fixer.remove(node.parent);
120+
}
121+
return fixer.remove(node);
122+
},
108123
});
109124
} else if (
110125
!isAliasNameAllowed(
@@ -120,7 +135,12 @@ export default createESLintRule<Options, MessageIds>({
120135
suggest: [
121136
{
122137
messageId: 'suggestRemoveAliasName',
123-
fix: (fixer) => fixer.remove(node),
138+
fix: (fixer) => {
139+
if (node.parent && isAliasMetadataProperty) {
140+
return fixer.remove(node.parent);
141+
}
142+
return fixer.remove(node);
143+
},
124144
},
125145
{
126146
messageId: 'suggestReplaceOriginalNameWithAliasName',

0 commit comments

Comments
 (0)