Skip to content

Commit 7394202

Browse files
authored
Add no-dupe-style-properties rule (#127)
1 parent 157081d commit 7394202

20 files changed

+375
-60
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
243243
| Rule ID | Description | |
244244
|:--------|:------------|:---|
245245
| [@ota-meshi/svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks/) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
246+
| [@ota-meshi/svelte/no-dupe-style-properties](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-style-properties/) | disallow duplicate style properties | :star: |
246247
| [@ota-meshi/svelte/no-dynamic-slot-name](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dynamic-slot-name/) | disallow dynamic slot name | :star::wrench: |
247248
| [@ota-meshi/svelte/no-not-function-handler](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-not-function-handler/) | disallow use of not function in event handler | :star: |
248249
| [@ota-meshi/svelte/no-object-in-text-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-object-in-text-mustaches/) | disallow objects in text mustache interpolation | :star: |

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
1616
| Rule ID | Description | |
1717
|:--------|:------------|:---|
1818
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./rules/no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
19+
| [@ota-meshi/svelte/no-dupe-style-properties](./rules/no-dupe-style-properties.md) | disallow duplicate style properties | :star: |
1920
| [@ota-meshi/svelte/no-dynamic-slot-name](./rules/no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
2021
| [@ota-meshi/svelte/no-not-function-handler](./rules/no-not-function-handler.md) | disallow use of not function in event handler | :star: |
2122
| [@ota-meshi/svelte/no-object-in-text-mustaches](./rules/no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/no-dupe-style-properties"
5+
description: "disallow duplicate style properties"
6+
---
7+
8+
# @ota-meshi/svelte/no-dupe-style-properties
9+
10+
> disallow duplicate style properties
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+
- :gear: This rule is included in `"plugin:@ota-meshi/svelte/recommended"`.
14+
15+
## :book: Rule Details
16+
17+
This rule reports duplicate style properties.
18+
19+
<ESLintCodeBlock>
20+
21+
<!--eslint-skip-->
22+
23+
```svelte
24+
<script>
25+
/* eslint @ota-meshi/svelte/no-dupe-style-properties: "error" */
26+
let red = "red"
27+
</script>
28+
29+
<!-- ✓ GOOD -->
30+
<div style="background: green; background-color: {red};">...</div>
31+
<div style:background="green" style="background-color: {red}">...</div>
32+
33+
<!-- ✗ BAD -->
34+
<div style="background: green; background: {red};">...</div>
35+
<div style:background="green" style="background: {red}">...</div>
36+
```
37+
38+
</ESLintCodeBlock>
39+
40+
## :wrench: Options
41+
42+
Nothing.
43+
44+
## :mag: Implementation
45+
46+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-style-properties.ts)
47+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-style-properties.ts)

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export = {
1010
"@ota-meshi/svelte/no-at-debug-tags": "warn",
1111
"@ota-meshi/svelte/no-at-html-tags": "error",
1212
"@ota-meshi/svelte/no-dupe-else-if-blocks": "error",
13+
"@ota-meshi/svelte/no-dupe-style-properties": "error",
1314
"@ota-meshi/svelte/no-dynamic-slot-name": "error",
1415
"@ota-meshi/svelte/no-inner-declarations": "error",
1516
"@ota-meshi/svelte/no-not-function-handler": "error",

src/rules/no-dupe-style-properties.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { createRule } from "../utils"
3+
import type { SvelteStyleInlineRoot, SvelteStyleRoot } from "../utils/css-utils"
4+
import { parseStyleAttributeValue } from "../utils/css-utils"
5+
6+
export default createRule("no-dupe-style-properties", {
7+
meta: {
8+
docs: {
9+
description: "disallow duplicate style properties",
10+
category: "Possible Errors",
11+
recommended: true,
12+
},
13+
schema: [],
14+
messages: {
15+
unexpected: "Duplicate property '{{name}}'.",
16+
},
17+
type: "problem",
18+
},
19+
create(context) {
20+
type StyleDecl = {
21+
prop: string
22+
loc: AST.SourceLocation
23+
}
24+
type StyleDeclSet = {
25+
decls: StyleDecl[]
26+
}
27+
28+
return {
29+
SvelteStartTag(node: AST.SvelteStartTag) {
30+
const reported = new Set<StyleDecl>()
31+
const beforeDeclarations = new Map<string, StyleDecl>()
32+
for (const { decls } of iterateStyleDeclSetFromAttrs(node.attributes)) {
33+
for (const decl of decls) {
34+
const already = beforeDeclarations.get(decl.prop)
35+
if (already) {
36+
for (const report of [already, decl].filter(
37+
(n) => !reported.has(n),
38+
)) {
39+
context.report({
40+
node,
41+
loc: report.loc,
42+
messageId: "unexpected",
43+
data: { name: report.prop },
44+
})
45+
reported.add(report)
46+
}
47+
}
48+
}
49+
for (const decl of decls) {
50+
beforeDeclarations.set(decl.prop, decl)
51+
}
52+
}
53+
},
54+
}
55+
56+
/** Iterate the style decl set from attrs */
57+
function* iterateStyleDeclSetFromAttrs(
58+
attrs: AST.SvelteStartTag["attributes"],
59+
): Iterable<StyleDeclSet> {
60+
for (const attr of attrs) {
61+
if (attr.type === "SvelteStyleDirective") {
62+
yield {
63+
decls: [{ prop: attr.key.name.name, loc: attr.key.name.loc! }],
64+
}
65+
} else if (attr.type === "SvelteAttribute") {
66+
if (attr.key.name !== "style") {
67+
continue
68+
}
69+
const root = parseStyleAttributeValue(attr, context)
70+
if (!root) {
71+
continue
72+
}
73+
yield* iterateStyleDeclSetFromStyleRoot(root)
74+
}
75+
}
76+
}
77+
78+
/** Iterate the style decl set from style root */
79+
function* iterateStyleDeclSetFromStyleRoot(
80+
root: SvelteStyleRoot | SvelteStyleInlineRoot,
81+
): Iterable<StyleDeclSet> {
82+
for (const child of root.nodes) {
83+
if (child.type === "decl") {
84+
yield {
85+
decls: [
86+
{
87+
prop: child.prop.name,
88+
get loc() {
89+
return child.prop.loc
90+
},
91+
},
92+
],
93+
}
94+
} else if (child.type === "inline") {
95+
const decls: StyleDecl[] = []
96+
for (const root of child.getAllInlineStyles().values()) {
97+
for (const set of iterateStyleDeclSetFromStyleRoot(root)) {
98+
decls.push(...set.decls)
99+
}
100+
}
101+
yield { decls }
102+
}
103+
}
104+
}
105+
},
106+
})

src/rules/no-shorthand-style-property-overrides.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AST } from "svelte-eslint-parser"
22
import { createRule } from "../utils"
3-
import type { SvelteStyleRoot } from "../utils/css-utils"
3+
import type { SvelteStyleInlineRoot, SvelteStyleRoot } from "../utils/css-utils"
44
import {
55
getVendorPrefix,
66
stripVendorPrefix,
@@ -92,7 +92,7 @@ export default createRule("no-shorthand-style-property-overrides", {
9292

9393
/** Iterate the style decl set from style root */
9494
function* iterateStyleDeclSetFromStyleRoot(
95-
root: SvelteStyleRoot,
95+
root: SvelteStyleRoot | SvelteStyleInlineRoot,
9696
): Iterable<StyleDeclSet> {
9797
for (const child of root.nodes) {
9898
if (child.type === "decl") {

0 commit comments

Comments
 (0)