diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 1dc054775..4392e5b27 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -8,6 +8,7 @@ "@angular-eslint/contextual-lifecycle": "error", "@angular-eslint/directive-class-suffix": "error", "@angular-eslint/directive-selector": "error", + "@angular-eslint/input-not-event-emitter": "error", "@angular-eslint/no-attribute-decorator": "error", "@angular-eslint/no-conflicting-lifecycle": "error", "@angular-eslint/no-empty-lifecycle-method": "error", diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index 95938a4f3..f12687d37 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -26,6 +26,9 @@ import directiveClassSuffix, { import directiveSelector, { RULE_NAME as directiveSelectorRuleName, } from './rules/directive-selector'; +import inputNotEventEmitter, { + RULE_NAME as inputNotEventEmitterRuleName, +} from './rules/input-not-event-emitter'; import noAttributeDecorator, { RULE_NAME as noAttributeDecoratorRuleName, } from './rules/no-attribute-decorator'; @@ -119,6 +122,7 @@ export default { [contextualLifecycleRuleName]: contextualLifecycle, [directiveClassSuffixRuleName]: directiveClassSuffix, [directiveSelectorRuleName]: directiveSelector, + [inputNotEventEmitterRuleName]: inputNotEventEmitter, [noAttributeDecoratorRuleName]: noAttributeDecorator, [noConflictingLifecycleRuleName]: noConflictingLifecycle, [noForwardRefRuleName]: noForwardRef, diff --git a/packages/eslint-plugin/src/rules/input-not-event-emitter.ts b/packages/eslint-plugin/src/rules/input-not-event-emitter.ts new file mode 100644 index 000000000..0862969e0 --- /dev/null +++ b/packages/eslint-plugin/src/rules/input-not-event-emitter.ts @@ -0,0 +1,46 @@ +import { Selectors } from '@angular-eslint/utils'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { createESLintRule } from '../utils/create-eslint-rule'; + +type Options = []; +export type MessageIds = 'inputNotEventEmitter' | 'suggestChangeInputToOutput'; +export const RULE_NAME = 'input-not-event-emitter'; + +export default createESLintRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer to not declare `@Input` with an `EventEmitter`', + recommended: false, + }, + hasSuggestions: true, + schema: [], + messages: { + inputNotEventEmitter: + 'Prefer to not declare `@Input` with an `EventEmitter`', + suggestChangeInputToOutput: 'Change `@Input` to `@Output`', + }, + }, + defaultOptions: [], + create(context) { + return { + [`${Selectors.INPUT_DECORATOR} > ${Selectors.OUTPUT_DECORATOR}`]({ + parent: type, + }: { + parent: TSESTree.Decorator; + }) { + context.report({ + node: type, + messageId: 'inputNotEventEmitter', + suggest: [ + { + messageId: 'suggestChangeInputToOutput', + fix: (fixer) => fixer.replaceText(type, '@Output '), + }, + ], + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/input-not-event-emitter/cases.ts b/packages/eslint-plugin/tests/rules/input-not-event-emitter/cases.ts new file mode 100644 index 000000000..f7cb1caee --- /dev/null +++ b/packages/eslint-plugin/tests/rules/input-not-event-emitter/cases.ts @@ -0,0 +1,42 @@ +import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/utils'; +import type { MessageIds } from '../../../src/rules/input-not-event-emitter'; + +const messageId: MessageIds = 'inputNotEventEmitter'; +const suggestChangeInputToOutput: MessageIds = 'suggestChangeInputToOutput'; + +export const valid = [ + ` + class Test { + testEmitter = new EventEmitter(); + } + `, + ` + class Test { + @Output() readonly testEmitter = new EventEmitter(); + } + `, +]; + +export const invalid = [ + convertAnnotatedSourceToFailureCase({ + description: 'should fail when an @Input is declared as an `EventEmitter`', + annotatedSource: ` + class Test { + @Input() readonly testEmitter = new EventEmitter(); + ~~~~~~~~ + } + `, + messageId, + suggestions: [ + { + messageId: suggestChangeInputToOutput, + output: ` + class Test { + @Output() readonly testEmitter = new EventEmitter(); + + } + `, + }, + ], + }), +]; \ No newline at end of file diff --git a/packages/eslint-plugin/tests/rules/input-not-event-emitter/spec.ts b/packages/eslint-plugin/tests/rules/input-not-event-emitter/spec.ts new file mode 100644 index 000000000..241f031c2 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/input-not-event-emitter/spec.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '@angular-eslint/utils'; +import rule, { RULE_NAME } from '../../../src/rules/input-not-event-emitter'; +import { invalid, valid } from './cases'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run(RULE_NAME, rule, { + valid, + invalid, +}); \ No newline at end of file