Skip to content

feat: Added the no-unused-class-name rule using parser services #489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
660d537
test(no-unused-class-name): Added rule tests
marekdedic Mar 11, 2023
7753853
Merge branch 'main' into no-unused-class-name
marekdedic Apr 12, 2023
3647bc1
test(no-unused-class-name): added fixtures
marekdedic Apr 12, 2023
214ee0d
feat(no-unused-class-name): added the implementation of the rule
marekdedic Apr 14, 2023
f12bc23
feat(no-unused-class-name): added support for nested rules
marekdedic Apr 14, 2023
64e5a9a
feat(no-unused-class-name): added proper lint issue message
marekdedic Apr 15, 2023
c018c05
docs(no-unused-class-name): added docs
marekdedic Apr 15, 2023
d7053db
test(no-unused-class-name): renamed fixture
marekdedic Apr 15, 2023
ea8a7b5
feat(no-unused-class-name): added support for multiple classes on one…
marekdedic Apr 15, 2023
cde0a7f
test(no-unused-class-name): added test for SASS support
marekdedic Apr 16, 2023
57ba04b
test(no-unused-class-name): removed test for deprecated SASS syntax
marekdedic Apr 16, 2023
61e153e
feat(no-unused-class-name): handling unavailable style AST
marekdedic Apr 16, 2023
09431e9
chore(no-unused-class-name): extracted findClassesInAttribute
marekdedic Apr 16, 2023
efb0115
test(no-unused-class-name): ignoring invalid style language test case…
marekdedic Apr 16, 2023
281c287
test(no-unused-class-name): added tests for various selectors
marekdedic Apr 16, 2023
8c63f78
chore(no-unused-class-name): switched from for cycle to flatMap
marekdedic Apr 16, 2023
60e84ca
chore(no-unused-class-name): removed unneeded type assertions
marekdedic Apr 16, 2023
612a2dc
feat(no-unused-class-name): added support for class directives
marekdedic Apr 16, 2023
d6e7c62
chore(no-unused-class-name): using renamed SvelteStyle- node types
marekdedic Apr 17, 2023
c1d0fa3
chore(no-unused-class-name): added a changeset
marekdedic Apr 17, 2023
e627577
chore(no-unused-class-name): update to reflect parser types
marekdedic Apr 17, 2023
e0334b5
chore(no-unused-class-name): using type narrowing on ESLintCompatible…
marekdedic Apr 18, 2023
d3a5fd0
Merge branch 'main' into no-unused-class-name
marekdedic May 22, 2023
426ca67
chore(no-unused-class-name): removed yarn.lock
marekdedic May 22, 2023
45a2835
feat(no-ununsed-class-name): Using style AST from parser services
marekdedic May 22, 2023
0aeff3e
fix(no-ununsed-class-name): not reporting errors on invalid lang
marekdedic May 22, 2023
f594c9c
chore(no-unused-class-name): updated the rule to use the StyleContext…
marekdedic May 25, 2023
c31afeb
Merge branch 'main' into no-unused-class-name-parser-services
marekdedic Jun 11, 2023
66e17c2
chore(no-unused-class-name): updated the rule to use the new StyleCon…
marekdedic Jun 11, 2023
a69787f
test(no-unused-class-name): added a test with an invalid style language
marekdedic Jun 11, 2023
d8ed1a6
test(no-unused-class-name): added a test with pseudo-classes
marekdedic Jun 18, 2023
8476b47
fix(no-unused-class-name): added support for pseudo-classes
marekdedic Jun 18, 2023
8aa5b9a
fix(no-unused-class-name): added support for class attributes with mu…
marekdedic Jun 18, 2023
2614df0
chore(no-unused-class-name): removed premature 'since' comment
marekdedic Jun 18, 2023
3541a67
fix(no-unused-class-name): fixed eslint error on fixture
marekdedic Jun 18, 2023
ee2739c
fix(no-unused-class-name): fixed eslint error on multiline class names
marekdedic Jun 18, 2023
b7fb1b0
fix(no-unused-class-name): fixed test fixture line offset
marekdedic Jun 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/moody-seas-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-svelte": minor
---

