Skip to content

Commit b1d7d45

Browse files
authored
Fix false positives for extend style lang attributes in rules that use compilation (#138)
1 parent 98d70fe commit b1d7d45

File tree

7 files changed

+142
-29
lines changed

7 files changed

+142
-29
lines changed

docs/rules/no-unknown-style-directive-property.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Note that this rule only checks the `style:property` directive. If you want to c
5656
}
5757
```
5858

59-
- `ignoreProperties` ... You can specify property names or patterns that you want to ignore from checking.
59+
- `ignoreProperties` ... You can specify property names or patterns that you want to ignore from checking. When specifying a pattern, specify a string like a regex literal. e.g. `"/pattern/i"`
6060
- `ignorePrefixed` ... If `true`, ignores properties with vendor prefix from checking. Default is `true`.
6161

6262
## :books: Further reading

src/rules/no-unused-svelte-ignore.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import type { AST } from "svelte-eslint-parser"
22
import { isOpeningParenToken } from "eslint-utils"
33
import type { Warning } from "../shared/svelte-compile-warns"
4-
import { getSvelteCompileWarnings } from "../shared/svelte-compile-warns"
4+
import {
5+
getSvelteCompileWarnings,
6+
extractStyleElementsWithLangOtherThanCSS,
7+
} from "../shared/svelte-compile-warns"
58
import { createRule } from "../utils"
69
import type { ASTNodeWithParent } from "../types"
710

811
const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore/m
9-
12+
const CSS_WARN_CODES = new Set([
13+
"css-unused-selector",
14+
"css-invalid-global",
15+
"css-invalid-global-selector",
16+
])
1017
type IgnoreItem = {
1118
range: [number, number]
1219
code: string
@@ -113,6 +120,7 @@ export default createRule("no-unused-svelte-ignore", {
113120
if (!ignoreComments.length) {
114121
return {}
115122
}
123+
116124
const warnings = getSvelteCompileWarnings(context, {
117125
warnings: "onlyWarnings",
118126
removeComments: new Set(ignoreComments.map((i) => i.token)),
@@ -128,15 +136,24 @@ export default createRule("no-unused-svelte-ignore", {
128136
if (!node) {
129137
continue
130138
}
131-
l: for (const comment of extractLeadingComments(node).reverse()) {
132-
for (const ignoreItem of ignoreComments) {
133-
if (
134-
ignoreItem.token === comment &&
135-
ignoreItem.code === warning.code
136-
) {
137-
used.add(ignoreItem)
138-
break l
139-
}
139+
for (const comment of extractLeadingComments(node).reverse()) {
140+
const ignoreItem = ignoreComments.find(
141+
(item) => item.token === comment && item.code === warning.code,
142+
)
143+
if (ignoreItem) {
144+
used.add(ignoreItem)
145+
}
146+
}
147+
}
148+
149+
// Styles with non-CSS lang attributes are ignored from compilation and cannot determine css errors.
150+
for (const node of extractStyleElementsWithLangOtherThanCSS(context)) {
151+
for (const comment of extractLeadingComments(node).reverse()) {
152+
const ignoreItem = ignoreComments.find(
153+
(item) => item.token === comment && CSS_WARN_CODES.has(item.code),
154+
)
155+
if (ignoreItem) {
156+
used.add(ignoreItem)
140157
}
141158
}
142159
}
@@ -160,7 +177,10 @@ export default createRule("no-unused-svelte-ignore", {
160177
}
161178
let targetNode = sourceCode.getNodeByRangeIndex(index)
162179
while (targetNode) {
163-
if (targetNode.type === "SvelteElement") {
180+
if (
181+
targetNode.type === "SvelteElement" ||
182+
targetNode.type === "SvelteStyleElement"
183+
) {
164184
return targetNode
165185
}
166186
if (targetNode.parent) {

src/shared/svelte-compile-warns.ts

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,8 @@ export function getSvelteCompileWarnings(
3535
option: GetSvelteWarningsOption,
3636
): Warning[] | null {
3737
const sourceCode = context.getSourceCode()
38-
const text = !option.removeComments
39-
? sourceCode.text
40-
: (() => {
41-
let code = ""
42-
let start = 0
43-
for (const token of [...option.removeComments].sort(
44-
(a, b) => a.range[0] - b.range[0],
45-
)) {
46-
code +=
47-
sourceCode.text.slice(start, token.range[0]) +
48-
sourceCode.text.slice(...token.range).replace(/[^\t\n\r ]/g, " ")
49-
start = token.range[1]
50-
}
51-
code += sourceCode.text.slice(start)
52-
return code
53-
})()
38+
39+
const text = buildStrippedText(context, option)
5440

5541
if (!context.parserServices.esTreeNodeToTSNodeMap) {
5642
return getWarningsFromCode(text, option)
@@ -293,6 +279,67 @@ export function getSvelteCompileWarnings(
293279
return warnings
294280
}
295281

282+
/**
283+
* Extracts the style with the lang attribute other than CSS.
284+
*/
285+
export function* extractStyleElementsWithLangOtherThanCSS(
286+
context: RuleContext,
287+
): Iterable<AST.SvelteStyleElement> {
288+
const sourceCode = context.getSourceCode()
289+
const root = sourceCode.ast
290+
for (const node of root.body) {
291+
if (node.type === "SvelteStyleElement") {
292+
const langAttr = node.startTag.attributes.find(
293+
(attr): attr is AST.SvelteAttribute =>
294+
attr.type === "SvelteAttribute" && attr.key.name === "lang",
295+
)
296+
if (
297+
langAttr &&
298+
langAttr.value.length === 1 &&
299+
langAttr.value[0].type === "SvelteLiteral" &&
300+
langAttr.value[0].value.toLowerCase() !== "css"
301+
) {
302+
yield node
303+
}
304+
}
305+
}
306+
}
307+
308+
/**
309+
* Build the text stripped of tokens that are not needed for compilation.
310+
*/
311+
function buildStrippedText(
312+
context: RuleContext,
313+
option: GetSvelteWarningsOption,
314+
) {
315+
const sourceCode = context.getSourceCode()
316+
const baseText = sourceCode.text
317+
318+
const removeTokens: (AST.Token | AST.Comment | AST.SvelteText)[] =
319+
option.removeComments ? [...option.removeComments] : []
320+
321+
// Strips the style with the lang attribute other than CSS.
322+
for (const node of extractStyleElementsWithLangOtherThanCSS(context)) {
323+
removeTokens.push(...node.children)
324+
}
325+
if (!removeTokens.length) {
326+
return baseText
327+
}
328+
329+
removeTokens.sort((a, b) => a.range[0] - b.range[0])
330+
331+
let code = ""
332+
let start = 0
333+
for (const token of removeTokens) {
334+
code +=
335+
baseText.slice(start, token.range[0]) +
336+
baseText.slice(...token.range).replace(/[^\t\n\r ]/g, " ")
337+
start = token.range[1]
338+
}
339+
code += baseText.slice(start)
340+
return code
341+
}
342+
296343
type TS = typeof typescript
297344

298345
/**
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="foo" />
2+
3+
<!-- svelte-ignore css-unused-selector -->
4+
<style>
5+
.bar {
6+
height: 10px;
7+
width: 10px;
8+
}
9+
</style>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class="foo">
2+
<div class="bar" />
3+
</div>
4+
5+
<!-- svelte-ignore css-unused-selector -->
6+
<style lang="postcss">
7+
.foo {
8+
height: 20px;
9+
width: 20px;
10+
& .foo {
11+
height: 10px;
12+
width: 10px;
13+
}
14+
}
15+
</style>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="foo" />
2+
3+
<style>
4+
.foo {
5+
height: 10px;
6+
width: 10px;
7+
}
8+
</style>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<div class="foo">
2+
<div class="bar" />
3+
</div>
4+
5+
<style lang="postcss">
6+
.foo {
7+
height: 20px;
8+
width: 20px;
9+
& .bar {
10+
height: 10px;
11+
width: 10px;
12+
}
13+
}
14+
</style>

0 commit comments

Comments
 (0)