From e0134775968f1b705fe212e68b5cd87d3e2148a5 Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Fri, 15 Mar 2024 22:20:41 +0100 Subject: [PATCH] feat(eslint-plugin): add prefer-standalone rule for checking all directives (#1627) --- packages/builder/src/lint.impl.spec.ts | 6 +- packages/eslint-plugin/README.md | 10 +- .../docs/rules/prefer-standalone-component.md | 6 + .../docs/rules/prefer-standalone.md | 784 ++++++++++++++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/index.ts | 4 + .../src/rules/prefer-standalone-component.ts | 2 + .../src/rules/prefer-standalone.ts | 89 ++ .../tests/rules/prefer-standalone/cases.ts | 410 +++++++++ .../tests/rules/prefer-standalone/spec.ts | 12 + 10 files changed, 1320 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/prefer-standalone.md create mode 100644 packages/eslint-plugin/src/rules/prefer-standalone.ts create mode 100644 packages/eslint-plugin/tests/rules/prefer-standalone/cases.ts create mode 100644 packages/eslint-plugin/tests/rules/prefer-standalone/spec.ts diff --git a/packages/builder/src/lint.impl.spec.ts b/packages/builder/src/lint.impl.spec.ts index 787db23da..4b3072fd6 100644 --- a/packages/builder/src/lint.impl.spec.ts +++ b/packages/builder/src/lint.impl.spec.ts @@ -4,7 +4,7 @@ import { json, logging, schema } from '@angular-devkit/core'; import type { ESLint } from 'eslint'; import { mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, sep } from 'node:path'; import type { Schema } from './schema'; // Add a placeholder file for the native workspace context within nx implementation details otherwise it will hang in CI @@ -178,7 +178,7 @@ describe('Linter Builder', () => { fix: true, quiet: false, cache: true, - cacheLocation: 'cacheLocation1/', + cacheLocation: `cacheLocation1${sep}`, cacheStrategy: 'content', format: 'stylish', force: false, @@ -214,7 +214,7 @@ describe('Linter Builder', () => { fix: true, quiet: false, cache: true, - cacheLocation: 'cacheLocation1/', + cacheLocation: `cacheLocation1${sep}`, cacheStrategy: 'content', format: 'stylish', force: false, diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 16a88d0f8..4b09601d7 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -64,7 +64,7 @@ | [`pipe-prefix`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/pipe-prefix.md) | Enforce consistent prefix for pipes. | | | | | [`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: | | [`prefer-output-readonly`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-output-readonly.md) | Prefer to declare `@Output` as `readonly` since they are not supposed to be reassigned | | | :bulb: | -| [`prefer-standalone-component`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-standalone-component.md) | Ensures component `standalone` property is set to `true` in the component decorator | | :wrench: | | +| [`prefer-standalone`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-standalone.md) | Ensures component, directive and pipe `standalone` property is set to `true` in the component decorator | | :wrench: | | | [`relative-url-prefix`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/relative-url-prefix.md) | The ./ and ../ prefix is standard syntax for relative URLs; don't depend on Angular's current ability to do without that prefix. See more at https://angular.io/styleguide#style-05-04 | | | | | [`require-localize-metadata`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/require-localize-metadata.md) | Ensures that $localize tagged messages contain helpful metadata to aid with translations. | | | | | [`sort-ngmodule-metadata-arrays`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/sort-ngmodule-metadata-arrays.md) | Ensures ASC alphabetical order for `NgModule` metadata arrays for easy visual scanning | | :wrench: | | @@ -83,4 +83,12 @@ +### Deprecated + + +| Rule | Replaced by | +| --- | --- | +| [`prefer-standalone-component`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-standalone-component.md) | [`prefer-standalone`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-standalone-component.md) | + + diff --git a/packages/eslint-plugin/docs/rules/prefer-standalone-component.md b/packages/eslint-plugin/docs/rules/prefer-standalone-component.md index 68abf7ac8..0032143a8 100644 --- a/packages/eslint-plugin/docs/rules/prefer-standalone-component.md +++ b/packages/eslint-plugin/docs/rules/prefer-standalone-component.md @@ -15,6 +15,12 @@ # `@angular-eslint/prefer-standalone-component` +## ⚠️ THIS RULE IS DEPRECATED + +Please use `@angular-eslint/prefer-standalone` instead. + +--- + Ensures component `standalone` property is set to `true` in the component decorator - Type: suggestion diff --git a/packages/eslint-plugin/docs/rules/prefer-standalone.md b/packages/eslint-plugin/docs/rules/prefer-standalone.md new file mode 100644 index 000000000..c5f8e8346 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-standalone.md @@ -0,0 +1,784 @@ + + +
+ +# `@angular-eslint/prefer-standalone` + +Ensures component, directive and pipe `standalone` property is set to `true` in the component decorator + +- Type: suggestion +- 🔧 Supports autofix (`--fix`) + +
+ +## Rule Options + +The rule does not have any configuration options. + +
+ +## Usage Examples + +> 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. + +
+ +
+❌ - Toggle examples of incorrect code for this rule + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Component({}) +~~~~~~~~~~~~~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Component({ standalone: false }) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Component({ +~~~~~~~~~~~~ +standalone: false, +~~~~~~~~~~~~~~~~~~ +template: '
' +~~~~~~~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Component({ +~~~~~~~~~~~~ +template: '
' +~~~~~~~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Component({ +~~~~~~~~~~~~ +selector: 'my-selector', +~~~~~~~~~~~~~~~~~~~~~~~ +template: '
' +~~~~~~~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Directive({}) +~~~~~~~~~~~~~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Directive({ standalone: false }) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Directive({ +~~~~~~~~~~~~ + standalone: false, + ~~~~~~~~~~~~~~~~~~ + selector: 'x-selector' + ~~~~~~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Directive({ +~~~~~~~~~~~~ + selector: 'test-selector' + ~~~~~~~~~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Directive({ +~~~~~~~~~~~~ + selector: 'my-selector', + ~~~~~~~~~~~~~~~~~~~~~~~~ + providers: [] + ~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Pipe({}) +~~~~~~~~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Pipe({ standalone: false }) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Pipe({ +~~~~~~~ + standalone: false, + ~~~~~~~~~~~~~~~~~~ + name: 'pipe-name' + ~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Pipe({ +~~~~~~~ + name: 'test-name' + ~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```ts +@Pipe({ +~~~~~~~ + selector: 'my-selector', + ~~~~~~~~~~~~~~~~~~~~~~~~ + name: 'test-name' + ~~~~~~~~~~~~~~~~~ +}) +~~ +class Test {} +``` + +
+ +
+ +--- + +
+ +
+✅ - Toggle examples of correct code for this rule + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + standalone: true, +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + standalone: true, + selector: 'test-selector' +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Component({ + selector: 'test-selector', + standalone: true, + template: '
', + styleUrls: ['./test.css'] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Directive({ + standalone: true, +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Directive({ + standalone: true, + selector: 'test-selector' +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Directive({ + selector: 'test-selector', + standalone: true, + providers: [] +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Pipe({ + standalone: true, +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Pipe({ + standalone: true, + name: 'test-pipe' +}) +class Test {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/prefer-standalone": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```ts +@Pipe({ + name: 'my-pipe', + standalone: true, + pure: true +}) +class Test {} +``` + +
+ +
diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 5f0fdc680..c3354701b 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -29,6 +29,7 @@ "@angular-eslint/pipe-prefix": "error", "@angular-eslint/prefer-on-push-component-change-detection": "error", "@angular-eslint/prefer-output-readonly": "error", + "@angular-eslint/prefer-standalone": "error", "@angular-eslint/prefer-standalone-component": "error", "@angular-eslint/relative-url-prefix": "error", "@angular-eslint/require-localize-metadata": "error", diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts index ce72f8e0f..683899ef8 100644 --- a/packages/eslint-plugin/src/index.ts +++ b/packages/eslint-plugin/src/index.ts @@ -82,6 +82,9 @@ import preferOnPushComponentChangeDetection, { import preferOutputReadonly, { RULE_NAME as preferOutputReadonlyRuleName, } from './rules/prefer-output-readonly'; +import preferStandalone, { + RULE_NAME as preferStandaloneRuleName, +} from './rules/prefer-standalone'; import preferStandaloneComponent, { RULE_NAME as preferStandaloneComponentRuleName, } from './rules/prefer-standalone-component'; @@ -146,6 +149,7 @@ export = { [pipePrefixRuleName]: pipePrefix, [preferOnPushComponentChangeDetectionRuleName]: preferOnPushComponentChangeDetection, + [preferStandaloneRuleName]: preferStandalone, [preferStandaloneComponentRuleName]: preferStandaloneComponent, [preferOutputReadonlyRuleName]: preferOutputReadonly, [relativeUrlPrefixRuleName]: relativeUrlPrefix, diff --git a/packages/eslint-plugin/src/rules/prefer-standalone-component.ts b/packages/eslint-plugin/src/rules/prefer-standalone-component.ts index a6eb73822..b3867c05c 100644 --- a/packages/eslint-plugin/src/rules/prefer-standalone-component.ts +++ b/packages/eslint-plugin/src/rules/prefer-standalone-component.ts @@ -20,6 +20,8 @@ export default createESLintRule({ docs: { description: `Ensures component \`${METADATA_PROPERTY_NAME}\` property is set to \`${IS_STANDALONE}\` in the component decorator`, }, + deprecated: true, + replacedBy: ['prefer-standalone'], fixable: 'code', schema: [], messages: { diff --git a/packages/eslint-plugin/src/rules/prefer-standalone.ts b/packages/eslint-plugin/src/rules/prefer-standalone.ts new file mode 100644 index 000000000..795ecf3c0 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-standalone.ts @@ -0,0 +1,89 @@ +import { + ASTUtils, + RuleFixes, + isNotNullOrUndefined, + Selectors, +} from '@angular-eslint/utils'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { createESLintRule } from '../utils/create-eslint-rule'; + +type Options = []; +type DecoratorTypes = 'component' | 'directive' | 'pipe'; +export type MessageIds = 'preferStandalone'; +export const RULE_NAME = 'prefer-standalone'; +const METADATA_PROPERTY_NAME = 'standalone'; +const IS_STANDALONE = 'true'; + +export default createESLintRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: `Ensures component, directive and pipe \`${METADATA_PROPERTY_NAME}\` property is set to \`${IS_STANDALONE}\` in the component decorator`, + }, + fixable: 'code', + schema: [], + messages: { + preferStandalone: `The {{type}} \`${METADATA_PROPERTY_NAME}\` property should be set to \`${IS_STANDALONE}\``, + }, + }, + defaultOptions: [], + create(context) { + const standaloneRuleFactory = + (type: DecoratorTypes) => (node: TSESTree.Decorator) => { + const standalone = ASTUtils.getDecoratorPropertyValue( + node, + METADATA_PROPERTY_NAME, + ); + + if ( + standalone && + ASTUtils.isLiteral(standalone) && + standalone.value === true + ) { + return; + } + + context.report({ + node: nodeToReport(node), + messageId: 'preferStandalone', + data: { type }, + fix: (fixer) => { + if ( + standalone && + ASTUtils.isLiteral(standalone) && + standalone.value !== true + ) { + return [fixer.replaceText(standalone, IS_STANDALONE)].filter( + isNotNullOrUndefined, + ); + } + + return [ + RuleFixes.getDecoratorPropertyAddFix( + node, + fixer, + `${METADATA_PROPERTY_NAME}: ${IS_STANDALONE}`, + ), + ].filter(isNotNullOrUndefined); + }, + }); + }; + + return { + [Selectors.COMPONENT_CLASS_DECORATOR]: standaloneRuleFactory('component'), + [Selectors.DIRECTIVE_CLASS_DECORATOR]: standaloneRuleFactory('directive'), + [Selectors.PIPE_CLASS_DECORATOR]: standaloneRuleFactory('pipe'), + }; + }, +}); + +function nodeToReport(node: TSESTree.Node) { + if (!ASTUtils.isProperty(node)) { + return node; + } + + return ASTUtils.isMemberExpression(node.value) + ? node.value.property + : node.value; +} diff --git a/packages/eslint-plugin/tests/rules/prefer-standalone/cases.ts b/packages/eslint-plugin/tests/rules/prefer-standalone/cases.ts new file mode 100644 index 000000000..199d273a5 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-standalone/cases.ts @@ -0,0 +1,410 @@ +import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/utils'; +import type { MessageIds } from '../../../src/rules/prefer-standalone'; + +const messageId: MessageIds = 'preferStandalone'; + +export const valid = [ + ` + @Component({ + standalone: true, + }) + class Test {} + `, + ` + @Component({ + standalone: true, + selector: 'test-selector' + }) + class Test {} + `, + ` + @Component({ + selector: 'test-selector', + standalone: true, + template: '
', + styleUrls: ['./test.css'] + }) + class Test {} + `, + ` + @Directive({ + standalone: true, + }) + class Test {} + `, + ` + @Directive({ + standalone: true, + selector: 'test-selector' + }) + class Test {} + `, + ` + @Directive({ + selector: 'test-selector', + standalone: true, + providers: [] + }) + class Test {} + `, + ` + @Pipe({ + standalone: true, + }) + class Test {} + `, + ` + @Pipe({ + standalone: true, + name: 'test-pipe' + }) + class Test {} + `, + ` + @Pipe({ + name: 'my-pipe', + standalone: true, + pure: true + }) + class Test {} + `, +]; + +export const invalid = [ + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a component does not have the standalone property in the decorator', + annotatedSource: ` + @Component({}) + ~~~~~~~~~~~~~~ + class Test {} + `, + messageId, + data: { type: 'component' }, + annotatedOutput: ` + @Component({standalone: true}) + + class Test {} + `, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a component has the standalone property set to false in the decorator', + annotatedSource: ` + @Component({ standalone: false }) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + class Test {} + `, + messageId, + data: { type: 'component' }, + annotatedOutput: ` + @Component({ standalone: true }) + + class Test {} + `, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a component has the standalone property set to false in a decorator with multiple properties', + annotatedSource: ` + @Component({ + ~~~~~~~~~~~~ + standalone: false, + ~~~~~~~~~~~~~~~~~~ + template: '
' + ~~~~~~~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'component' }, + annotatedOutput: ` + @Component({ + + standalone: true, + + template: '
' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a component has no standalone property in a decorator with one property', + annotatedSource: ` + @Component({ + ~~~~~~~~~~~~ + template: '
' + ~~~~~~~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'component' }, + annotatedOutput: ` + @Component({ + + standalone: true,template: '
' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a component has no standalone property in a decorator with multiple properties', + annotatedSource: ` + @Component({ + ~~~~~~~~~~~~ + selector: 'my-selector', + ~~~~~~~~~~~~~~~~~~~~~~~ + template: '
' + ~~~~~~~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'component' }, + annotatedOutput: ` + @Component({ + + standalone: true,selector: 'my-selector', + + template: '
' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a directive does not have the standalone property in the decorator', + annotatedSource: ` + @Directive({}) + ~~~~~~~~~~~~~~ + class Test {} + `, + messageId, + data: { type: 'directive' }, + annotatedOutput: ` + @Directive({standalone: true}) + + class Test {} + `, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a directive has the standalone property set to false in the decorator', + annotatedSource: ` + @Directive({ standalone: false }) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + class Test {} + `, + messageId, + data: { type: 'directive' }, + annotatedOutput: ` + @Directive({ standalone: true }) + + class Test {} + `, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a directive has the standalone property set to false in a decorator with multiple properties', + annotatedSource: ` + @Directive({ + ~~~~~~~~~~~~ + standalone: false, + ~~~~~~~~~~~~~~~~~~ + selector: 'x-selector' + ~~~~~~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'directive' }, + annotatedOutput: ` + @Directive({ + + standalone: true, + + selector: 'x-selector' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a directive has no standalone property in a decorator with one property', + annotatedSource: ` + @Directive({ + ~~~~~~~~~~~~ + selector: 'test-selector' + ~~~~~~~~~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'directive' }, + annotatedOutput: ` + @Directive({ + + standalone: true,selector: 'test-selector' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a directive has no standalone property in a decorator with multiple properties', + annotatedSource: ` + @Directive({ + ~~~~~~~~~~~~ + selector: 'my-selector', + ~~~~~~~~~~~~~~~~~~~~~~~~ + providers: [] + ~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'directive' }, + annotatedOutput: ` + @Directive({ + + standalone: true,selector: 'my-selector', + + providers: [] + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a pipe does not have the standalone property in the decorator', + annotatedSource: ` + @Pipe({}) + ~~~~~~~~~ + class Test {} + `, + messageId, + data: { type: 'pipe' }, + annotatedOutput: ` + @Pipe({standalone: true}) + + class Test {} + `, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a pipe has the standalone property set to false in the decorator', + annotatedSource: ` + @Pipe({ standalone: false }) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + class Test {} + `, + messageId, + data: { type: 'pipe' }, + annotatedOutput: ` + @Pipe({ standalone: true }) + + class Test {} + `, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a pipe has the standalone property set to false in a decorator with multiple properties', + annotatedSource: ` + @Pipe({ + ~~~~~~~ + standalone: false, + ~~~~~~~~~~~~~~~~~~ + name: 'pipe-name' + ~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'pipe' }, + annotatedOutput: ` + @Pipe({ + + standalone: true, + + name: 'pipe-name' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a pipe has no standalone property in a decorator with one property', + annotatedSource: ` + @Pipe({ + ~~~~~~~ + name: 'test-name' + ~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'pipe' }, + annotatedOutput: ` + @Pipe({ + + standalone: true,name: 'test-name' + + }) + + class Test {} +`, + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail when a pipe has no standalone property in a decorator with multiple properties', + annotatedSource: ` + @Pipe({ + ~~~~~~~ + selector: 'my-selector', + ~~~~~~~~~~~~~~~~~~~~~~~~ + name: 'test-name' + ~~~~~~~~~~~~~~~~~ + }) + ~~ + class Test {} +`, + messageId, + data: { type: 'pipe' }, + annotatedOutput: ` + @Pipe({ + + standalone: true,selector: 'my-selector', + + name: 'test-name' + + }) + + class Test {} +`, + }), +]; diff --git a/packages/eslint-plugin/tests/rules/prefer-standalone/spec.ts b/packages/eslint-plugin/tests/rules/prefer-standalone/spec.ts new file mode 100644 index 000000000..7e784c534 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-standalone/spec.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '@angular-eslint/utils'; +import rule, { RULE_NAME } from '../../../src/rules/prefer-standalone'; +import { invalid, valid } from './cases'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run(RULE_NAME, rule, { + valid, + invalid, +});