feat: added the no-unused-class-name rule
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
/prettier-playground
/tests/fixtures/rules/indent/invalid/ts
/tests/fixtures/rules/indent/invalid/ts-v5
/tests/fixtures/rules/no-unused-class-name/valid/invalid-style01-input.svelte
/tests/fixtures/rules/no-unused-class-name/valid/unknown-lang01-input.svelte
/tests/fixtures/rules/valid-compile/invalid/ts
/tests/fixtures/rules/valid-compile/valid/babel
/tests/fixtures/rules/valid-compile/valid/ts
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [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-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 | |
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
| [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
| [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: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md) | disallow reactive statements that don't reference reactive values. | |
| [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 | |
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
| [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
Expand Down
61 changes: 61 additions & 0 deletions docs/rules/no-unused-class-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "svelte/no-unused-class-name"
description: "disallow the use of a class in the template without a corresponding style"
---

# svelte/no-unused-class-name

> disallow the use of a class in the template without a corresponding style

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>

## :book: Rule Details

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.

<ESLintCodeBlock>

<!--eslint-skip-->

```svelte
<script lang="ts">
/* eslint svelte/no-unused-class-name: "error" */
</scrip>

<!-- ✓ GOOD -->
<div class="first-class">Hello</div>
<div class="second-class">Hello</div>
<div class="third-class fourth-class">Hello</div>

<!-- ✗ BAD -->
<div class="fifth-class">Hello</div>
<div class="sixth-class first-class">Hello</div>

<style>
.first-class {
color: red;
}

.second-class,
.third-class {
color: blue;
}

.fourth-class {
color: green;
}
</style>
```

</ESLintCodeBlock>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-unused-class-name.ts)
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/no-unused-class-name.ts)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"postcss": "^8.4.5",
"postcss-load-config": "^3.1.4",
"postcss-safe-parser": "^6.0.0",
"postcss-selector-parser": "^6.0.11",
"svelte-eslint-parser": "^0.31.0"
},
"devDependencies": {
Expand Down
120 changes: 120 additions & 0 deletions src/rules/no-unused-class-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { createRule } from "../utils"
import type {
SourceLocation,
SvelteAttribute,
SvelteDirective,
SvelteShorthandAttribute,
SvelteSpecialDirective,
SvelteSpreadAttribute,
SvelteStyleDirective,
} from "svelte-eslint-parser/lib/ast"
import type { AnyNode } from "postcss"
import {
default as selectorParser,
type Node as SelectorNode,
} from "postcss-selector-parser"

export default createRule("no-unused-class-name", {
meta: {
docs: {
description:
"disallow the use of a class in the template without a corresponding style",
category: "Best Practices",
recommended: false,
},
schema: [],
messages: {},
type: "suggestion",
},
create(context) {
const classesUsedInTemplate: Record<string, SourceLocation> = {}

return {
SvelteElement(node) {
if (node.kind !== "html") {
return
}
const classes = node.startTag.attributes.flatMap(findClassesInAttribute)
for (const className of classes) {
classesUsedInTemplate[className] = node.startTag.loc
}
},
"Program:exit"() {
const styleContext = context.parserServices.getStyleContext()
if (["parse-error", "unknown-lang"].includes(styleContext.status)) {
return
}
const classesUsedInStyle =
styleContext.sourceAst != null
? findClassesInPostCSSNode(styleContext.sourceAst)
: []
for (const className in classesUsedInTemplate) {
if (!classesUsedInStyle.includes(className)) {
context.report({
loc: classesUsedInTemplate[className],
message: `Unused class "${className}".`,
})
}
}
},
}
},
})

/**
* Extract all class names used in a HTML element attribute.
*/
function findClassesInAttribute(
attribute:
| SvelteAttribute
| SvelteShorthandAttribute
| SvelteSpreadAttribute
| SvelteDirective
| SvelteStyleDirective
| SvelteSpecialDirective,
): string[] {
if (attribute.type === "SvelteAttribute" && attribute.key.name === "class") {
return attribute.value.flatMap((value) =>
value.type === "SvelteLiteral" ? value.value.trim().split(/\s+/u) : [],
)
}
if (attribute.type === "SvelteDirective" && attribute.kind === "Class") {
return [attribute.key.name.name]
}
return []
}

/**
* Extract all class names used in a PostCSS node.
*/
function findClassesInPostCSSNode(node: AnyNode): string[] {
if (node.type === "rule") {
let classes = node.nodes.flatMap(findClassesInPostCSSNode)
const processor = selectorParser()
classes = classes.concat(
findClassesInSelector(processor.astSync(node.selector)),
)
return classes
}
if (node.type === "root" || node.type === "atrule") {
return node.nodes.flatMap(findClassesInPostCSSNode)
}
return []
}

/**
* Extract all class names used in a PostCSS selector.
*/
function findClassesInSelector(node: SelectorNode): string[] {
if (node.type === "class") {
return [node.value]
}
if (
node.type === "pseudo" ||
node.type === "root" ||
node.type === "selector"
) {
return node.nodes.flatMap(findClassesInSelector)
}
return []
}
2 changes: 2 additions & 0 deletions src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import noStoreAsync from "../rules/no-store-async"
import noTargetBlank from "../rules/no-target-blank"
import noTrailingSpaces from "../rules/no-trailing-spaces"
import noUnknownStyleDirectiveProperty from "../rules/no-unknown-style-directive-property"
import noUnusedClassName from "../rules/no-unused-class-name"
import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore"
import noUselessMustaches from "../rules/no-useless-mustaches"
import preferClassDirective from "../rules/prefer-class-directive"
Expand Down Expand Up @@ -101,6 +102,7 @@ export const rules = [
noTargetBlank,
noTrailingSpaces,
noUnknownStyleDirectiveProperty,
noUnusedClassName,
noUnusedSvelteIgnore,
noUselessMustaches,
preferClassDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- message: Unused class "first".
line: 1
column: 1
suggestions: null
- message: Unused class "second".
line: 3
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class:first={true}>Hello</div>

<span class:second={false}>World!</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- message: Unused class "div-class-two".
line: 2
column: 1
suggestions: null
- message: Unused class "span-class-two".
line: 4
column: 1
suggestions: null
- message: Unused class "span-class-three".
line: 4
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- eslint-disable prettier/prettier -->
<div class="div-class div-class-two">Hello</div>

<span
class="
span-class
span-class-two
span-class-three
">World!</span>

<style>
.div-class {
color: red;
}

.span-class {
font-weight: bold;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- message: Unused class "div-class-two".
line: 1
column: 1
suggestions: null
- message: Unused class "span-class-two".
line: 3
column: 1
suggestions: null
- message: Unused class "span-class-three".
line: 3
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="div-class div-class-two">Hello</div>

<span class="span-class span-class-two span-class-three">World!</span>

<style>
.div-class {
color: red;
}

.span-class {
font-weight: bold;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- message: Unused class "div-class".
line: 1
column: 1
suggestions: null
- message: Unused class "span-class".
line: 3
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="div-class">Hello</div>

<span class="span-class">World!</span>

<style>
#div-class {
color: red;
}

#span-class {
font-weight: bold;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- message: Unused class "div-class".
line: 1
column: 1
suggestions: null
- message: Unused class "span-class".
line: 3
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="div-class">Hello</div>

<span class="span-class">World!</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- message: Unused class "div-class".
line: 1
column: 1
suggestions: null
- message: Unused class "span-class".
line: 3
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="div-class">Hello</div>

<span class="span-class">World!</span>

<style>
.unrelated-class {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="div-class">Hello</div>

<span class="span-class">World!</span>

<style>
.div-class + .span-class {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="container">
<div class="div-class">Hello</div>
</div>

<style>
.container > .div-class {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="container">
<div class="div-class">Hello</div>
</div>

<style>
.container .div-class {
color: red;
}
</style>
Loading