From 2883e185abca4cfd2a7191f5c10742d521f48a89 Mon Sep 17 00:00:00 2001 From: Rafael Santana Date: Tue, 13 Jul 2021 05:08:21 -0300 Subject: [PATCH] fix(eslint-plugin): [no-input-rename] handle alias and `inputs` metadata property (#583) --- .../src/rules/no-input-rename.ts | 257 ++++----- .../src/utils/get-aria-attribute-keys.ts | 56 ++ .../tests/rules/no-input-rename.test.ts | 487 +++++++++++------- 3 files changed, 498 insertions(+), 302 deletions(-) create mode 100644 packages/eslint-plugin/src/utils/get-aria-attribute-keys.ts diff --git a/packages/eslint-plugin/src/rules/no-input-rename.ts b/packages/eslint-plugin/src/rules/no-input-rename.ts index 96c3bfba7..079d30de0 100644 --- a/packages/eslint-plugin/src/rules/no-input-rename.ts +++ b/packages/eslint-plugin/src/rules/no-input-rename.ts @@ -1,74 +1,41 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils'; import { ASTUtils } from '@typescript-eslint/experimental-utils'; import { createESLintRule } from '../utils/create-eslint-rule'; +import { getAriaAttributeKeys } from '../utils/get-aria-attribute-keys'; import { - AngularClassDecorators, - getDecoratorPropertyValue, - isCallExpression, - isImportedFrom, - isStringLiteral, + COMPONENT_OR_DIRECTIVE_SELECTOR_LITERAL, + INPUTS_METADATA_PROPERTY_LITERAL, + INPUT_ALIAS, +} from '../utils/selectors'; +import { + capitalize, + getNearestNodeFrom, + getRawText, + getReplacementText, + isClassPropertyOrMethodDefinition, kebabToCamelCase, + withoutBracketsAndWhitespaces, } from '../utils/utils'; -type Options = [ - { - readonly allowedNames?: readonly string[]; - }, -]; -export type MessageIds = 'noInputRename'; +type Options = [{ readonly allowedNames?: readonly string[] }]; +export type MessageIds = + | 'noInputRename' + | 'suggestRemoveAliasName' + | 'suggestReplaceOriginalNameWithAliasName'; export const RULE_NAME = 'no-input-rename'; const STYLE_GUIDE_LINK = 'https://angular.io/guide/styleguide#style-05-13'; -// source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques -const safelistAliases = new Set([ - 'aria-activedescendant', - 'aria-atomic', - 'aria-autocomplete', - 'aria-busy', - 'aria-checked', - 'aria-controls', - 'aria-current', - 'aria-describedby', - 'aria-disabled', - 'aria-dragged', - 'aria-dropeffect', - 'aria-expanded', - 'aria-flowto', - 'aria-haspopup', - 'aria-hidden', - 'aria-invalid', - 'aria-label', - 'aria-labelledby', - 'aria-level', - 'aria-live', - 'aria-multiline', - 'aria-multiselectable', - 'aria-orientation', - 'aria-owns', - 'aria-posinset', - 'aria-pressed', - 'aria-readonly', - 'aria-relevant', - 'aria-required', - 'aria-selected', - 'aria-setsize', - 'aria-sort', - 'aria-valuemax', - 'aria-valuemin', - 'aria-valuenow', - 'aria-valuetext', -]); - export default createESLintRule({ name: RULE_NAME, meta: { type: 'suggestion', docs: { - description: - 'Disallows renaming directive inputs by providing a string to the decorator.', + description: 'Ensures that input bindings are not aliased', category: 'Best Practices', recommended: 'error', + suggestion: true, }, + fixable: 'code', schema: [ { type: 'object', @@ -86,92 +53,138 @@ export default createESLintRule({ }, ], messages: { - noInputRename: `@Inputs should not be aliased (${STYLE_GUIDE_LINK})`, + noInputRename: `Input bindings should not be aliased (${STYLE_GUIDE_LINK})`, + suggestRemoveAliasName: 'Remove alias name', + suggestReplaceOriginalNameWithAliasName: + 'Remove alias name and use it as the original name', }, }, - defaultOptions: [ - { - allowedNames: [], - }, - ], + defaultOptions: [{ allowedNames: [] }], create(context, [{ allowedNames = [] }]) { + let selectors: ReadonlySet = new Set(); + const ariaAttributeKeys = getAriaAttributeKeys(); + return { - ':matches(ClassProperty, MethodDefinition[kind="set"]) > Decorator[expression.callee.name="Input"]'( - node: TSESTree.Decorator, + [COMPONENT_OR_DIRECTIVE_SELECTOR_LITERAL]( + node: TSESTree.Literal | TSESTree.TemplateElement, ) { - const inputCallExpression = node.expression as TSESTree.CallExpression; + selectors = new Set( + withoutBracketsAndWhitespaces(getRawText(node)).split(','), + ); + }, + [INPUT_ALIAS](node: TSESTree.Literal | TSESTree.TemplateElement) { + const classPropertyOrMethodDefinition = getNearestNodeFrom( + node, + isClassPropertyOrMethodDefinition, + ); if ( - !isImportedFrom( - inputCallExpression.callee as TSESTree.Identifier, - '@angular/core', - ) - ) + !classPropertyOrMethodDefinition || + !ASTUtils.isIdentifier(classPropertyOrMethodDefinition.key) + ) { return; - if (inputCallExpression.arguments.length === 0) return; - - // handle directive's selector is also an input property - let directiveSelectors: readonly string[]; - - const canPropertyBeAliased = ( - propertyAlias: string, - propertyName: string, - ): boolean => { - return ( - allowedNames.includes(propertyAlias) || - (propertyAlias !== propertyName && - directiveSelectors && - directiveSelectors.some((x) => - new RegExp( - `^${x}((${ - propertyName[0].toUpperCase() + propertyName.slice(1) - }$)|(?=$))`, - ).test(propertyAlias), - )) || - (safelistAliases.has(propertyAlias) && - propertyName === kebabToCamelCase(propertyAlias)) - ); - }; - - const classProperty = node.parent as - | TSESTree.ClassProperty - | TSESTree.MethodDefinition; - const { decorators } = (classProperty.parent as TSESTree.ClassBody) - .parent as TSESTree.ClassDeclaration; - const decorator = decorators?.find( - (decorator) => - isCallExpression(decorator.expression) && - ASTUtils.isIdentifier(decorator.expression.callee) && - decorator.expression.callee.name === - AngularClassDecorators.Directive, - ); + } - if (decorator) { - const selector = getDecoratorPropertyValue(decorator, 'selector'); + const aliasName = getRawText(node); + const propertyName = getRawText(classPropertyOrMethodDefinition.key); - if (selector && isStringLiteral(selector)) { - directiveSelectors = selector.value - .replace(/[[\]\s]/g, '') - .split(','); - } + if ( + allowedNames.includes(aliasName) || + (ariaAttributeKeys.has(aliasName) && + propertyName === kebabToCamelCase(aliasName)) + ) { + return; } - const propertyAlias = ( - inputCallExpression.arguments[0] as TSESTree.Literal - ).value; + if (aliasName === propertyName) { + context.report({ + node, + messageId: 'noInputRename', + fix: (fixer) => fixer.remove(node), + }); + } else if (!isAliasNameAllowed(selectors, propertyName, aliasName)) { + context.report({ + node, + messageId: 'noInputRename', + suggest: [ + { + messageId: 'suggestRemoveAliasName', + fix: (fixer) => fixer.remove(node), + }, + { + messageId: 'suggestReplaceOriginalNameWithAliasName', + fix: (fixer) => [ + fixer.remove(node), + fixer.replaceText( + classPropertyOrMethodDefinition.key, + aliasName.includes('-') ? `'${aliasName}'` : aliasName, + ), + ], + }, + ], + }); + } + }, + [INPUTS_METADATA_PROPERTY_LITERAL]( + node: TSESTree.Literal | TSESTree.TemplateElement, + ) { + const [propertyName, aliasName] = withoutBracketsAndWhitespaces( + getRawText(node), + ).split(':'); if ( - propertyAlias && - ASTUtils.isIdentifier(classProperty.key) && - canPropertyBeAliased(propertyAlias.toString(), classProperty.key.name) - ) + !aliasName || + allowedNames.includes(aliasName) || + (ariaAttributeKeys.has(aliasName) && + propertyName === kebabToCamelCase(aliasName)) + ) { return; + } - context.report({ - node: classProperty, - messageId: 'noInputRename', - }); + if (aliasName === propertyName) { + context.report({ + node, + messageId: 'noInputRename', + fix: (fixer) => + fixer.replaceText(node, getReplacementText(node, propertyName)), + }); + } else if (!isAliasNameAllowed(selectors, propertyName, aliasName)) { + context.report({ + node, + messageId: 'noInputRename', + suggest: ( + [ + ['suggestRemoveAliasName', propertyName], + ['suggestReplaceOriginalNameWithAliasName', aliasName], + ] as const + ).map(([messageId, name]) => ({ + messageId, + fix: (fixer) => + fixer.replaceText(node, getReplacementText(node, name)), + })), + }); + } + }, + 'ClassDeclaration:exit'() { + selectors = new Set(); }, }; }, }); + +function composedName(selector: string, propertyName: string): string { + return `${selector}${capitalize(propertyName)}`; +} + +function isAliasNameAllowed( + selectors: ReadonlySet, + propertyName: string, + aliasName: string, +): boolean { + return [...selectors].some((selector) => { + return ( + selector === aliasName || + composedName(selector, propertyName) === aliasName + ); + }); +} diff --git a/packages/eslint-plugin/src/utils/get-aria-attribute-keys.ts b/packages/eslint-plugin/src/utils/get-aria-attribute-keys.ts new file mode 100644 index 000000000..bcaf69505 --- /dev/null +++ b/packages/eslint-plugin/src/utils/get-aria-attribute-keys.ts @@ -0,0 +1,56 @@ +let ariaAttributeKeys: ReadonlySet | null = null; + +export function getAriaAttributeKeys(): ReadonlySet { + return ( + ariaAttributeKeys ?? + (ariaAttributeKeys = new Set([ + // Source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques + 'aria-activedescendant', + 'aria-atomic', + 'aria-autocomplete', + 'aria-busy', + 'aria-checked', + 'aria-colcount', + 'aria-colindex', + 'aria-colspan', + 'aria-controls', + 'aria-current', + 'aria-describedby', + 'aria-details', + 'aria-disabled', + 'aria-dragged', + 'aria-dropeffect', + 'aria-errormessage', + 'aria-expanded', + 'aria-flowto', + 'aria-haspopup', + 'aria-hidden', + 'aria-invalid', + 'aria-label', + 'aria-labelledby', + 'aria-level', + 'aria-live', + 'aria-modal', + 'aria-multiline', + 'aria-multiselectable', + 'aria-orientation', + 'aria-owns', + 'aria-placeholder', + 'aria-posinset', + 'aria-pressed', + 'aria-readonly', + 'aria-relevant', + 'aria-required', + 'aria-rowcount', + 'aria-rowindex', + 'aria-rowspan', + 'aria-selected', + 'aria-setsize', + 'aria-sort', + 'aria-valuemax', + 'aria-valuemin', + 'aria-valuenow', + 'aria-valuetext', + ])) + ); +} diff --git a/packages/eslint-plugin/tests/rules/no-input-rename.test.ts b/packages/eslint-plugin/tests/rules/no-input-rename.test.ts index 67a3e4928..f4ffeab3d 100644 --- a/packages/eslint-plugin/tests/rules/no-input-rename.test.ts +++ b/packages/eslint-plugin/tests/rules/no-input-rename.test.ts @@ -11,292 +11,419 @@ import rule, { RULE_NAME } from '../../src/rules/no-input-rename'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - }, }); const messageId: MessageIds = 'noInputRename'; +const suggestRemoveAliasName: MessageIds = 'suggestRemoveAliasName'; +const suggestReplaceOriginalNameWithAliasName: MessageIds = + 'suggestReplaceOriginalNameWithAliasName'; ruleTester.run(RULE_NAME, rule, { valid: [ - // should succeed when a component input property is not renamed + `class Test {}`, ` - import { Input } from '@angular/core'; - @Component - class TestComponent { - @Input() label: string; + @Page({ + inputs: ['play', popstate, \`online\`, 'obsolete: obsol', 'store: storage'], + }) + class Test {} + `, + ` + @Component() + class Test { + change = new EventEmitter(); } `, - // should succeed when a component input setter is not renamed ` - import { Input } from '@angular/core'; - @Component - class TestComponent { - @Input() set label(label: string) {} + @Directive() + class Test { + @Input() buttonChange = new EventEmitter<'change'>(); } `, - // should succeed when a directive selector is strictly equal to the alias ` - import { Input } from '@angular/core'; - @Directive({ - selector: '[foo]' + @Component({ + inputs, }) - class TestDirective { - @Input('foo') bar = new EventEmitter(); - } + class Test {} `, - // should succeed when the first directive selector is strictly equal to the alias ` - import { Input } from '@angular/core'; @Directive({ - selector: '[foo], test' + inputs: [...test], }) - class TestDirective { - @Input('foo') bar = new EventEmitter(); - } + class Test {} `, - // should succeed when the second directive selector is strictly equal to the alias ` - import { Input } from '@angular/core'; - @Directive({ - selector: '[foo], myselector' + @Component({ + inputs: func(), }) - class TestDirective { - @Input('myselector') bar: string; - } + class Test {} `, - // should succeed when a directive selector is also an input property ` - import { Input } from '@angular/core'; @Directive({ - selector: '[foo], label2' + inputs: [func(), 'a'], }) - class TestDirective { - @Input() foo: string; + class Test {} + `, + ` + @Component({}) + class Test { + @Input() set setter(setter: string) {} } `, - // should succeed when a directive selector is also an input property with tag + { + code: ` + @Component({ + inputs: ['foo: aria-wrong'] + }) + class Test { + @Input('aria-wrong') set setter(setter: string) {} + } + `, + options: [{ allowedNames: ['aria-wrong'] }], + }, ` - import { Input } from '@angular/core'; - @Directive({ + const change = 'change'; + @Component() + class Test { + @Input(change) touchMove: EventEmitter<{ action: 'click' | 'close' }> = new EventEmitter<{ action: 'click' | 'close' }>(); + } + `, + ` + const blur = 'blur'; + const click = 'click'; + @Directive() + class Test { + @Input(blur) [click]: EventEmitter; + } + `, + ` + @Component({ selector: 'foo[bar]' }) - class TestDirective { + class Test { @Input() bar: string; } `, - // should succeed when an input alias is kebab-cased and whitelisted ` - import { Input } from '@angular/core'; @Directive({ - selector: 'foo' + 'selector': 'foo', + 'inputs': [\`test: ${'foo'}\`] }) - class TestDirective { - @Input('aria-label') ariaLabel: string; - } + class Test {} `, - // should succeed when an input alias is strictly equal to the selector plus the property name ` - import { Input } from '@angular/core'; - @Directive({ - selector: 'foo' + @Component({ + selector: '[foo], test', }) - class TestDirective { - @Input('fooMyColor') myColor: string; + class Test { + @Input('foo') label: string; } `, - // should succeed when an Input decorator is not imported from '@angular/core' ` - import { Input } from 'baz'; - @Component({ + @Directive({ selector: 'foo' }) - class TestComponent { - @Input('bar') label: string; + class Test { + @Input('aria-label') ariaLabel: string; } `, - // should succeed when an input alias rename to the value listed in the allowedRenames { code: ` - import { Input } from '@angular/core'; - @Directive({ - selector: 'foo' + @Component({ + inputs: ['foo: allowedName'] }) - class TestDirective { - @Input('allowedName') bar: string; + class Test { + @Input() bar: string; } `, - options: [ - { - allowedNames: ['allowedName'], - }, - ], + options: [{ allowedNames: ['allowedName'] }], }, + ` + @Directive({ + selector: 'foo' + }) + class Test { + @Input('fooMyColor') myColor: string; + } + `, ], invalid: [ convertAnnotatedSourceToFailureCase({ - description: 'should fail when a component input property is renamed', + description: + 'should fail if `inputs` metadata property is aliased in `@Component`', annotatedSource: ` - import { Input } from '@angular/core'; - @Component({ - selector: 'foo' - }) - class TestComponent { - @Input('bar') label: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Component({ + inputs: ['a: b'] + ~~~~~~ + }) + class Test {} `, messageId, - }), - convertAnnotatedSourceToFailureCase({ - description: 'should fail when a component input setter is renamed', - annotatedSource: ` - import { Input } from '@angular/core'; - @Component({ - selector: 'foo' - }) - class TestComponent { - @Input('bar') set label(label: string) {} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + suggestions: ( + [ + [suggestRemoveAliasName, 'a'], + [suggestReplaceOriginalNameWithAliasName, 'b'], + ] as const + ).map(([messageId, name]) => ({ + messageId, + output: ` + @Component({ + inputs: ['${name}'] + + }) + class Test {} `, - messageId, + })), }), convertAnnotatedSourceToFailureCase({ description: - 'should fail when a component input property is fake renamed', + 'should fail if `inputs` metadata property is aliased in `@Directive`', annotatedSource: ` - import { Input } from '@angular/core'; - @Component({ - selector: 'foo' - }) - class TestComponent { - @Input('foo') label: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Directive({ + outputs: ['abort'], + inputs: [boundary, \`test: copy\`, 'check: check'], + ~~~~~~~~~~~~ + }) + class Test {} `, messageId, + options: [{ allowedNames: ['check', 'test'] }], + suggestions: ( + [ + [suggestRemoveAliasName, 'test'], + [suggestReplaceOriginalNameWithAliasName, 'copy'], + ] as const + ).map(([messageId, name]) => ({ + messageId, + output: ` + @Directive({ + outputs: ['abort'], + inputs: [boundary, \`${name}\`, 'check: check'], + + }) + class Test {} + `, + })), }), convertAnnotatedSourceToFailureCase({ - description: 'should fail when a component input setter is fake renamed', + description: + 'should fail if `inputs` metadata property is `Literal` and aliased with the same name in `@Component`', annotatedSource: ` - import { Input } from '@angular/core'; - @Component({ - selector: 'foo' - }) - class TestComponent { - @Input('foo') set label(label: string) {} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~s - } + @Component({ + 'inputs': ['orientation: orientation'], + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + }) + class Test {} `, messageId, + annotatedOutput: ` + @Component({ + 'inputs': ['orientation'], + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + }) + class Test {} + `, }), convertAnnotatedSourceToFailureCase({ - description: 'should fail when a directive input property is renamed', + description: 'should fail if input property is aliased with backticks', annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: '[foo]' - }) - class TestDirective { - @Input('labelText') label: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Component() + class Test { + @Custom() @Input(\`change\`) _change = getInput(); + ~~~~~~~~ + } `, messageId, + suggestions: ( + [ + [suggestRemoveAliasName, '_change'], + [suggestReplaceOriginalNameWithAliasName, 'change'], + ] as const + ).map(([messageId, propertyName]) => ({ + messageId, + output: ` + @Component() + class Test { + @Custom() @Input() ${propertyName} = getInput(); + + } + `, + })), }), convertAnnotatedSourceToFailureCase({ - description: - 'should fail when a directive input property is renamed and its name is strictly equal to the property', + description: 'should fail if input property is aliased', annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: '[label]' - }) - class TestDirective { - @Input('label') label: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Directive() + class Test { + @Input('change') change = (this.subject$ as Subject<{blur: boolean}>).pipe(); + ~~~~~~~~ + } `, messageId, + annotatedOutput: ` + @Directive() + class Test { + @Input() change = (this.subject$ as Subject<{blur: boolean}>).pipe(); + ~~~~~~~~ + } + `, }), convertAnnotatedSourceToFailureCase({ - description: - 'should fail when a directive input property has the same name as the alias', + description: 'should fail if input setter is aliased', annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: '[foo]' - }) - class TestDirective { - @Input('label') label: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Component() + class Test { + @Input(\`${'devicechange'}\`) set setter(setter: string) {} + ~~~~~~~~~~~~~~ + + @Input('allowedName') test: string; + } `, messageId, + options: [{ allowedNames: ['allowedName'] }], + suggestions: ( + [ + [suggestRemoveAliasName, 'setter'], + [suggestReplaceOriginalNameWithAliasName, 'devicechange'], + ] as const + ).map(([messageId, propertyName]) => ({ + messageId, + output: ` + @Component() + class Test { + @Input() set ${propertyName}(setter: string) {} + + + @Input('allowedName') test: string; + } + `, + })), }), convertAnnotatedSourceToFailureCase({ - description: `should fail when a directive input alias is kebab-cased and whitelisted, but the property doesn't match the alias`, + description: `should fail if a input 'aria-*' alias name does not match the property name`, annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: 'foo' - }) - class TestDirective { - @Input('aria-invalid') ariaBusy: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Directive({ + selector: 'foo' + }) + class Test { + @Input('aria-invalid') ariaBusy: string; + ~~~~~~~~~~~~~~ + } `, messageId, + suggestions: ( + [ + [suggestRemoveAliasName, 'ariaBusy'], + [suggestReplaceOriginalNameWithAliasName, "'aria-invalid'"], + ] as const + ).map(([messageId, propertyName]) => ({ + messageId, + output: ` + @Directive({ + selector: 'foo' + }) + class Test { + @Input() ${propertyName}: string; + + } + `, + })), }), convertAnnotatedSourceToFailureCase({ - description: `should fail when a directive input alias is prefixed by directive's selector, but the suffix does not match the property name`, + description: `should fail if input alias is prefixed by directive's selector, but the suffix does not match the property name`, annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: 'foo' - }) - class TestDirective { - @Input('fooColor') colors: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Component({ + selector: 'foo' + }) + class Test { + @Input('fooColor') colors: string; + ~~~~~~~~~~ + } `, messageId, + suggestions: ( + [ + [suggestRemoveAliasName, 'colors'], + [suggestReplaceOriginalNameWithAliasName, 'fooColor'], + ] as const + ).map(([messageId, propertyName]) => ({ + messageId, + output: ` + @Component({ + selector: 'foo' + }) + class Test { + @Input() ${propertyName}: string; + + } + `, + })), }), convertAnnotatedSourceToFailureCase({ description: - 'should fail when a directive input alias is not strictly equal to the selector plus the property name', + 'should fail if input alias is not strictly equal to the selector plus the property name in `camelCase` form', annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: 'foo' - }) - class TestDirective { - @Input('foocolor') color: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Directive({ + 'selector': 'foo' + }) + class Test { + @Input('foocolor') color: string; + ~~~~~~~~~~ + } `, messageId, + suggestions: ( + [ + [suggestRemoveAliasName, 'color'], + [suggestReplaceOriginalNameWithAliasName, 'foocolor'], + ] as const + ).map(([messageId, propertyName]) => ({ + messageId, + output: ` + @Directive({ + 'selector': 'foo' + }) + class Test { + @Input() ${propertyName}: string; + + } + `, + })), }), convertAnnotatedSourceToFailureCase({ description: - 'should fail when a directive input property is renamed and not listed in the allowedRenames list', + 'should fail if input property is aliased without `@Component` or `@Directive` decorator', annotatedSource: ` - import { Input } from '@angular/core'; - @Directive({ - selector: 'foo' - }) - class TestDirective { - @Input('disallowedName') bar: string; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } + @Component({ + selector: 'click', + }) + class Test {} + + @Injectable() + class Test { + @Input('click') blur = this.getInput(); + ~~~~~~~ + } `, messageId, - options: [ - { - allowedNames: ['allowedName'], - }, - ], + suggestions: ( + [ + [suggestRemoveAliasName, 'blur'], + [suggestReplaceOriginalNameWithAliasName, 'click'], + ] as const + ).map(([messageId, propertyName]) => ({ + messageId, + output: ` + @Component({ + selector: 'click', + }) + class Test {} + + @Injectable() + class Test { + @Input() ${propertyName} = this.getInput(); + + } + `, + })), }), ], });