diff --git a/packages/eslint-plugin/docs/rules/no-output-rename.md b/packages/eslint-plugin/docs/rules/no-output-rename.md index 733aa2a79..ac8617f3a 100644 --- a/packages/eslint-plugin/docs/rules/no-output-rename.md +++ b/packages/eslint-plugin/docs/rules/no-output-rename.md @@ -725,6 +725,105 @@ class Test { #### ✅ Valid Code +```ts +@Component({ + selector: 'foo', + hostDirectives: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/no-output-rename": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + selector: 'foo', + 'hostDirectives': [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/no-output-rename": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + selector: 'foo', + ['hostDirectives']: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/no-output-rename": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + ```ts @Directive({ selector: 'foo' diff --git a/packages/eslint-plugin/docs/rules/no-outputs-metadata-property.md b/packages/eslint-plugin/docs/rules/no-outputs-metadata-property.md index e8b5b73a4..600025b9c 100644 --- a/packages/eslint-plugin/docs/rules/no-outputs-metadata-property.md +++ b/packages/eslint-plugin/docs/rules/no-outputs-metadata-property.md @@ -489,6 +489,105 @@ class Test {} class Test {} ``` +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/no-outputs-metadata-property": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + selector: 'foo', + hostDirectives: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/no-outputs-metadata-property": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + selector: 'foo', + 'hostDirectives': [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/no-outputs-metadata-property": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + selector: 'foo', + ['hostDirectives']: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] +}) +class Test {} +``` +
diff --git a/packages/eslint-plugin/src/rules/no-output-rename.ts b/packages/eslint-plugin/src/rules/no-output-rename.ts index 23f71258a..6d446d085 100644 --- a/packages/eslint-plugin/src/rules/no-output-rename.ts +++ b/packages/eslint-plugin/src/rules/no-output-rename.ts @@ -98,6 +98,29 @@ export default createESLintRule({ [Selectors.OUTPUTS_METADATA_PROPERTY_LITERAL]( node: TSESTree.Literal | TSESTree.TemplateElement, ) { + const ancestorMaybeHostDirectiveAPI = + node.parent?.parent?.parent?.parent?.parent; + if ( + ancestorMaybeHostDirectiveAPI && + ASTUtils.isProperty(ancestorMaybeHostDirectiveAPI) + ) { + /** + * Angular v15 introduced the directive composition API: https://angular.io/guide/directive-composition-api + * Renaming host directive outputs using this API is not a bad practice and should not be reported + */ + const hostDirectiveAPIPropertyName = 'hostDirectives'; + if ( + (ASTUtils.isLiteral(ancestorMaybeHostDirectiveAPI.key) && + ancestorMaybeHostDirectiveAPI.key.value === + hostDirectiveAPIPropertyName) || + (TSESLintASTUtils.isIdentifier(ancestorMaybeHostDirectiveAPI.key) && + ancestorMaybeHostDirectiveAPI.key.name === + hostDirectiveAPIPropertyName) + ) { + return; + } + } + const [propertyName, aliasName] = withoutBracketsAndWhitespaces( ASTUtils.getRawText(node), ).split(':'); diff --git a/packages/eslint-plugin/src/rules/no-outputs-metadata-property.ts b/packages/eslint-plugin/src/rules/no-outputs-metadata-property.ts index 881ccd5be..98f7f15c1 100644 --- a/packages/eslint-plugin/src/rules/no-outputs-metadata-property.ts +++ b/packages/eslint-plugin/src/rules/no-outputs-metadata-property.ts @@ -1,5 +1,6 @@ -import { Selectors } from '@angular-eslint/utils'; +import { ASTUtils, Selectors } from '@angular-eslint/utils'; import type { TSESTree } from '@typescript-eslint/utils'; +import { ASTUtils as TSESLintASTUtils } from '@typescript-eslint/utils'; import { createESLintRule } from '../utils/create-eslint-rule'; type Options = []; @@ -29,6 +30,30 @@ export default createESLintRule({ } ${Selectors.metadataProperty(METADATA_PROPERTY_NAME)}`]( node: TSESTree.Property, ) { + /** + * Angular v15 introduced the directive composition API: https://angular.io/guide/directive-composition-api + * Using host directive outputs using this API is not a bad practice and should not be reported + */ + const ancestorMayBeHostDirectiveAPI = node.parent?.parent?.parent; + + if ( + ancestorMayBeHostDirectiveAPI && + ASTUtils.isProperty(ancestorMayBeHostDirectiveAPI) + ) { + const hostDirectiveAPIPropertyName = 'hostDirectives'; + + if ( + (ASTUtils.isLiteral(ancestorMayBeHostDirectiveAPI.key) && + ancestorMayBeHostDirectiveAPI.key.value === + hostDirectiveAPIPropertyName) || + (TSESLintASTUtils.isIdentifier(ancestorMayBeHostDirectiveAPI.key) && + ancestorMayBeHostDirectiveAPI.key.name === + hostDirectiveAPIPropertyName) + ) { + return; + } + } + context.report({ node, messageId: 'noOutputsMetadataProperty', diff --git a/packages/eslint-plugin/tests/rules/no-output-rename/cases.ts b/packages/eslint-plugin/tests/rules/no-output-rename/cases.ts index fc9539e3f..692e0a166 100644 --- a/packages/eslint-plugin/tests/rules/no-output-rename/cases.ts +++ b/packages/eslint-plugin/tests/rules/no-output-rename/cases.ts @@ -108,6 +108,40 @@ export const valid = [ @Output('foo') label: string; } `, + /** + * Renaming outputs when using the directive composition API is not a bad practice + * https://angular.io/guide/directive-composition-api + */ + ` + @Component({ + selector: 'foo', + hostDirectives: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] + }) + class Test {} + `, + ` + @Component({ + selector: 'foo', + 'hostDirectives': [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] + }) + class Test {} + `, + ` + @Component({ + selector: 'foo', + ['hostDirectives']: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] + }) + class Test {} + `, ` @Directive({ selector: 'foo' diff --git a/packages/eslint-plugin/tests/rules/no-outputs-metadata-property/cases.ts b/packages/eslint-plugin/tests/rules/no-outputs-metadata-property/cases.ts index daa6aed39..e09dd4ee7 100644 --- a/packages/eslint-plugin/tests/rules/no-outputs-metadata-property/cases.ts +++ b/packages/eslint-plugin/tests/rules/no-outputs-metadata-property/cases.ts @@ -45,6 +45,40 @@ export const valid = [ }) class Test {} `, + /** + * Renaming outputs when using the directive composition API is not a bad practice + * https://angular.io/guide/directive-composition-api + */ + ` + @Component({ + selector: 'foo', + hostDirectives: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] + }) + class Test {} + `, + ` + @Component({ + selector: 'foo', + 'hostDirectives': [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] + }) + class Test {} + `, + ` + @Component({ + selector: 'foo', + ['hostDirectives']: [{ + directive: CdkMenuItem, + outputs: ['cdkMenuItemTriggered: triggered'], + }] + }) + class Test {} + `, ]; export const invalid = [