Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
49 changes: 49 additions & 0 deletions docs/rules/no-dynamic-slot-name.md
Original file line number Diff line number Diff line change
@@ -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: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
- :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 `<slot>` name.
Dynamic `<slot>` names are not allowed in Svelte, so you must use static names.

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.

<eslint-code-block fix>

<!--eslint-skip-->

```svelte
<script>
/* eslint @ota-meshi/svelte/no-dynamic-slot-name: "error" */
const SLOT_NAME = "bad"
</script>

<!-- ✓ GOOD -->
<slot name="good" />

<!-- ✗ BAD -->
<slot name={SLOT_NAME} />
```

</eslint-code-block>

## :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)
2 changes: 1 addition & 1 deletion docs/rules/valid-compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This rule uses Svelte compiler to check the source code.

</eslint-code-block>

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

Expand Down
1 change: 1 addition & 0 deletions src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
66 changes: 7 additions & 59 deletions src/rules/html-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '"',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -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],
Expand All @@ -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"(
Expand Down
95 changes: 95 additions & 0 deletions src/rules/no-dynamic-slot-name.ts
Original file line number Diff line number Diff line change
@@ -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: "`<slot>` name cannot be dynamic.",
requireValue: "`<slot>` 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.Identifier>(),
): 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
}
},
})
2 changes: 1 addition & 1 deletion src/rules/valid-compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading