Skip to content

Commit cbb4347

Browse files
authored
Add no-unused-svelte-ignore rule (#87)
1 parent ce33799 commit cbb4347

24 files changed

+840
-384
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ These rules relate to better ways of doing things to help you avoid problems:
269269
|:--------|:------------|:---|
270270
| [@ota-meshi/svelte/button-has-type](https://ota-meshi.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | |
271271
| [@ota-meshi/svelte/no-at-debug-tags](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: |
272+
| [@ota-meshi/svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
272273
| [@ota-meshi/svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
273274

274275
## Stylistic Issues

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ These rules relate to better ways of doing things to help you avoid problems:
3838
|:--------|:------------|:---|
3939
| [@ota-meshi/svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
4040
| [@ota-meshi/svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
41+
| [@ota-meshi/svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
4142
| [@ota-meshi/svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
4243

4344
## Stylistic Issues

docs/rules/no-unused-svelte-ignore.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/no-unused-svelte-ignore"
5+
description: "disallow unused svelte-ignore comments"
6+
---
7+
8+
# @ota-meshi/svelte/no-unused-svelte-ignore
9+
10+
> disallow unused svelte-ignore comments
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+
- :gear: This rule is included in `"plugin:@ota-meshi/svelte/recommended"`.
14+
15+
## :book: Rule Details
16+
17+
This rule warns unnecessary `svelte-ignore` comments.
18+
19+
<ESLintCodeBlock>
20+
21+
<!--eslint-skip-->
22+
23+
```svelte
24+
<script>
25+
/* eslint @ota-meshi/svelte/no-unused-svelte-ignore: "error" */
26+
</script>
27+
28+
<!-- ✓ GOOD -->
29+
<!-- svelte-ignore a11y-autofocus a11y-missing-attribute -->
30+
<img src="https://example.com/img.png" autofocus />
31+
32+
<!-- ✗ BAD -->
33+
<!-- svelte-ignore a11y-autofocus a11y-missing-attribute -->
34+
<img src="https://example.com/img.png" alt="Foo" />
35+
```
36+
37+
</ESLintCodeBlock>
38+
39+
## :wrench: Options
40+
41+
Nothing.
42+
43+
## :books: Further Reading
44+
45+
- [Svelte - Docs > Comments](https://svelte.dev/docs#template-syntax-comments)
46+
47+
## :mag: Implementation
48+
49+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-unused-svelte-ignore.ts)
50+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-unused-svelte-ignore.ts)

src/configs/recommended.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export = {
1515
"@ota-meshi/svelte/no-inner-declarations": "error",
1616
"@ota-meshi/svelte/no-not-function-handler": "error",
1717
"@ota-meshi/svelte/no-object-in-text-mustaches": "error",
18+
"@ota-meshi/svelte/no-unused-svelte-ignore": "error",
1819
"@ota-meshi/svelte/system": "error",
1920
"@ota-meshi/svelte/valid-compile": "error",
2021
},

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

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { isOpeningParenToken } from "eslint-utils"
3+
import type { Warning } from "../shared/svelte-compile-warns"
4+
import { getSvelteCompileWarnings } from "../shared/svelte-compile-warns"
5+
import { createRule } from "../utils"
6+
import type { ASTNodeWithParent } from "../types"
7+
8+
const SVELTE_IGNORE_PATTERN = /^\s*svelte-ignore/m
9+
10+
type IgnoreItem = {
11+
range: [number, number]
12+
code: string
13+
token: AST.Token | AST.Comment
14+
}
15+
16+
/** Checks whether given comment has missing code svelte-ignore */
17+
function hasMissingCodeIgnore(text: string) {
18+
const m1 = SVELTE_IGNORE_PATTERN.exec(text)
19+
if (!m1) {
20+
return false
21+
}
22+
const ignoreStart = m1.index + m1[0].length
23+
const beforeText = text.slice(ignoreStart)
24+
return !beforeText.trim()
25+
}
26+
27+
/** Extract svelte-ignore rule names */
28+
function extractSvelteIgnore(
29+
text: string,
30+
startIndex: number,
31+
token: AST.Token | AST.Comment,
32+
): IgnoreItem[] | null {
33+
const m1 = SVELTE_IGNORE_PATTERN.exec(text)
34+
if (!m1) {
35+
return null
36+
}
37+
const ignoreStart = m1.index + m1[0].length
38+
const beforeText = text.slice(ignoreStart)
39+
if (!/^\s/.test(beforeText) || !beforeText.trim()) {
40+
return null
41+
}
42+
let start = startIndex + ignoreStart
43+
44+
const results: IgnoreItem[] = []
45+
for (const code of beforeText.split(/\s/)) {
46+
const end = start + code.length
47+
const trimmed = code.trim()
48+
if (trimmed) {
49+
results.push({
50+
code: trimmed,
51+
range: [start, end],
52+
token,
53+
})
54+
}
55+
start = end + 1 /* space */
56+
}
57+
58+
return results
59+
}
60+
61+
export default createRule("no-unused-svelte-ignore", {
62+
meta: {
63+
docs: {
64+
description: "disallow unused svelte-ignore comments",
65+
category: "Best Practices",
66+
recommended: true,
67+
},
68+
schema: [],
69+
messages: {
70+
unused: "svelte-ignore comment is used, but not warned",
71+
missingCode: "svelte-ignore comment must include the code",
72+
},
73+
type: "suggestion",
74+
},
75+
76+
// eslint-disable-next-line complexity -- X(
77+
create(context) {
78+
const sourceCode = context.getSourceCode()
79+
80+
const ignoreComments: IgnoreItem[] = []
81+
const used = new Set<IgnoreItem>()
82+
for (const comment of sourceCode.getAllComments()) {
83+
const ignores = extractSvelteIgnore(
84+
comment.value,
85+
comment.range[0] + 2,
86+
comment,
87+
)
88+
if (ignores) {
89+
ignoreComments.push(...ignores)
90+
} else if (hasMissingCodeIgnore(comment.value)) {
91+
context.report({
92+
node: comment,
93+
messageId: "missingCode",
94+
})
95+
}
96+
}
97+
for (const token of sourceCode.ast.tokens) {
98+
if (token.type === "HTMLComment") {
99+
const text = token.value.slice(4, -3)
100+
const ignores = extractSvelteIgnore(text, token.range[0] + 4, token)
101+
if (ignores) {
102+
ignoreComments.push(...ignores)
103+
} else if (hasMissingCodeIgnore(text)) {
104+
context.report({
105+
node: token,
106+
messageId: "missingCode",
107+
})
108+
}
109+
}
110+
}
111+
ignoreComments.sort((a, b) => b.range[0] - a.range[0])
112+
113+
if (!ignoreComments.length) {
114+
return {}
115+
}
116+
for (const warning of getSvelteCompileWarnings(context, {
117+
removeComments: new Set(ignoreComments.map((i) => i.token)),
118+
})) {
119+
if (!warning.code) {
120+
continue
121+
}
122+
const node = getWarningNode(warning)
123+
if (!node) {
124+
continue
125+
}
126+
l: for (const comment of extractLeadingComments(node).reverse()) {
127+
for (const ignoreItem of ignoreComments) {
128+
if (
129+
ignoreItem.token === comment &&
130+
ignoreItem.code === warning.code
131+
) {
132+
used.add(ignoreItem)
133+
break l
134+
}
135+
}
136+
}
137+
}
138+
139+
for (const unused of ignoreComments.filter((i) => !used.has(i))) {
140+
context.report({
141+
loc: {
142+
start: sourceCode.getLocFromIndex(unused.range[0]),
143+
end: sourceCode.getLocFromIndex(unused.range[1]),
144+
},
145+
messageId: "unused",
146+
})
147+
}
148+
return {}
149+
150+
/** Get warning node */
151+
function getWarningNode(warning: Warning) {
152+
const index = getWarningIndex(warning)
153+
if (index == null) {
154+
return null
155+
}
156+
let targetNode = sourceCode.getNodeByRangeIndex(index)
157+
while (targetNode) {
158+
if (targetNode.type === "SvelteElement") {
159+
return targetNode
160+
}
161+
if (targetNode.parent) {
162+
if (
163+
targetNode.parent.type === "Program" ||
164+
targetNode.parent.type === "SvelteScriptElement"
165+
) {
166+
return targetNode
167+
}
168+
} else {
169+
return null
170+
}
171+
targetNode = targetNode.parent || null
172+
}
173+
174+
return null
175+
}
176+
177+
/** Get warning index */
178+
function getWarningIndex(warning: Warning) {
179+
const start = warning.start && sourceCode.getIndexFromLoc(warning.start)
180+
const end = warning.end && sourceCode.getIndexFromLoc(warning.end)
181+
if (start != null && end != null) {
182+
return Math.floor(start + (end - start) / 2)
183+
}
184+
return start ?? end
185+
}
186+
187+
/** Extract comments */
188+
function extractLeadingComments(node: ASTNodeWithParent) {
189+
const beforeToken = sourceCode.getTokenBefore(node, {
190+
includeComments: false,
191+
filter(token) {
192+
if (isOpeningParenToken(token)) {
193+
return false
194+
}
195+
const astToken = token as AST.Token
196+
if (astToken.type === "HTMLText") {
197+
return Boolean(astToken.value.trim())
198+
}
199+
return astToken.type !== "HTMLComment"
200+
},
201+
})
202+
if (beforeToken) {
203+
return sourceCode
204+
.getTokensBetween(beforeToken, node, { includeComments: true })
205+
.filter(isComment)
206+
}
207+
return sourceCode
208+
.getTokensBefore(node, { includeComments: true })
209+
.filter(isComment)
210+
}
211+
},
212+
})
213+
214+
/** Checks whether given token is comment token */
215+
function isComment(token: AST.Token | AST.Comment): boolean {
216+
return (
217+
token.type === "HTMLComment" ||
218+
token.type === "Block" ||
219+
token.type === "Line"
220+
)
221+
}

0 commit comments

Comments
 (0)