-
-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathno-unused-class-name.ts
123 lines (118 loc) · 3.25 KB
/
no-unused-class-name.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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) {
if (!context.parserServices.isSvelte) {
return {}
}
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 []
}