Skip to content

Commit 6797041

Browse files
authored
Add no-dynamic-slot-name rule (#75)
1 parent 566318c commit 6797041

24 files changed

+323
-73
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
248248
| Rule ID | Description | |
249249
|:--------|:------------|:---|
250250
| [@ota-meshi/svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks.html) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
251+
| [@ota-meshi/svelte/no-dynamic-slot-name](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dynamic-slot-name.html) | disallow dynamic slot name | :star::wrench: |
251252
| [@ota-meshi/svelte/no-not-function-handler](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-not-function-handler.html) | disallow use of not function in event handler | :star: |
252253
| [@ota-meshi/svelte/no-object-in-text-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-object-in-text-mustaches.html) | disallow objects in text mustache interpolation | :star: |
253254
| [@ota-meshi/svelte/valid-compile](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-compile.html) | disallow warnings when compiling. | :star: |

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
1616
| Rule ID | Description | |
1717
|:--------|:------------|:---|
1818
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
19+
| [@ota-meshi/svelte/no-dynamic-slot-name](./no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
1920
| [@ota-meshi/svelte/no-not-function-handler](./no-not-function-handler.md) | disallow use of not function in event handler | :star: |
2021
| [@ota-meshi/svelte/no-object-in-text-mustaches](./no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
2122
| [@ota-meshi/svelte/valid-compile](./valid-compile.md) | disallow warnings when compiling. | :star: |

docs/rules/no-dynamic-slot-name.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/no-dynamic-slot-name"
5+
description: "disallow dynamic slot name"
6+
---
7+
8+
# @ota-meshi/svelte/no-dynamic-slot-name
9+
10+
> disallow dynamic slot name
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+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
15+
16+
## :book: Rule Details
17+
18+
This rule reports the dynamically specified `<slot>` name.
19+
Dynamic `<slot>` names are not allowed in Svelte, so you must use static names.
20+
21+
The auto-fix of this rule can be replaced with a static `<slot>` name if the expression given to the `<slot>` name is static and resolvable.
22+
23+
<eslint-code-block fix>
24+
25+
<!--eslint-skip-->
26+
27+
```svelte
28+
<script>
29+
/* eslint @ota-meshi/svelte/no-dynamic-slot-name: "error" */
30+
const SLOT_NAME = "bad"
31+
</script>
32+
33+
<!-- ✓ GOOD -->
34+
<slot name="good" />
35+
36+
<!-- ✗ BAD -->
37+
<slot name={SLOT_NAME} />
38+
```
39+
40+
</eslint-code-block>
41+
42+
## :wrench: Options
43+
44+
Nothing.
45+
46+
## :mag: Implementation
47+
48+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dynamic-slot-name.ts)
49+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dynamic-slot-name.ts)

docs/rules/valid-compile.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ This rule uses Svelte compiler to check the source code.
3535

3636
</eslint-code-block>
3737

38-
Note that we exclude reports for some checks, such as `missing-declaration`, which you can check with different ESLint rules.
38+
Note that we exclude reports for some checks, such as `missing-declaration`, and `dynamic-slot-name`, which you can check with different ESLint rules.
3939

4040
## :wrench: Options
4141

src/configs/recommended.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export = {
1010
"@ota-meshi/svelte/no-at-debug-tags": "warn",
1111
"@ota-meshi/svelte/no-at-html-tags": "error",
1212
"@ota-meshi/svelte/no-dupe-else-if-blocks": "error",
13+
"@ota-meshi/svelte/no-dynamic-slot-name": "error",
1314
"@ota-meshi/svelte/no-inner-declarations": "error",
1415
"@ota-meshi/svelte/no-not-function-handler": "error",
1516
"@ota-meshi/svelte/no-object-in-text-mustaches": "error",

src/rules/html-quotes.ts

+7-59
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
isNotOpeningBraceToken,
66
isOpeningParenToken,
77
} from "eslint-utils"
8-
import type { NodeOrToken } from "../types"
98
import { createRule } from "../utils"
9+
import type { QuoteAndRange } from "../utils/ast-utils"
10+
import { getAttributeValueQuoteAndRange } from "../utils/ast-utils"
1011

1112
const QUOTE_CHARS = {
1213
double: '"',
@@ -61,56 +62,6 @@ export default createRule("html-quotes", {
6162
context.options[0]?.dynamic?.avoidInvalidUnquotedInHTML,
6263
)
6364

64-
type QuoteAndRange = {
65-
quote: "unquoted" | "double" | "single"
66-
range: [number, number]
67-
}
68-
69-
/** Get the quote and range from given attribute values */
70-
function getQuoteAndRange(
71-
attr:
72-
| AST.SvelteAttribute
73-
| AST.SvelteDirective
74-
| AST.SvelteSpecialDirective,
75-
valueTokens: NodeOrToken[],
76-
): QuoteAndRange | null {
77-
const valueFirstToken = valueTokens[0]
78-
const valueLastToken = valueTokens[valueTokens.length - 1]
79-
const eqToken = sourceCode.getTokenAfter(attr.key)
80-
if (
81-
!eqToken ||
82-
eqToken.value !== "=" ||
83-
valueFirstToken.range![0] < eqToken.range[1]
84-
) {
85-
// invalid
86-
return null
87-
}
88-
const beforeTokens = sourceCode.getTokensBetween(eqToken, valueFirstToken)
89-
if (beforeTokens.length === 0) {
90-
return {
91-
quote: "unquoted",
92-
range: [valueFirstToken.range![0], valueLastToken.range![1]],
93-
}
94-
} else if (
95-
beforeTokens.length > 1 ||
96-
(beforeTokens[0].value !== '"' && beforeTokens[0].value !== "'")
97-
) {
98-
// invalid
99-
return null
100-
}
101-
const beforeToken = beforeTokens[0]
102-
const afterToken = sourceCode.getTokenAfter(valueLastToken)
103-
if (!afterToken || afterToken.value !== beforeToken.value) {
104-
// invalid
105-
return null
106-
}
107-
108-
return {
109-
quote: beforeToken.value === '"' ? "double" : "single",
110-
range: [beforeToken.range[0], afterToken.range[1]],
111-
}
112-
}
113-
11465
/** Checks whether the given text can remove quotes in HTML. */
11566
function canBeUnquotedInHTML(text: string) {
11667
return !/[\s"'<=>`]/u.test(text)
@@ -202,11 +153,8 @@ export default createRule("html-quotes", {
202153
}
203154

204155
/** Verify for standard attribute */
205-
function verifyForValues(
206-
attr: AST.SvelteAttribute,
207-
valueNodes: AST.SvelteAttribute["value"],
208-
) {
209-
const quoteAndRange = getQuoteAndRange(attr, valueNodes)
156+
function verifyForValues(attr: AST.SvelteAttribute) {
157+
const quoteAndRange = getAttributeValueQuoteAndRange(attr, context)
210158
verifyQuote(preferQuote, quoteAndRange)
211159
}
212160

@@ -217,7 +165,7 @@ export default createRule("html-quotes", {
217165
kind: "text"
218166
},
219167
) {
220-
const quoteAndRange = getQuoteAndRange(attr, [valueNode])
168+
const quoteAndRange = getAttributeValueQuoteAndRange(attr, context)
221169
const text = sourceCode.text.slice(...valueNode.range)
222170
verifyQuote(
223171
avoidInvalidUnquotedInHTML && !canBeUnquotedInHTML(text)
@@ -251,7 +199,7 @@ export default createRule("html-quotes", {
251199
) {
252200
return
253201
}
254-
const quoteAndRange = getQuoteAndRange(attr, [beforeToken, afterToken])
202+
const quoteAndRange = getAttributeValueQuoteAndRange(attr, context)
255203
const text = sourceCode.text.slice(
256204
beforeToken.range[0],
257205
afterToken.range[1],
@@ -272,7 +220,7 @@ export default createRule("html-quotes", {
272220
) {
273221
verifyForDynamicMustacheTag(node, node.value[0])
274222
} else if (node.value.length >= 1) {
275-
verifyForValues(node, node.value)
223+
verifyForValues(node)
276224
}
277225
},
278226
"SvelteDirective, SvelteSpecialDirective"(

src/rules/no-dynamic-slot-name.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import type * as ESTree from "estree"
3+
import { createRule } from "../utils"
4+
import {
5+
findVariable,
6+
getAttributeValueQuoteAndRange,
7+
getStringIfConstant,
8+
} from "../utils/ast-utils"
9+
10+
export default createRule("no-dynamic-slot-name", {
11+
meta: {
12+
docs: {
13+
description: "disallow dynamic slot name",
14+
category: "Possible Errors",
15+
recommended: true,
16+
},
17+
fixable: "code",
18+
schema: [],
19+
messages: {
20+
unexpected: "`<slot>` name cannot be dynamic.",
21+
requireValue: "`<slot>` name requires a value.",
22+
},
23+
type: "problem",
24+
},
25+
create(context) {
26+
return {
27+
"SvelteElement[name.name='slot'] > SvelteStartTag.startTag > SvelteAttribute[key.name='name']"(
28+
node: AST.SvelteAttribute,
29+
) {
30+
if (node.value.length === 0) {
31+
context.report({
32+
node,
33+
messageId: "requireValue",
34+
})
35+
return
36+
}
37+
for (const vNode of node.value) {
38+
if (vNode.type === "SvelteMustacheTag") {
39+
context.report({
40+
node: vNode,
41+
messageId: "unexpected",
42+
fix(fixer) {
43+
const text = getStaticText(vNode.expression)
44+
if (text == null) {
45+
return null
46+
}
47+
48+
if (node.value.length === 1) {
49+
const range = getAttributeValueQuoteAndRange(
50+
node,
51+
context,
52+
)!.range
53+
return fixer.replaceTextRange(range, `"${text}"`)
54+
}
55+
const range = vNode.range
56+
return fixer.replaceTextRange(range, text)
57+
},
58+
})
59+
}
60+
}
61+
},
62+
}
63+
64+
/**
65+
* Get static text from given expression
66+
*/
67+
function getStaticText(node: ESTree.Expression) {
68+
const expr = findRootExpression(node)
69+
return getStringIfConstant(expr)
70+
}
71+
72+
/** Find data expression */
73+
function findRootExpression(
74+
node: ESTree.Expression,
75+
already = new Set<ESTree.Identifier>(),
76+
): ESTree.Expression {
77+
if (node.type !== "Identifier" || already.has(node)) {
78+
return node
79+
}
80+
already.add(node)
81+
const variable = findVariable(context, node)
82+
if (!variable || variable.defs.length !== 1) {
83+
return node
84+
}
85+
const def = variable.defs[0]
86+
if (def.type === "Variable") {
87+
if (def.parent.kind === "const" && def.node.init) {
88+
const init = def.node.init
89+
return findRootExpression(init, already)
90+
}
91+
}
92+
return node
93+
}
94+
},
95+
})

src/rules/valid-compile.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export default createRule("valid-compile", {
8787
const sourceCode = context.getSourceCode()
8888
const text = sourceCode.text
8989

90-
const ignores = ["missing-declaration"]
90+
const ignores = ["missing-declaration", "dynamic-slot-name"]
9191

9292
/**
9393
* report

0 commit comments

Comments
 (0)