diff --git a/README.md b/README.md
index 72f0bf788..3614ffdec 100644
--- a/README.md
+++ b/README.md
@@ -248,6 +248,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| Rule ID | Description | |
|:--------|:------------|:---|
| [@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: |
+| [@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: |
| [@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: |
| [@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: |
| [@ota-meshi/svelte/valid-compile](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-compile.html) | disallow warnings when compiling. | :star: |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 7bf67423b..46e95ce3d 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -16,6 +16,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| Rule ID | Description | |
|:--------|:------------|:---|
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
+| [@ota-meshi/svelte/no-dynamic-slot-name](./no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
| [@ota-meshi/svelte/no-not-function-handler](./no-not-function-handler.md) | disallow use of not function in event handler | :star: |
| [@ota-meshi/svelte/no-object-in-text-mustaches](./no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
| [@ota-meshi/svelte/valid-compile](./valid-compile.md) | disallow warnings when compiling. | :star: |
diff --git a/docs/rules/no-dynamic-slot-name.md b/docs/rules/no-dynamic-slot-name.md
new file mode 100644
index 000000000..9cf15d212
--- /dev/null
+++ b/docs/rules/no-dynamic-slot-name.md
@@ -0,0 +1,49 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "@ota-meshi/svelte/no-dynamic-slot-name"
+description: "disallow dynamic slot name"
+---
+
+# @ota-meshi/svelte/no-dynamic-slot-name
+
+> disallow dynamic slot name
+
+- :exclamation: **_This rule has not been released yet._**
+- :gear: This rule is included in `"plugin:@ota-meshi/svelte/recommended"`.
+- :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.
+
+## :book: Rule Details
+
+This rule reports the dynamically specified `` name.
+Dynamic `` names are not allowed in Svelte, so you must use static names.
+
+The auto-fix of this rule can be replaced with a static `` name if the expression given to the `` name is static and resolvable.
+
+
+
+
+
+```svelte
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dynamic-slot-name.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dynamic-slot-name.ts)
diff --git a/docs/rules/valid-compile.md b/docs/rules/valid-compile.md
index b403a908b..5eb17bcba 100644
--- a/docs/rules/valid-compile.md
+++ b/docs/rules/valid-compile.md
@@ -35,7 +35,7 @@ This rule uses Svelte compiler to check the source code.
-Note that we exclude reports for some checks, such as `missing-declaration`, which you can check with different ESLint rules.
+Note that we exclude reports for some checks, such as `missing-declaration`, and `dynamic-slot-name`, which you can check with different ESLint rules.
## :wrench: Options
diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts
index c4db9c76e..03c499b30 100644
--- a/src/configs/recommended.ts
+++ b/src/configs/recommended.ts
@@ -10,6 +10,7 @@ export = {
"@ota-meshi/svelte/no-at-debug-tags": "warn",
"@ota-meshi/svelte/no-at-html-tags": "error",
"@ota-meshi/svelte/no-dupe-else-if-blocks": "error",
+ "@ota-meshi/svelte/no-dynamic-slot-name": "error",
"@ota-meshi/svelte/no-inner-declarations": "error",
"@ota-meshi/svelte/no-not-function-handler": "error",
"@ota-meshi/svelte/no-object-in-text-mustaches": "error",
diff --git a/src/rules/html-quotes.ts b/src/rules/html-quotes.ts
index 6d0559966..34019a5c6 100644
--- a/src/rules/html-quotes.ts
+++ b/src/rules/html-quotes.ts
@@ -5,8 +5,9 @@ import {
isNotOpeningBraceToken,
isOpeningParenToken,
} from "eslint-utils"
-import type { NodeOrToken } from "../types"
import { createRule } from "../utils"
+import type { QuoteAndRange } from "../utils/ast-utils"
+import { getAttributeValueQuoteAndRange } from "../utils/ast-utils"
const QUOTE_CHARS = {
double: '"',
@@ -61,56 +62,6 @@ export default createRule("html-quotes", {
context.options[0]?.dynamic?.avoidInvalidUnquotedInHTML,
)
- type QuoteAndRange = {
- quote: "unquoted" | "double" | "single"
- range: [number, number]
- }
-
- /** Get the quote and range from given attribute values */
- function getQuoteAndRange(
- attr:
- | AST.SvelteAttribute
- | AST.SvelteDirective
- | AST.SvelteSpecialDirective,
- valueTokens: NodeOrToken[],
- ): QuoteAndRange | null {
- const valueFirstToken = valueTokens[0]
- const valueLastToken = valueTokens[valueTokens.length - 1]
- const eqToken = sourceCode.getTokenAfter(attr.key)
- if (
- !eqToken ||
- eqToken.value !== "=" ||
- valueFirstToken.range![0] < eqToken.range[1]
- ) {
- // invalid
- return null
- }
- const beforeTokens = sourceCode.getTokensBetween(eqToken, valueFirstToken)
- if (beforeTokens.length === 0) {
- return {
- quote: "unquoted",
- range: [valueFirstToken.range![0], valueLastToken.range![1]],
- }
- } else if (
- beforeTokens.length > 1 ||
- (beforeTokens[0].value !== '"' && beforeTokens[0].value !== "'")
- ) {
- // invalid
- return null
- }
- const beforeToken = beforeTokens[0]
- const afterToken = sourceCode.getTokenAfter(valueLastToken)
- if (!afterToken || afterToken.value !== beforeToken.value) {
- // invalid
- return null
- }
-
- return {
- quote: beforeToken.value === '"' ? "double" : "single",
- range: [beforeToken.range[0], afterToken.range[1]],
- }
- }
-
/** Checks whether the given text can remove quotes in HTML. */
function canBeUnquotedInHTML(text: string) {
return !/[\s"'<=>`]/u.test(text)
@@ -202,11 +153,8 @@ export default createRule("html-quotes", {
}
/** Verify for standard attribute */
- function verifyForValues(
- attr: AST.SvelteAttribute,
- valueNodes: AST.SvelteAttribute["value"],
- ) {
- const quoteAndRange = getQuoteAndRange(attr, valueNodes)
+ function verifyForValues(attr: AST.SvelteAttribute) {
+ const quoteAndRange = getAttributeValueQuoteAndRange(attr, context)
verifyQuote(preferQuote, quoteAndRange)
}
@@ -217,7 +165,7 @@ export default createRule("html-quotes", {
kind: "text"
},
) {
- const quoteAndRange = getQuoteAndRange(attr, [valueNode])
+ const quoteAndRange = getAttributeValueQuoteAndRange(attr, context)
const text = sourceCode.text.slice(...valueNode.range)
verifyQuote(
avoidInvalidUnquotedInHTML && !canBeUnquotedInHTML(text)
@@ -251,7 +199,7 @@ export default createRule("html-quotes", {
) {
return
}
- const quoteAndRange = getQuoteAndRange(attr, [beforeToken, afterToken])
+ const quoteAndRange = getAttributeValueQuoteAndRange(attr, context)
const text = sourceCode.text.slice(
beforeToken.range[0],
afterToken.range[1],
@@ -272,7 +220,7 @@ export default createRule("html-quotes", {
) {
verifyForDynamicMustacheTag(node, node.value[0])
} else if (node.value.length >= 1) {
- verifyForValues(node, node.value)
+ verifyForValues(node)
}
},
"SvelteDirective, SvelteSpecialDirective"(
diff --git a/src/rules/no-dynamic-slot-name.ts b/src/rules/no-dynamic-slot-name.ts
new file mode 100644
index 000000000..ac08d71b0
--- /dev/null
+++ b/src/rules/no-dynamic-slot-name.ts
@@ -0,0 +1,95 @@
+import type { AST } from "svelte-eslint-parser"
+import type * as ESTree from "estree"
+import { createRule } from "../utils"
+import {
+ findVariable,
+ getAttributeValueQuoteAndRange,
+ getStringIfConstant,
+} from "../utils/ast-utils"
+
+export default createRule("no-dynamic-slot-name", {
+ meta: {
+ docs: {
+ description: "disallow dynamic slot name",
+ category: "Possible Errors",
+ recommended: true,
+ },
+ fixable: "code",
+ schema: [],
+ messages: {
+ unexpected: "`` name cannot be dynamic.",
+ requireValue: "`` name requires a value.",
+ },
+ type: "problem",
+ },
+ create(context) {
+ return {
+ "SvelteElement[name.name='slot'] > SvelteStartTag.startTag > SvelteAttribute[key.name='name']"(
+ node: AST.SvelteAttribute,
+ ) {
+ if (node.value.length === 0) {
+ context.report({
+ node,
+ messageId: "requireValue",
+ })
+ return
+ }
+ for (const vNode of node.value) {
+ if (vNode.type === "SvelteMustacheTag") {
+ context.report({
+ node: vNode,
+ messageId: "unexpected",
+ fix(fixer) {
+ const text = getStaticText(vNode.expression)
+ if (text == null) {
+ return null
+ }
+
+ if (node.value.length === 1) {
+ const range = getAttributeValueQuoteAndRange(
+ node,
+ context,
+ )!.range
+ return fixer.replaceTextRange(range, `"${text}"`)
+ }
+ const range = vNode.range
+ return fixer.replaceTextRange(range, text)
+ },
+ })
+ }
+ }
+ },
+ }
+
+ /**
+ * Get static text from given expression
+ */
+ function getStaticText(node: ESTree.Expression) {
+ const expr = findRootExpression(node)
+ return getStringIfConstant(expr)
+ }
+
+ /** Find data expression */
+ function findRootExpression(
+ node: ESTree.Expression,
+ already = new Set(),
+ ): ESTree.Expression {
+ if (node.type !== "Identifier" || already.has(node)) {
+ return node
+ }
+ already.add(node)
+ const variable = findVariable(context, node)
+ if (!variable || variable.defs.length !== 1) {
+ return node
+ }
+ const def = variable.defs[0]
+ if (def.type === "Variable") {
+ if (def.parent.kind === "const" && def.node.init) {
+ const init = def.node.init
+ return findRootExpression(init, already)
+ }
+ }
+ return node
+ }
+ },
+})
diff --git a/src/rules/valid-compile.ts b/src/rules/valid-compile.ts
index 201a72d32..237b79eb2 100644
--- a/src/rules/valid-compile.ts
+++ b/src/rules/valid-compile.ts
@@ -87,7 +87,7 @@ export default createRule("valid-compile", {
const sourceCode = context.getSourceCode()
const text = sourceCode.text
- const ignores = ["missing-declaration"]
+ const ignores = ["missing-declaration", "dynamic-slot-name"]
/**
* report
diff --git a/src/utils/ast-utils.ts b/src/utils/ast-utils.ts
index 770995e23..969f3a6c4 100644
--- a/src/utils/ast-utils.ts
+++ b/src/utils/ast-utils.ts
@@ -229,3 +229,111 @@ export function getScope(
return scopeManager.scopes[0]
}
+
+export type QuoteAndRange = {
+ quote: "unquoted" | "double" | "single"
+ range: [number, number]
+}
+/** Get the quote and range from given attribute values */
+export function getAttributeValueQuoteAndRange(
+ attr:
+ | SvAST.SvelteAttribute
+ | SvAST.SvelteDirective
+ | SvAST.SvelteSpecialDirective,
+ context: RuleContext,
+): QuoteAndRange | null {
+ const sourceCode = context.getSourceCode()
+ const valueTokens = getAttributeValueRangeTokens(attr, sourceCode)
+ if (valueTokens == null) {
+ return null
+ }
+ const { firstToken: valueFirstToken, lastToken: valueLastToken } = valueTokens
+ const eqToken = sourceCode.getTokenAfter(attr.key)
+ if (
+ !eqToken ||
+ eqToken.value !== "=" ||
+ valueFirstToken.range[0] < eqToken.range[1]
+ ) {
+ // invalid
+ return null
+ }
+ const beforeTokens = sourceCode.getTokensBetween(eqToken, valueFirstToken)
+ if (beforeTokens.length === 0) {
+ return {
+ quote: "unquoted",
+ range: [valueFirstToken.range[0], valueLastToken.range[1]],
+ }
+ } else if (
+ beforeTokens.length > 1 ||
+ (beforeTokens[0].value !== '"' && beforeTokens[0].value !== "'")
+ ) {
+ // invalid
+ return null
+ }
+ const beforeToken = beforeTokens[0]
+ const afterToken = sourceCode.getTokenAfter(valueLastToken)
+ if (!afterToken || afterToken.value !== beforeToken.value) {
+ // invalid
+ return null
+ }
+
+ return {
+ quote: beforeToken.value === '"' ? "double" : "single",
+ range: [beforeToken.range[0], afterToken.range[1]],
+ }
+}
+
+/** Get the value tokens from given attribute */
+function getAttributeValueRangeTokens(
+ attr:
+ | SvAST.SvelteAttribute
+ | SvAST.SvelteDirective
+ | SvAST.SvelteSpecialDirective,
+ sourceCode: SourceCode,
+) {
+ if (attr.type === "SvelteAttribute") {
+ if (!attr.value.length) {
+ return null
+ }
+ const firstToken = attr.value[0]
+ const lastToken = attr.value[attr.value.length - 1]
+ return {
+ firstToken,
+ lastToken,
+ }
+ }
+ let firstToken, lastToken
+ if (attr.expression == null) {
+ return null
+ }
+ if (
+ attr.key.range[0] <= attr.expression.range![0] &&
+ attr.expression.range![1] <= attr.key.range[1]
+ ) {
+ // shorthand
+ return null
+ }
+ firstToken = sourceCode.getTokenBefore(attr.expression)
+ lastToken = sourceCode.getTokenAfter(attr.expression)
+ while (
+ firstToken &&
+ lastToken &&
+ eslintUtils.isOpeningParenToken(firstToken) &&
+ eslintUtils.isClosingParenToken(lastToken)
+ ) {
+ firstToken = sourceCode.getTokenBefore(firstToken)
+ lastToken = sourceCode.getTokenAfter(lastToken)
+ }
+ if (
+ !firstToken ||
+ !lastToken ||
+ eslintUtils.isNotOpeningBraceToken(firstToken) ||
+ eslintUtils.isNotClosingBraceToken(lastToken)
+ ) {
+ return null
+ }
+ return {
+ firstToken,
+ lastToken,
+ }
+}
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index e9d71effd..d34fe0690 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -8,6 +8,7 @@ import maxAttributesPerLine from "../rules/max-attributes-per-line"
import noAtDebugTags from "../rules/no-at-debug-tags"
import noAtHtmlTags from "../rules/no-at-html-tags"
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
+import noDynamicSlotName from "../rules/no-dynamic-slot-name"
import noInnerDeclarations from "../rules/no-inner-declarations"
import noNotFunctionHandler from "../rules/no-not-function-handler"
import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
@@ -29,6 +30,7 @@ export const rules = [
noAtDebugTags,
noAtHtmlTags,
noDupeElseIfBlocks,
+ noDynamicSlotName,
noInnerDeclarations,
noNotFunctionHandler,
noObjectInTextMustaches,
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-errors.json b/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-errors.json
new file mode 100644
index 000000000..2a7cd69e2
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "`` name requires a value.",
+ "line": 1,
+ "column": 7
+ }
+]
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-input.svelte b/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-input.svelte
new file mode 100644
index 000000000..39acf5953
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-input.svelte
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-output.svelte b/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-output.svelte
new file mode 100644
index 000000000..39acf5953
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/bool-slot01-output.svelte
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-errors.json b/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-errors.json
new file mode 100644
index 000000000..3c677e18a
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "`` name cannot be dynamic.",
+ "line": 1,
+ "column": 12
+ }
+]
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-input.svelte b/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-input.svelte
new file mode 100644
index 000000000..d57fb9b28
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-input.svelte
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-output.svelte b/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-output.svelte
new file mode 100644
index 000000000..a25a9ab8f
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/literal-slot01-output.svelte
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-errors.json b/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-errors.json
new file mode 100644
index 000000000..8b5b1a12a
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "`` name cannot be dynamic.",
+ "line": 5,
+ "column": 12
+ }
+]
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-input.svelte b/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-input.svelte
new file mode 100644
index 000000000..87e1a41aa
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-input.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-output.svelte b/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-output.svelte
new file mode 100644
index 000000000..30e1906d3
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/invalid/var-slot01-output.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/fixtures/rules/no-dynamic-slot-name/valid/slot01-input.svelte b/tests/fixtures/rules/no-dynamic-slot-name/valid/slot01-input.svelte
new file mode 100644
index 000000000..a25a9ab8f
--- /dev/null
+++ b/tests/fixtures/rules/no-dynamic-slot-name/valid/slot01-input.svelte
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/rules/valid-compile/invalid/dyamic-slot01-errors.json b/tests/fixtures/rules/valid-compile/invalid/dyamic-slot01-errors.json
deleted file mode 100644
index 59bf566cd..000000000
--- a/tests/fixtures/rules/valid-compile/invalid/dyamic-slot01-errors.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
- {
- "message": " name cannot be dynamic(dynamic-slot-name)",
- "line": 5,
- "column": 7
- }
-]
diff --git a/tests/fixtures/rules/valid-compile/invalid/dyamic-slot01-input.svelte b/tests/fixtures/rules/valid-compile/invalid/dyamic-slot01-input.svelte
deleted file mode 100644
index ea5ab5077..000000000
--- a/tests/fixtures/rules/valid-compile/invalid/dyamic-slot01-input.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/tests/fixtures/rules/valid-compile/valid/dyamic-slot01-input.svelte b/tests/fixtures/rules/valid-compile/valid/dyamic-slot01-input.svelte
new file mode 100644
index 000000000..87e1a41aa
--- /dev/null
+++ b/tests/fixtures/rules/valid-compile/valid/dyamic-slot01-input.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/src/rules/no-dynamic-slot-name.ts b/tests/src/rules/no-dynamic-slot-name.ts
new file mode 100644
index 000000000..bf6e81df7
--- /dev/null
+++ b/tests/src/rules/no-dynamic-slot-name.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-dynamic-slot-name"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "no-dynamic-slot-name",
+ rule as any,
+ loadTestCases("no-dynamic-slot-name"),
+)