From 0e7fb73427f3831efe336a3f63c90c9b3c150871 Mon Sep 17 00:00:00 2001 From: Dmitry Zakharov Date: Wed, 28 Sep 2022 16:53:13 +0300 Subject: [PATCH] feat(eslint-plugin-template): [no-inline-styles] add rule --- .../docs/rules/no-inline-styles.md | 636 ++++++++++++++++++ .../src/configs/all.json | 1 + packages/eslint-plugin-template/src/index.ts | 4 + .../src/rules/no-inline-styles.ts | 87 +++ .../tests/rules/no-inline-styles/cases.ts | 87 +++ .../tests/rules/no-inline-styles/spec.ts | 12 + 6 files changed, 827 insertions(+) create mode 100644 packages/eslint-plugin-template/docs/rules/no-inline-styles.md create mode 100644 packages/eslint-plugin-template/src/rules/no-inline-styles.ts create mode 100644 packages/eslint-plugin-template/tests/rules/no-inline-styles/cases.ts create mode 100644 packages/eslint-plugin-template/tests/rules/no-inline-styles/spec.ts diff --git a/packages/eslint-plugin-template/docs/rules/no-inline-styles.md b/packages/eslint-plugin-template/docs/rules/no-inline-styles.md new file mode 100644 index 0000000000..c81759f058 --- /dev/null +++ b/packages/eslint-plugin-template/docs/rules/no-inline-styles.md @@ -0,0 +1,636 @@ + + +
+ +# `@angular-eslint/template/no-inline-styles` + +Using html inline styles using style attribute is prohibited, use classes instead. + +- Type: suggestion + +
+ +## Rule Options + +The rule accepts an options object with the following properties: + +```ts +interface Options { + allowNgStyle?: boolean; +} + +``` + +
+ +## 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/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + +
+ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +
+
+``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +
+ +--- + +
+ +
+✅ - Toggle examples of correct code for this rule + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +Foo eating a sandwich. +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +Meaningful description +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +This is descriptive! +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Custom Config + +```json +{ + "rules": { + "@angular-eslint/template/no-inline-styles": [ + "error", + { + "allowNgStyle": true + } + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + + + +
diff --git a/packages/eslint-plugin-template/src/configs/all.json b/packages/eslint-plugin-template/src/configs/all.json index 83ce809c79..1968d518d0 100644 --- a/packages/eslint-plugin-template/src/configs/all.json +++ b/packages/eslint-plugin-template/src/configs/all.json @@ -23,6 +23,7 @@ "@angular-eslint/template/no-call-expression": "error", "@angular-eslint/template/no-distracting-elements": "error", "@angular-eslint/template/no-duplicate-attributes": "error", + "@angular-eslint/template/no-inline-styles": "error", "@angular-eslint/template/no-negated-async": "error", "@angular-eslint/template/no-positive-tabindex": "error", "@angular-eslint/template/use-track-by-function": "error" diff --git a/packages/eslint-plugin-template/src/index.ts b/packages/eslint-plugin-template/src/index.ts index ef923dd1ed..e3092e7d43 100644 --- a/packages/eslint-plugin-template/src/index.ts +++ b/packages/eslint-plugin-template/src/index.ts @@ -63,6 +63,9 @@ import noDistractingElements, { import noDuplicateAttributes, { RULE_NAME as noDuplicateAttributesRuleName, } from './rules/no-duplicate-attributes'; +import noInlineStyles, { + RULE_NAME as noInlineStylesRuleName, +} from './rules/no-inline-styles'; import noNegatedAsync, { RULE_NAME as noNegatedAsyncRuleName, } from './rules/no-negated-async'; @@ -107,6 +110,7 @@ export default { [noCallExpressionRuleName]: noCallExpression, [noDistractingElementsRuleName]: noDistractingElements, [noDuplicateAttributesRuleName]: noDuplicateAttributes, + [noInlineStylesRuleName]: noInlineStyles, [noNegatedAsyncRuleName]: noNegatedAsync, [noPositiveTabindexRuleName]: noPositiveTabindex, [useTrackByFunctionRuleName]: useTrackByFunction, diff --git a/packages/eslint-plugin-template/src/rules/no-inline-styles.ts b/packages/eslint-plugin-template/src/rules/no-inline-styles.ts new file mode 100644 index 0000000000..37407009b2 --- /dev/null +++ b/packages/eslint-plugin-template/src/rules/no-inline-styles.ts @@ -0,0 +1,87 @@ +import type { TmplAstElement } from '@angular-eslint/bundled-angular-compiler'; +import { + createESLintRule, + getTemplateParserServices, +} from '../utils/create-eslint-rule'; + +type Options = [ + { + readonly allowNgStyle?: boolean; + }, +]; +const DEFAULT_OPTIONS: Options[number] = { allowNgStyle: false }; +export type MessageIds = 'noInlineStyles'; +export const RULE_NAME = 'no-inline-styles'; + +export default createESLintRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Using html inline styles using style attribute is prohibited, use classes instead.', + recommended: false, + }, + schema: [ + { + type: 'object', + properties: { + allowNgStyle: { + type: 'boolean', + default: DEFAULT_OPTIONS.allowNgStyle, + }, + }, + additionalProperties: false, + }, + ], + messages: { + noInlineStyles: + '<{{element}}/> element should not have inline styles via style attribute. Please use classes instead.', + }, + }, + defaultOptions: [DEFAULT_OPTIONS], + create(context, [{ allowNgStyle }]) { + const parserServices = getTemplateParserServices(context); + + return { + Element$1(node: TmplAstElement) { + const isInvalid = isNodeHasStyleAttribute(node, { allowNgStyle }); + + if (isInvalid) { + const loc = parserServices.convertElementSourceSpanToLoc( + context, + node, + ); + + context.report({ + loc, + messageId: 'noInlineStyles', + data: { + element: node.name, + }, + }); + } + }, + }; + }, +}); + +/** + * Check that the any element for example `` has a `style` attribute or `attr.style` binding. + */ +function isNodeHasStyleAttribute(node: TmplAstElement, options: any): boolean { + return options.allowNgStyle + ? node.attributes.some(({ name }) => isStyle(name)) || + node.inputs.some(({ name }) => isStyle(name)) + : node.attributes.some(({ name }) => isStyle(name)) || + node.inputs.some(({ name }) => isStyle(name)) || + node.inputs.some(({ name }) => isNgStyle(name)); +} + +function isStyle(name: string): name is 'style' { + return name === 'style'; +} + +function isNgStyle(name: string): name is 'ngStyle' { + return name === 'ngStyle'; +} diff --git a/packages/eslint-plugin-template/tests/rules/no-inline-styles/cases.ts b/packages/eslint-plugin-template/tests/rules/no-inline-styles/cases.ts new file mode 100644 index 0000000000..7a139bc8ca --- /dev/null +++ b/packages/eslint-plugin-template/tests/rules/no-inline-styles/cases.ts @@ -0,0 +1,87 @@ +import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/utils'; +import type { MessageIds } from '../../../src/rules/no-inline-styles'; + +const messageId: MessageIds = 'noInlineStyles'; + +export const valid = [ + 'Foo eating a sandwich.', + '', + ``, + '', + '', + '', + 'Meaningful description', + '', + '', + '', + 'This is descriptive!', + '', + '', + '', + '', + { + code: ``, + options: [{ allowNgStyle: true }], + }, +]; + +export const invalid = [ + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail element when style attribute exist', + annotatedSource: ` + +
+ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +
+
+ `, + data: { element: 'img' }, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail when object style attribute exist', + annotatedSource: ` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + data: { element: 'object' }, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail when area style attribute exist', + annotatedSource: ` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + data: { element: 'area' }, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail when input element with style attribute exist', + annotatedSource: ` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + data: { element: 'input' }, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail when input element with style attribute exist', + annotatedSource: ` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + data: { element: 'input' }, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail when input element with ngStyle attribute exist', + annotatedSource: ` + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + data: { element: 'input' }, + }), +]; diff --git a/packages/eslint-plugin-template/tests/rules/no-inline-styles/spec.ts b/packages/eslint-plugin-template/tests/rules/no-inline-styles/spec.ts new file mode 100644 index 0000000000..84745738b8 --- /dev/null +++ b/packages/eslint-plugin-template/tests/rules/no-inline-styles/spec.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '@angular-eslint/utils'; +import rule, { RULE_NAME } from '../../../src/rules/no-inline-styles'; +import { invalid, valid } from './cases'; + +const ruleTester = new RuleTester({ + parser: '@angular-eslint/template-parser', +}); + +ruleTester.run(RULE_NAME, rule, { + valid, + invalid, +});