Skip to content

Commit

Permalink
feat: Added the no-unused-class-name rule using parser services (#489)
Browse files Browse the repository at this point in the history
  • Loading branch information
marekdedic committed Jun 19, 2023
1 parent eca2c3d commit cc321f4
Show file tree
Hide file tree
Showing 34 changed files with 488 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-seas-kick.md
@@ -0,0 +1,5 @@
---
"eslint-plugin-svelte": minor
---

feat: added the no-unused-class-name rule
2 changes: 2 additions & 0 deletions .eslintignore
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
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
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
@@ -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
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
@@ -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
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
@@ -0,0 +1,8 @@
- message: Unused class "first".
line: 1
column: 1
suggestions: null
- message: Unused class "second".
line: 3
column: 1
suggestions: null
@@ -0,0 +1,3 @@
<div class:first={true}>Hello</div>

<span class:second={false}>World!</span>
@@ -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
@@ -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>
@@ -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
@@ -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>
@@ -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
@@ -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>
@@ -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
@@ -0,0 +1,3 @@
<div class="div-class">Hello</div>

<span class="span-class">World!</span>
@@ -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
@@ -0,0 +1,9 @@
<div class="div-class">Hello</div>

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

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

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

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

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

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

0 comments on commit cc321f4

Please sign in to comment.