diff --git a/.eslintignore b/.eslintignore
index 797ba184c..6a24eef33 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -11,6 +11,7 @@
/tests/fixtures/rules/indent/invalid/ts-v5
/tests/fixtures/rules/valid-compile/invalid/ts
/tests/fixtures/rules/valid-compile/valid/ts
+/tests/fixtures/rules/prefer-style-directive
/.svelte-kit
/svelte.config-dist.js
/build
diff --git a/README.md b/README.md
index cf26da3b9..c3a2bb31d 100644
--- a/README.md
+++ b/README.md
@@ -284,6 +284,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| [@ota-meshi/svelte/max-attributes-per-line](https://ota-meshi.github.io/eslint-plugin-svelte/rules/max-attributes-per-line/) | enforce the maximum number of attributes per line | :wrench: |
| [@ota-meshi/svelte/mustache-spacing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/mustache-spacing/) | enforce unified spacing in mustache | :wrench: |
| [@ota-meshi/svelte/prefer-class-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
+| [@ota-meshi/svelte/prefer-style-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
| [@ota-meshi/svelte/shorthand-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
| [@ota-meshi/svelte/spaced-html-comment](https://ota-meshi.github.io/eslint-plugin-svelte/rules/spaced-html-comment/) | enforce consistent spacing after the `` in a HTML comment | :wrench: |
diff --git a/docs-svelte-kit/src/lib/components/ESLintPlayground.svelte b/docs-svelte-kit/src/lib/components/ESLintPlayground.svelte
index 0873f2f16..bca252604 100644
--- a/docs-svelte-kit/src/lib/components/ESLintPlayground.svelte
+++ b/docs-svelte-kit/src/lib/components/ESLintPlayground.svelte
@@ -24,6 +24,7 @@
lastname: 'Lovelace'
};
let current = 'foo';
+ let color = 'red';
<` +
`/script>
@@ -60,6 +61,8 @@
class={current === 'foo' ? 'selected' : ''}
on:click="{() => current = 'foo'}"
>foo
+
+
...
`
const state = deserializeState(
diff --git a/docs/rules.md b/docs/rules.md
index 37d621d73..e6ba92f76 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -53,6 +53,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| [@ota-meshi/svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
| [@ota-meshi/svelte/mustache-spacing](./rules/mustache-spacing.md) | enforce unified spacing in mustache | :wrench: |
| [@ota-meshi/svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
+| [@ota-meshi/svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
| [@ota-meshi/svelte/shorthand-attribute](./rules/shorthand-attribute.md) | enforce use of shorthand syntax in attribute | :wrench: |
| [@ota-meshi/svelte/spaced-html-comment](./rules/spaced-html-comment.md) | enforce consistent spacing after the `` in a HTML comment | :wrench: |
diff --git a/docs/rules/prefer-class-directive.md b/docs/rules/prefer-class-directive.md
index 7cd683172..b6a542b7c 100644
--- a/docs/rules/prefer-class-directive.md
+++ b/docs/rules/prefer-class-directive.md
@@ -42,6 +42,12 @@ You cannot enforce this style by using [prettier-plugin-svelte]. That is, this r
Nothing.
+## :couple: Related Rules
+
+- [@ota-meshi/svelte/prefer-style-directive]
+
+[@ota-meshi/svelte/prefer-style-directive]: ./prefer-style-directive.md
+
## :books: Further Reading
- [Svelte - Tutorial > 13. Classes / The class directive](https://svelte.dev/tutorial/classes)
diff --git a/docs/rules/prefer-style-directive.md b/docs/rules/prefer-style-directive.md
new file mode 100644
index 000000000..103790369
--- /dev/null
+++ b/docs/rules/prefer-style-directive.md
@@ -0,0 +1,61 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "@ota-meshi/svelte/prefer-style-directive"
+description: "require style directives instead of style attribute"
+---
+
+# @ota-meshi/svelte/prefer-style-directive
+
+> require style directives instead of style attribute
+
+- :exclamation: **_This rule has not been released yet._**
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule aims to replace a style attribute with the style directive.
+
+Style directive were added in Svelte v3.46.
+
+
+
+
+
+```svelte
+
+
+
+...
+
+
+...
+```
+
+
+
+You cannot enforce this style by using [prettier-plugin-svelte]. That is, this rule does not conflict with [prettier-plugin-svelte] and can be used with [prettier-plugin-svelte].
+
+[prettier-plugin-svelte]: https://github.com/sveltejs/prettier-plugin-svelte
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [@ota-meshi/svelte/prefer-class-directive]
+
+[@ota-meshi/svelte/prefer-class-directive]: ./prefer-class-directive.md
+
+## :books: Further Reading
+
+- [Svelte - Docs > style:property](https://svelte.dev/docs#template-syntax-element-directives-style-property)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/prefer-style-directive.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/prefer-style-directive.ts)
diff --git a/package.json b/package.json
index 6787d7af4..6803e54bb 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"dependencies": {
"debug": "^4.3.1",
"eslint-utils": "^3.0.0",
+ "postcss": "^8.4.5",
"sourcemap-codec": "^1.4.8",
"svelte-eslint-parser": "^0.11.0"
},
diff --git a/src/rules/prefer-style-directive.ts b/src/rules/prefer-style-directive.ts
new file mode 100644
index 000000000..2958da4df
--- /dev/null
+++ b/src/rules/prefer-style-directive.ts
@@ -0,0 +1,98 @@
+import type { AST } from "svelte-eslint-parser"
+import { parse as parseCss } from "postcss"
+import { createRule } from "../utils"
+
+export default createRule("prefer-style-directive", {
+ meta: {
+ docs: {
+ description: "require style directives instead of style attribute",
+ category: "Stylistic Issues",
+ recommended: false,
+ },
+ fixable: "code",
+ schema: [],
+ messages: {
+ unexpected: "Can use style directives instead.",
+ },
+ type: "suggestion",
+ },
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ return {
+ "SvelteStartTag > SvelteAttribute"(
+ node: AST.SvelteAttribute & {
+ parent: AST.SvelteStartTag
+ },
+ ) {
+ if (node.key.name !== "style") {
+ return
+ }
+ const mustacheTags = node.value.filter(
+ (v) => v.type === "SvelteMustacheTag",
+ )
+ const valueStartIndex = node.value[0].range[0]
+ const cssCode = node.value
+ .map((value) => {
+ if (value.type === "SvelteMustacheTag") {
+ return "_".repeat(value.range[1] - value.range[0])
+ }
+ return sourceCode.getText(value)
+ })
+ .join("")
+ const root = parseCss(cssCode)
+ root.walkDecls((decl) => {
+ if (
+ node.parent.attributes.some(
+ (attr) =>
+ attr.type === "SvelteStyleDirective" &&
+ attr.key.name.name === decl.prop,
+ )
+ ) {
+ // has style directive
+ return
+ }
+
+ const declRange: AST.Range = [
+ valueStartIndex + decl.source!.start!.offset,
+ valueStartIndex + decl.source!.end!.offset + 1,
+ ]
+ if (
+ mustacheTags.some(
+ (tag) =>
+ (tag.range[0] < declRange[0] && declRange[0] < tag.range[1]) ||
+ (tag.range[0] < declRange[1] && declRange[1] < tag.range[1]),
+ )
+ ) {
+ // intersection
+ return
+ }
+ const declValueStartIndex =
+ declRange[0] + decl.prop.length + (decl.raws.between || "").length
+ const declValueRange: AST.Range = [
+ declValueStartIndex,
+ declValueStartIndex + (decl.raws.value?.value || decl.value).length,
+ ]
+
+ context.report({
+ node,
+ messageId: "unexpected",
+ *fix(fixer) {
+ const styleDirective = `style:${
+ decl.prop
+ }="${sourceCode.text.slice(...declValueRange)}"`
+ if (root.nodes.length === 1 && root.nodes[0] === decl) {
+ yield fixer.replaceTextRange(node.range, styleDirective)
+ } else {
+ yield fixer.removeRange(declRange)
+ yield fixer.insertTextAfterRange(
+ node.range,
+ ` ${styleDirective}`,
+ )
+ }
+ },
+ })
+ })
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 68326bfde..d0b924678 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -18,6 +18,7 @@ import noTargetBlank from "../rules/no-target-blank"
import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore"
import noUselessMustaches from "../rules/no-useless-mustaches"
import preferClassDirective from "../rules/prefer-class-directive"
+import preferStyleDirective from "../rules/prefer-style-directive"
import shorthandAttribute from "../rules/shorthand-attribute"
import spacedHtmlComment from "../rules/spaced-html-comment"
import system from "../rules/system"
@@ -43,6 +44,7 @@ export const rules = [
noUnusedSvelteIgnore,
noUselessMustaches,
preferClassDirective,
+ preferStyleDirective,
shorthandAttribute,
spacedHtmlComment,
system,
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-errors.json b/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-errors.json
new file mode 100644
index 000000000..d07093c24
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "Can use style directives instead.",
+ "line": 1,
+ "column": 6
+ }
+]
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-input.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-input.svelte
new file mode 100644
index 000000000..886a594e0
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-input.svelte
@@ -0,0 +1 @@
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-output.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-output.svelte
new file mode 100644
index 000000000..141e90f01
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/complex-test01-output.svelte
@@ -0,0 +1 @@
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test01-errors.json b/tests/fixtures/rules/prefer-style-directive/invalid/test01-errors.json
new file mode 100644
index 000000000..433fc031c
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "Can use style directives instead.",
+ "line": 10,
+ "column": 6
+ }
+]
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test01-input.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/test01-input.svelte
new file mode 100644
index 000000000..11e76640a
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test01-input.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+...
+
+
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test01-output.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/test01-output.svelte
new file mode 100644
index 000000000..1c822cf8e
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test01-output.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+...
+
+
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test02-errors.json b/tests/fixtures/rules/prefer-style-directive/invalid/test02-errors.json
new file mode 100644
index 000000000..ec785109e
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test02-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Can use style directives instead.",
+ "line": 1,
+ "column": 6
+ },
+ {
+ "message": "Can use style directives instead.",
+ "line": 1,
+ "column": 6
+ }
+]
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test02-input.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/test02-input.svelte
new file mode 100644
index 000000000..93edb7300
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test02-input.svelte
@@ -0,0 +1 @@
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test02-output.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/test02-output.svelte
new file mode 100644
index 000000000..a9984482f
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test02-output.svelte
@@ -0,0 +1 @@
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test03-errors.json b/tests/fixtures/rules/prefer-style-directive/invalid/test03-errors.json
new file mode 100644
index 000000000..d07093c24
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test03-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "Can use style directives instead.",
+ "line": 1,
+ "column": 6
+ }
+]
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test03-input.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/test03-input.svelte
new file mode 100644
index 000000000..99b2d8388
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test03-input.svelte
@@ -0,0 +1 @@
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/invalid/test03-output.svelte b/tests/fixtures/rules/prefer-style-directive/invalid/test03-output.svelte
new file mode 100644
index 000000000..c1dedaa09
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/invalid/test03-output.svelte
@@ -0,0 +1 @@
+...
diff --git a/tests/fixtures/rules/prefer-style-directive/valid/test01-input.svelte b/tests/fixtures/rules/prefer-style-directive/valid/test01-input.svelte
new file mode 100644
index 000000000..70b1536e1
--- /dev/null
+++ b/tests/fixtures/rules/prefer-style-directive/valid/test01-input.svelte
@@ -0,0 +1,6 @@
+
+
+
+...
diff --git a/tests/src/rules/prefer-style-directive.ts b/tests/src/rules/prefer-style-directive.ts
new file mode 100644
index 000000000..cd8c4e364
--- /dev/null
+++ b/tests/src/rules/prefer-style-directive.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/prefer-style-directive"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "prefer-style-directive",
+ rule as any,
+ loadTestCases("prefer-style-directive"),
+)