Skip to content

Commit cc321f4

Browse files
authored
feat: Added the no-unused-class-name rule using parser services (#489)
1 parent eca2c3d commit cc321f4

34 files changed

+488
-0
lines changed

.changeset/moody-seas-kick.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": minor
3+
---
4+
5+
feat: added the no-unused-class-name rule

.eslintignore

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
/prettier-playground
99
/tests/fixtures/rules/indent/invalid/ts
1010
/tests/fixtures/rules/indent/invalid/ts-v5
11+
/tests/fixtures/rules/no-unused-class-name/valid/invalid-style01-input.svelte
12+
/tests/fixtures/rules/no-unused-class-name/valid/unknown-lang01-input.svelte
1113
/tests/fixtures/rules/valid-compile/invalid/ts
1214
/tests/fixtures/rules/valid-compile/valid/babel
1315
/tests/fixtures/rules/valid-compile/valid/ts

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ These rules relate to better ways of doing things to help you avoid problems:
343343
| [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. | |
344344
| [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: |
345345
| [svelte/no-reactive-literals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | don't assign literal values in reactive statements | :bulb: |
346+
| [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 | |
346347
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
347348
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
348349
| [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ These rules relate to better ways of doing things to help you avoid problems:
5656
| [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md) | disallow reactive statements that don't reference reactive values. | |
5757
| [svelte/no-reactive-functions](./rules/no-reactive-functions.md) | it's not necessary to define functions in reactive statements | :bulb: |
5858
| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | don't assign literal values in reactive statements | :bulb: |
59+
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
5960
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
6061
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
6162
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |

docs/rules/no-unused-class-name.md

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/no-unused-class-name"
5+
description: "disallow the use of a class in the template without a corresponding style"
6+
---
7+
8+
# svelte/no-unused-class-name
9+
10+
> disallow the use of a class in the template without a corresponding style
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+
14+
## :book: Rule Details
15+
16+
This rule is aimed at reducing unused classes in the HTML template. While `svelte-check` will produce the `css-unused-selector` if your `<style>` block includes any classes that aren't used in the template, this rule works the other way around - it reports cases wehre the template contains classes that aren't referred to in the `<style>` block.
17+
18+
<ESLintCodeBlock>
19+
20+
<!--eslint-skip-->
21+
22+
```svelte
23+
<script lang="ts">
24+
/* eslint svelte/no-unused-class-name: "error" */
25+
</scrip>
26+
27+
<!-- ✓ GOOD -->
28+
<div class="first-class">Hello</div>
29+
<div class="second-class">Hello</div>
30+
<div class="third-class fourth-class">Hello</div>
31+
32+
<!-- ✗ BAD -->
33+
<div class="fifth-class">Hello</div>
34+
<div class="sixth-class first-class">Hello</div>
35+
36+
<style>
37+
.first-class {
38+
color: red;
39+
}
40+
41+
.second-class,
42+
.third-class {
43+
color: blue;
44+
}
45+
46+
.fourth-class {
47+
color: green;
48+
}
49+
</style>
50+
```
51+
52+
</ESLintCodeBlock>
53+
54+
## :wrench: Options
55+
56+
Nothing.
57+
58+
## :mag: Implementation
59+
60+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-unused-class-name.ts)
61+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/no-unused-class-name.ts)

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"postcss": "^8.4.5",
7676
"postcss-load-config": "^3.1.4",
7777
"postcss-safe-parser": "^6.0.0",
78+
"postcss-selector-parser": "^6.0.11",
7879
"svelte-eslint-parser": "^0.31.0"
7980
},
8081
"devDependencies": {

src/rules/no-unused-class-name.ts

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { createRule } from "../utils"
2+
import type {
3+
SourceLocation,
4+
SvelteAttribute,
5+
SvelteDirective,
6+
SvelteShorthandAttribute,
7+
SvelteSpecialDirective,
8+
SvelteSpreadAttribute,
9+
SvelteStyleDirective,
10+
} from "svelte-eslint-parser/lib/ast"
11+
import type { AnyNode } from "postcss"
12+
import {
13+
default as selectorParser,
14+
type Node as SelectorNode,
15+
} from "postcss-selector-parser"
16+
17+
export default createRule("no-unused-class-name", {
18+
meta: {
19+
docs: {
20+
description:
21+
"disallow the use of a class in the template without a corresponding style",
22+
category: "Best Practices",
23+
recommended: false,
24+
},
25+
schema: [],
26+
messages: {},
27+
type: "suggestion",
28+
},
29+
create(context) {
30+
const classesUsedInTemplate: Record<string, SourceLocation> = {}
31+
32+
return {
33+
SvelteElement(node) {
34+
if (node.kind !== "html") {
35+
return
36+
}
37+
const classes = node.startTag.attributes.flatMap(findClassesInAttribute)
38+
for (const className of classes) {
39+
classesUsedInTemplate[className] = node.startTag.loc
40+
}
41+
},
42+
"Program:exit"() {
43+
const styleContext = context.parserServices.getStyleContext()
44+
if (["parse-error", "unknown-lang"].includes(styleContext.status)) {
45+
return
46+
}
47+
const classesUsedInStyle =
48+
styleContext.sourceAst != null
49+
? findClassesInPostCSSNode(styleContext.sourceAst)
50+
: []
51+
for (const className in classesUsedInTemplate) {
52+
if (!classesUsedInStyle.includes(className)) {
53+
context.report({
54+
loc: classesUsedInTemplate[className],
55+
message: `Unused class "${className}".`,
56+
})
57+
}
58+
}
59+
},
60+
}
61+
},
62+
})
63+
64+
/**
65+
* Extract all class names used in a HTML element attribute.
66+
*/
67+
function findClassesInAttribute(
68+
attribute:
69+
| SvelteAttribute
70+
| SvelteShorthandAttribute
71+
| SvelteSpreadAttribute
72+
| SvelteDirective
73+
| SvelteStyleDirective
74+
| SvelteSpecialDirective,
75+
): string[] {
76+
if (attribute.type === "SvelteAttribute" && attribute.key.name === "class") {
77+
return attribute.value.flatMap((value) =>
78+
value.type === "SvelteLiteral" ? value.value.trim().split(/\s+/u) : [],
79+
)
80+
}
81+
if (attribute.type === "SvelteDirective" && attribute.kind === "Class") {
82+
return [attribute.key.name.name]
83+
}
84+
return []
85+
}
86+
87+
/**
88+
* Extract all class names used in a PostCSS node.
89+
*/
90+
function findClassesInPostCSSNode(node: AnyNode): string[] {
91+
if (node.type === "rule") {
92+
let classes = node.nodes.flatMap(findClassesInPostCSSNode)
93+
const processor = selectorParser()
94+
classes = classes.concat(
95+
findClassesInSelector(processor.astSync(node.selector)),
96+
)
97+
return classes
98+
}
99+
if (node.type === "root" || node.type === "atrule") {
100+
return node.nodes.flatMap(findClassesInPostCSSNode)
101+
}
102+
return []
103+
}
104+
105+
/**
106+
* Extract all class names used in a PostCSS selector.
107+
*/
108+
function findClassesInSelector(node: SelectorNode): string[] {
109+
if (node.type === "class") {
110+
return [node.value]
111+
}
112+
if (
113+
node.type === "pseudo" ||
114+
node.type === "root" ||
115+
node.type === "selector"
116+
) {
117+
return node.nodes.flatMap(findClassesInSelector)
118+
}
119+
return []
120+
}

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import noStoreAsync from "../rules/no-store-async"
4141
import noTargetBlank from "../rules/no-target-blank"
4242
import noTrailingSpaces from "../rules/no-trailing-spaces"
4343
import noUnknownStyleDirectiveProperty from "../rules/no-unknown-style-directive-property"
44+
import noUnusedClassName from "../rules/no-unused-class-name"
4445
import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore"
4546
import noUselessMustaches from "../rules/no-useless-mustaches"
4647
import preferClassDirective from "../rules/prefer-class-directive"
@@ -101,6 +102,7 @@ export const rules = [
101102
noTargetBlank,
102103
noTrailingSpaces,
103104
noUnknownStyleDirectiveProperty,
105+
noUnusedClassName,
104106
noUnusedSvelteIgnore,
105107
noUselessMustaches,
106108
preferClassDirective,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: Unused class "first".
2+
line: 1
3+
column: 1
4+
suggestions: null
5+
- message: Unused class "second".
6+
line: 3
7+
column: 1
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class:first={true}>Hello</div>
2+
3+
<span class:second={false}>World!</span>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Unused class "div-class-two".
2+
line: 2
3+
column: 1
4+
suggestions: null
5+
- message: Unused class "span-class-two".
6+
line: 4
7+
column: 1
8+
suggestions: null
9+
- message: Unused class "span-class-three".
10+
line: 4
11+
column: 1
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!-- eslint-disable prettier/prettier -->
2+
<div class="div-class div-class-two">Hello</div>
3+
4+
<span
5+
class="
6+
span-class
7+
span-class-two
8+
span-class-three
9+
">World!</span>
10+
11+
<style>
12+
.div-class {
13+
color: red;
14+
}
15+
16+
.span-class {
17+
font-weight: bold;
18+
}
19+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Unused class "div-class-two".
2+
line: 1
3+
column: 1
4+
suggestions: null
5+
- message: Unused class "span-class-two".
6+
line: 3
7+
column: 1
8+
suggestions: null
9+
- message: Unused class "span-class-three".
10+
line: 3
11+
column: 1
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="div-class div-class-two">Hello</div>
2+
3+
<span class="span-class span-class-two span-class-three">World!</span>
4+
5+
<style>
6+
.div-class {
7+
color: red;
8+
}
9+
10+
.span-class {
11+
font-weight: bold;
12+
}
13+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: Unused class "div-class".
2+
line: 1
3+
column: 1
4+
suggestions: null
5+
- message: Unused class "span-class".
6+
line: 3
7+
column: 1
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="div-class">Hello</div>
2+
3+
<span class="span-class">World!</span>
4+
5+
<style>
6+
#div-class {
7+
color: red;
8+
}
9+
10+
#span-class {
11+
font-weight: bold;
12+
}
13+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: Unused class "div-class".
2+
line: 1
3+
column: 1
4+
suggestions: null
5+
- message: Unused class "span-class".
6+
line: 3
7+
column: 1
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="div-class">Hello</div>
2+
3+
<span class="span-class">World!</span>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: Unused class "div-class".
2+
line: 1
3+
column: 1
4+
suggestions: null
5+
- message: Unused class "span-class".
6+
line: 3
7+
column: 1
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="div-class">Hello</div>
2+
3+
<span class="span-class">World!</span>
4+
5+
<style>
6+
.unrelated-class {
7+
color: red;
8+
}
9+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="div-class">Hello</div>
2+
3+
<span class="span-class">World!</span>
4+
5+
<style>
6+
.div-class + .span-class {
7+
color: red;
8+
}
9+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="container">
2+
<div class="div-class">Hello</div>
3+
</div>
4+
5+
<style>
6+
.container > .div-class {
7+
color: red;
8+
}
9+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="container">
2+
<div class="div-class">Hello</div>
3+
</div>
4+
5+
<style>
6+
.container .div-class {
7+
color: red;
8+
}
9+
</style>

0 commit comments

Comments
 (0)