diff --git a/.changeset/happy-monkeys-grin.md b/.changeset/happy-monkeys-grin.md new file mode 100644 index 000000000..883c87397 --- /dev/null +++ b/.changeset/happy-monkeys-grin.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +feat: added the no-inline-styles rule diff --git a/README.md b/README.md index 7df272aa1..151fad7a0 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: | | [svelte/no-ignored-unsubscribe](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-ignored-unsubscribe/) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | | | [svelte/no-immutable-reactive-statements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-immutable-reactive-statements/) | disallow reactive statements that don't reference reactive values. | | +| [svelte/no-inline-styles](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inline-styles/) | disallow attributes and directives that produce inline styles | | | [svelte/no-reactive-functions](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-functions/) | it's not necessary to define functions in reactive statements | :bulb: | | [svelte/no-reactive-literals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | don't assign literal values in reactive statements | :bulb: | | [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | | diff --git a/docs/rules.md b/docs/rules.md index 1f982942b..257cb4f93 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -55,6 +55,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: | | [svelte/no-ignored-unsubscribe](./rules/no-ignored-unsubscribe.md) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. | | | [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md) | disallow reactive statements that don't reference reactive values. | | +| [svelte/no-inline-styles](./rules/no-inline-styles.md) | disallow attributes and directives that produce inline styles | | | [svelte/no-reactive-functions](./rules/no-reactive-functions.md) | it's not necessary to define functions in reactive statements | :bulb: | | [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | don't assign literal values in reactive statements | :bulb: | | [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | | diff --git a/docs/rules/no-inline-styles.md b/docs/rules/no-inline-styles.md new file mode 100644 index 000000000..8d20e1ab5 --- /dev/null +++ b/docs/rules/no-inline-styles.md @@ -0,0 +1,69 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/no-inline-styles' +description: 'disallow attributes and directives that produce inline styles' +--- + +# svelte/no-inline-styles + +> disallow attributes and directives that produce inline styles + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule reports all attributes and directives that would compile to inline styles. This is mainly useful when adding Content Security Policy to your app, as having inline styles requires the `style-src: 'unsafe-inline'` directive, which is generally discouraged and unsafe. + + + + + +```svelte + + + +Hello World! + +Hello World! + + +Hello World! + +Hello World! + +Hello World! +``` + + + +## :wrench: Options + +```json +{ + "svelte/no-inline-styles": [ + "error", + { + "allowTransitions": false + } + ] +} +``` + +- `allowTransitions` ... Most svelte transitions (including the built-in ones) use inline styles. However, it is theoretically possible to only use transitions that don't (see this [issue](https://github.com/sveltejs/svelte/issues/6662) about removing inline styles from built-in transitions). This option allows transitions to be used in such cases. Default `false`. + +## :books: Further Reading + +- [CSP documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) + +## :mag: Implementation + +- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-inline-styles.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/no-inline-styles.ts) diff --git a/src/rules/no-inline-styles.ts b/src/rules/no-inline-styles.ts new file mode 100644 index 000000000..31350d84c --- /dev/null +++ b/src/rules/no-inline-styles.ts @@ -0,0 +1,53 @@ +import { createRule } from '../utils'; + +export default createRule('no-inline-styles', { + meta: { + docs: { + description: 'disallow attributes and directives that produce inline styles', + category: 'Best Practices', + recommended: false + }, + schema: [ + { + type: 'object', + properties: { + allowTransitions: { + type: 'boolean' + } + }, + additionalProperties: false + } + ], + messages: { + hasStyleAttribute: 'Found disallowed style attribute.', + hasStyleDirective: 'Found disallowed style directive.', + hasTransition: 'Found disallowed transition.' + }, + type: 'suggestion' + }, + create(context) { + const allowTransitions: boolean = context.options[0]?.allowTransitions ?? false; + return { + SvelteElement(node) { + if (node.kind !== 'html') { + return; + } + for (const attribute of node.startTag.attributes) { + if (attribute.type === 'SvelteStyleDirective') { + context.report({ loc: attribute.loc, messageId: 'hasStyleDirective' }); + } + if (attribute.type === 'SvelteAttribute' && attribute.key.name === 'style') { + context.report({ loc: attribute.loc, messageId: 'hasStyleAttribute' }); + } + if ( + !allowTransitions && + attribute.type === 'SvelteDirective' && + attribute.kind === 'Transition' + ) { + context.report({ loc: attribute.loc, messageId: 'hasTransition' }); + } + } + } + }; + } +}); diff --git a/src/utils/rules.ts b/src/utils/rules.ts index c496dec7b..7a7713e45 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -29,6 +29,7 @@ import noExportLoadInSvelteModuleInKitPages from '../rules/no-export-load-in-sve import noExtraReactiveCurlies from '../rules/no-extra-reactive-curlies'; import noIgnoredUnsubscribe from '../rules/no-ignored-unsubscribe'; import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statements'; +import noInlineStyles from '../rules/no-inline-styles'; import noInnerDeclarations from '../rules/no-inner-declarations'; import noNotFunctionHandler from '../rules/no-not-function-handler'; import noObjectInTextMustaches from '../rules/no-object-in-text-mustaches'; @@ -91,6 +92,7 @@ export const rules = [ noExtraReactiveCurlies, noIgnoredUnsubscribe, noImmutableReactiveStatements, + noInlineStyles, noInnerDeclarations, noNotFunctionHandler, noObjectInTextMustaches, diff --git a/tests/fixtures/rules/no-inline-styles/invalid/style-attribute01-errors.yaml b/tests/fixtures/rules/no-inline-styles/invalid/style-attribute01-errors.yaml new file mode 100644 index 000000000..900dc6000 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/invalid/style-attribute01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found disallowed style attribute. + line: 1 + column: 7 + suggestions: null diff --git a/tests/fixtures/rules/no-inline-styles/invalid/style-attribute01-input.svelte b/tests/fixtures/rules/no-inline-styles/invalid/style-attribute01-input.svelte new file mode 100644 index 000000000..6849356dc --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/invalid/style-attribute01-input.svelte @@ -0,0 +1 @@ +Hello World! diff --git a/tests/fixtures/rules/no-inline-styles/invalid/style-directive01-errors.yaml b/tests/fixtures/rules/no-inline-styles/invalid/style-directive01-errors.yaml new file mode 100644 index 000000000..f82800cb8 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/invalid/style-directive01-errors.yaml @@ -0,0 +1,4 @@ +- message: Found disallowed style directive. + line: 5 + column: 7 + suggestions: null diff --git a/tests/fixtures/rules/no-inline-styles/invalid/style-directive01-input.svelte b/tests/fixtures/rules/no-inline-styles/invalid/style-directive01-input.svelte new file mode 100644 index 000000000..43659e321 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/invalid/style-directive01-input.svelte @@ -0,0 +1,5 @@ + + +Hello World! diff --git a/tests/fixtures/rules/no-inline-styles/invalid/transition01-errors.yaml b/tests/fixtures/rules/no-inline-styles/invalid/transition01-errors.yaml new file mode 100644 index 000000000..974575921 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/invalid/transition01-errors.yaml @@ -0,0 +1,8 @@ +- message: Found disallowed transition. + line: 5 + column: 7 + suggestions: null +- message: Found disallowed transition. + line: 7 + column: 7 + suggestions: null diff --git a/tests/fixtures/rules/no-inline-styles/invalid/transition01-input.svelte b/tests/fixtures/rules/no-inline-styles/invalid/transition01-input.svelte new file mode 100644 index 000000000..1ca7f5331 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/invalid/transition01-input.svelte @@ -0,0 +1,7 @@ + + +Hello World! + +Hello World! diff --git a/tests/fixtures/rules/no-inline-styles/valid/allowed-transitions/_config.json b/tests/fixtures/rules/no-inline-styles/valid/allowed-transitions/_config.json new file mode 100644 index 000000000..7fc09d166 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/valid/allowed-transitions/_config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "allowTransitions": true + } + ] +} diff --git a/tests/fixtures/rules/no-inline-styles/valid/allowed-transitions/transition01-input.svelte b/tests/fixtures/rules/no-inline-styles/valid/allowed-transitions/transition01-input.svelte new file mode 100644 index 000000000..1ca7f5331 --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/valid/allowed-transitions/transition01-input.svelte @@ -0,0 +1,7 @@ + + +Hello World! + +Hello World! diff --git a/tests/fixtures/rules/no-inline-styles/valid/class-attribute01-input.svelte b/tests/fixtures/rules/no-inline-styles/valid/class-attribute01-input.svelte new file mode 100644 index 000000000..db36f53fc --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/valid/class-attribute01-input.svelte @@ -0,0 +1 @@ +Hello World! diff --git a/tests/fixtures/rules/no-inline-styles/valid/class-directive01-input.svelte b/tests/fixtures/rules/no-inline-styles/valid/class-directive01-input.svelte new file mode 100644 index 000000000..e8614058c --- /dev/null +++ b/tests/fixtures/rules/no-inline-styles/valid/class-directive01-input.svelte @@ -0,0 +1 @@ +Hello World! diff --git a/tests/src/rules/no-inline-styles.ts b/tests/src/rules/no-inline-styles.ts new file mode 100644 index 000000000..e45d7bb5f --- /dev/null +++ b/tests/src/rules/no-inline-styles.ts @@ -0,0 +1,12 @@ +import { RuleTester } from 'eslint'; +import rule from '../../../src/rules/no-inline-styles'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +tester.run('no-inline-styles', rule as any, loadTestCases('no-inline-styles'));