Skip to content

Commit ad41bbd

Browse files
authored
Add prefer-style-directive rule (#95)
1 parent 617e3de commit ad41bbd

23 files changed

+255
-0
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
/tests/fixtures/rules/indent/invalid/ts-v5
1212
/tests/fixtures/rules/valid-compile/invalid/ts
1313
/tests/fixtures/rules/valid-compile/valid/ts
14+
/tests/fixtures/rules/prefer-style-directive
1415
/.svelte-kit
1516
/svelte.config-dist.js
1617
/build

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
284284
| [@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: |
285285
| [@ota-meshi/svelte/mustache-spacing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/mustache-spacing/) | enforce unified spacing in mustache | :wrench: |
286286
| [@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: |
287+
| [@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: |
287288
| [@ota-meshi/svelte/shorthand-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
288289
| [@ota-meshi/svelte/spaced-html-comment](https://ota-meshi.github.io/eslint-plugin-svelte/rules/spaced-html-comment/) | enforce consistent spacing after the `<!--` and before the `-->` in a HTML comment | :wrench: |
289290

docs-svelte-kit/src/lib/components/ESLintPlayground.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
lastname: 'Lovelace'
2525
};
2626
let current = 'foo';
27+
let color = 'red';
2728
<` +
2829
`/script>
2930
@@ -60,6 +61,8 @@
6061
class={current === 'foo' ? 'selected' : ''}
6162
on:click="{() => current = 'foo'}"
6263
>foo</button>
64+
65+
<div style="color: {color};">...</div>
6366
`
6467
6568
const state = deserializeState(

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
5353
| [@ota-meshi/svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
5454
| [@ota-meshi/svelte/mustache-spacing](./rules/mustache-spacing.md) | enforce unified spacing in mustache | :wrench: |
5555
| [@ota-meshi/svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
56+
| [@ota-meshi/svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
5657
| [@ota-meshi/svelte/shorthand-attribute](./rules/shorthand-attribute.md) | enforce use of shorthand syntax in attribute | :wrench: |
5758
| [@ota-meshi/svelte/spaced-html-comment](./rules/spaced-html-comment.md) | enforce consistent spacing after the `<!--` and before the `-->` in a HTML comment | :wrench: |
5859

docs/rules/prefer-class-directive.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ You cannot enforce this style by using [prettier-plugin-svelte]. That is, this r
4242

4343
Nothing.
4444

45+
## :couple: Related Rules
46+
47+
- [@ota-meshi/svelte/prefer-style-directive]
48+
49+
[@ota-meshi/svelte/prefer-style-directive]: ./prefer-style-directive.md
50+
4551
## :books: Further Reading
4652

4753
- [Svelte - Tutorial > 13. Classes / The class directive](https://svelte.dev/tutorial/classes)

docs/rules/prefer-style-directive.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/prefer-style-directive"
5+
description: "require style directives instead of style attribute"
6+
---
7+
8+
# @ota-meshi/svelte/prefer-style-directive
9+
10+
> require style directives instead of style attribute
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :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.
14+
15+
## :book: Rule Details
16+
17+
This rule aims to replace a style attribute with the style directive.
18+
19+
Style directive were added in Svelte v3.46.
20+
21+
<ESLintCodeBlock fix>
22+
23+
<!--eslint-skip-->
24+
25+
```svelte
26+
<script>
27+
/* eslint @ota-meshi/svelte/prefer-style-directive: "error" */
28+
let color = "red"
29+
</script>
30+
31+
<!-- ✓ GOOD -->
32+
<div style:color={color}>...</div>
33+
34+
<!-- ✗ BAD -->
35+
<div style="color: {color};">...</div>
36+
```
37+
38+
</ESLintCodeBlock>
39+
40+
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].
41+
42+
[prettier-plugin-svelte]: https://github.com/sveltejs/prettier-plugin-svelte
43+
44+
## :wrench: Options
45+
46+
Nothing.
47+
48+
## :couple: Related Rules
49+
50+
- [@ota-meshi/svelte/prefer-class-directive]
51+
52+
[@ota-meshi/svelte/prefer-class-directive]: ./prefer-class-directive.md
53+
54+
## :books: Further Reading
55+
56+
- [Svelte - Docs > style:property](https://svelte.dev/docs#template-syntax-element-directives-style-property)
57+
58+
## :mag: Implementation
59+
60+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/prefer-style-directive.ts)
61+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/prefer-style-directive.ts)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"dependencies": {
5656
"debug": "^4.3.1",
5757
"eslint-utils": "^3.0.0",
58+
"postcss": "^8.4.5",
5859
"sourcemap-codec": "^1.4.8",
5960
"svelte-eslint-parser": "^0.11.0"
6061
},

src/rules/prefer-style-directive.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { parse as parseCss } from "postcss"
3+
import { createRule } from "../utils"
4+
5+
export default createRule("prefer-style-directive", {
6+
meta: {
7+
docs: {
8+
description: "require style directives instead of style attribute",
9+
category: "Stylistic Issues",
10+
recommended: false,
11+
},
12+
fixable: "code",
13+
schema: [],
14+
messages: {
15+
unexpected: "Can use style directives instead.",
16+
},
17+
type: "suggestion",
18+
},
19+
create(context) {
20+
const sourceCode = context.getSourceCode()
21+
return {
22+
"SvelteStartTag > SvelteAttribute"(
23+
node: AST.SvelteAttribute & {
24+
parent: AST.SvelteStartTag
25+
},
26+
) {
27+
if (node.key.name !== "style") {
28+
return
29+
}
30+
const mustacheTags = node.value.filter(
31+
(v) => v.type === "SvelteMustacheTag",
32+
)
33+
const valueStartIndex = node.value[0].range[0]
34+
const cssCode = node.value
35+
.map((value) => {
36+
if (value.type === "SvelteMustacheTag") {
37+
return "_".repeat(value.range[1] - value.range[0])
38+
}
39+
return sourceCode.getText(value)
40+
})
41+
.join("")
42+
const root = parseCss(cssCode)
43+
root.walkDecls((decl) => {
44+
if (
45+
node.parent.attributes.some(
46+
(attr) =>
47+
attr.type === "SvelteStyleDirective" &&
48+
attr.key.name.name === decl.prop,
49+
)
50+
) {
51+
// has style directive
52+
return
53+
}
54+
55+
const declRange: AST.Range = [
56+
valueStartIndex + decl.source!.start!.offset,
57+
valueStartIndex + decl.source!.end!.offset + 1,
58+
]
59+
if (
60+
mustacheTags.some(
61+
(tag) =>
62+
(tag.range[0] < declRange[0] && declRange[0] < tag.range[1]) ||
63+
(tag.range[0] < declRange[1] && declRange[1] < tag.range[1]),
64+
)
65+
) {
66+
// intersection
67+
return
68+
}
69+
const declValueStartIndex =
70+
declRange[0] + decl.prop.length + (decl.raws.between || "").length
71+
const declValueRange: AST.Range = [
72+
declValueStartIndex,
73+
declValueStartIndex + (decl.raws.value?.value || decl.value).length,
74+
]
75+
76+
context.report({
77+
node,
78+
messageId: "unexpected",
79+
*fix(fixer) {
80+
const styleDirective = `style:${
81+
decl.prop
82+
}="${sourceCode.text.slice(...declValueRange)}"`
83+
if (root.nodes.length === 1 && root.nodes[0] === decl) {
84+
yield fixer.replaceTextRange(node.range, styleDirective)
85+
} else {
86+
yield fixer.removeRange(declRange)
87+
yield fixer.insertTextAfterRange(
88+
node.range,
89+
` ${styleDirective}`,
90+
)
91+
}
92+
},
93+
})
94+
})
95+
},
96+
}
97+
},
98+
})

src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import noTargetBlank from "../rules/no-target-blank"
1818
import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore"
1919
import noUselessMustaches from "../rules/no-useless-mustaches"
2020
import preferClassDirective from "../rules/prefer-class-directive"
21+
import preferStyleDirective from "../rules/prefer-style-directive"
2122
import shorthandAttribute from "../rules/shorthand-attribute"
2223
import spacedHtmlComment from "../rules/spaced-html-comment"
2324
import system from "../rules/system"
@@ -43,6 +44,7 @@ export const rules = [
4344
noUnusedSvelteIgnore,
4445
noUselessMustaches,
4546
preferClassDirective,
47+
preferStyleDirective,
4648
shorthandAttribute,
4749
spacedHtmlComment,
4850
system,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Can use style directives instead.",
4+
"line": 1,
5+
"column": 6
6+
}
7+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style="color: {r}e{d}">...</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style:color="{r}e{d}">...</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Can use style directives instead.",
4+
"line": 10,
5+
"column": 6
6+
}
7+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
<script>
3+
let red = "red"
4+
</script>
5+
6+
<!-- ✓ GOOD -->
7+
<div style:color={red}>...</div>
8+
9+
<!-- ✗ BAD -->
10+
<div style="color: {red};">...</div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
<script>
3+
let red = "red"
4+
</script>
5+
6+
<!-- ✓ GOOD -->
7+
<div style:color={red}>...</div>
8+
9+
<!-- ✗ BAD -->
10+
<div style:color="{red}">...</div>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Can use style directives instead.",
4+
"line": 1,
5+
"column": 6
6+
},
7+
{
8+
"message": "Can use style directives instead.",
9+
"line": 1,
10+
"column": 6
11+
}
12+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style="color: {red}; width: 12px">...</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style=" width: 12px" style:color="{red}">...</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Can use style directives instead.",
4+
"line": 1,
5+
"column": 6
6+
}
7+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style="color: red;">...</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div style:color="red">...</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
<script>
3+
let red = "red"
4+
</script>
5+
6+
<div style:color={red}>...</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/prefer-style-directive"
3+
import { loadTestCases } from "../../utils/utils"
4+
5+
const tester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: "module",
9+
},
10+
})
11+
12+
tester.run(
13+
"prefer-style-directive",
14+
rule as any,
15+
loadTestCases("prefer-style-directive"),
16+
)

0 commit comments

Comments
 (0)