Skip to content

feat: add svelte/no-dupe-on-directives and svelte/no-dupe-use-directives rules #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2022
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
5 changes: 5 additions & 0 deletions .changeset/little-points-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-svelte": minor
---

feat: add `svelte/no-dupe-use-directives` rule
5 changes: 5 additions & 0 deletions .changeset/pink-zoos-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-svelte": minor
---

feat: add `svelte/no-dupe-on-directives` rule
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ These rules relate to possible syntax or logic errors in Svelte code:
|:--------|:------------|:---|
| [svelte/no-dom-manipulating](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dom-manipulating/) | disallow DOM manipulating | |
| [svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks/) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
| [svelte/no-dupe-on-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-on-directives/) | disallow duplicate `on:` directives | |
| [svelte/no-dupe-style-properties](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-style-properties/) | disallow duplicate style properties | :star: |
| [svelte/no-dupe-use-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-use-directives/) | disallow duplicate `use:` directives | |
| [svelte/no-dynamic-slot-name](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dynamic-slot-name/) | disallow dynamic slot name | :star::wrench: |
| [svelte/no-export-load-in-svelte-module-in-kit-pages](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-export-load-in-svelte-module-in-kit-pages/) | disallow exporting load functions in `*.svelte` module in Svelte Kit page components. | |
| [svelte/no-not-function-handler](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-not-function-handler/) | disallow use of not function in event handler | :star: |
Expand Down
2 changes: 2 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ These rules relate to possible syntax or logic errors in Svelte code:
|:--------|:------------|:---|
| [svelte/no-dom-manipulating](./rules/no-dom-manipulating.md) | disallow DOM manipulating | |
| [svelte/no-dupe-else-if-blocks](./rules/no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
| [svelte/no-dupe-on-directives](./rules/no-dupe-on-directives.md) | disallow duplicate `on:` directives | |
| [svelte/no-dupe-style-properties](./rules/no-dupe-style-properties.md) | disallow duplicate style properties | :star: |
| [svelte/no-dupe-use-directives](./rules/no-dupe-use-directives.md) | disallow duplicate `use:` directives | |
| [svelte/no-dynamic-slot-name](./rules/no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
| [svelte/no-export-load-in-svelte-module-in-kit-pages](./rules/no-export-load-in-svelte-module-in-kit-pages.md) | disallow exporting load functions in `*.svelte` module in Svelte Kit page components. | |
| [svelte/no-not-function-handler](./rules/no-not-function-handler.md) | disallow use of not function in event handler | :star: |
Expand Down
53 changes: 53 additions & 0 deletions docs/rules/no-dupe-on-directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "svelte/no-dupe-on-directives"
description: "disallow duplicate `on:` directives"
---

# svelte/no-dupe-on-directives

> disallow duplicate `on:` directives

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>

## :book: Rule Details

We can define any number of `on:` directive with the same event name, but duplicate directives with the exact same event name and expression are probably a mistake.
This rule reports reports `on:` directives with exactly the same event name and expression.

<ESLintCodeBlock>

<!--eslint-skip-->

```svelte
<script>
/* eslint svelte/no-dupe-on-directives: "error" */
</script>

<!-- ✓ GOOD -->
<button on:click on:click={myHandler} />
<button on:click={foo} on:click={bar} />

<!-- ✗ BAD -->
<button on:click on:click />
<button on:click={myHandler} on:click={myHandler} />

<input
on:focus|once
on:focus
on:keydown={() => console.log("foo")}
on:keydown={() => console.log("foo")}
/>
```

</ESLintCodeBlock>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-on-directives.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-on-directives.ts)
46 changes: 46 additions & 0 deletions docs/rules/no-dupe-use-directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "svelte/no-dupe-use-directives"
description: "disallow duplicate `use:` directives"
---

# svelte/no-dupe-use-directives

> disallow duplicate `use:` directives

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>

## :book: Rule Details

We can define any number of `use:` directive with the same action, but duplicate directives with the exact same action and expression are probably a mistake.
This rule reports reports `use:` directives with exactly the same action and expression.

<ESLintCodeBlock>

<!--eslint-skip-->

```svelte
<script>
/* eslint svelte/no-dupe-use-directives: "error" */
</script>

<!-- ✓ GOOD -->
<div use:clickOutside use:clickOutside={param} />
<div use:clickOutside={foo} use:clickOutside={bar} />

<!-- ✗ BAD -->
<div use:clickOutside use:clickOutside />
<div use:clickOutside={param} use:clickOutside={param} />
```

</ESLintCodeBlock>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dupe-use-directives.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dupe-use-directives.ts)
84 changes: 84 additions & 0 deletions src/rules/no-dupe-on-directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { AST } from "svelte-eslint-parser"
import type { TSESTree } from "@typescript-eslint/types"
import { createRule } from "../utils"
import { equalTokens } from "../utils/ast-utils"

export default createRule("no-dupe-on-directives", {
meta: {
docs: {
description: "disallow duplicate `on:` directives",
category: "Possible Errors",
recommended: false,
},
schema: [],
messages: {
duplication:
"This `on:{{type}}` directive is the same and duplicate directives in L{{lineNo}}.",
},
type: "problem",
},
create(context) {
const sourceCode = context.getSourceCode()

const directiveDataMap = new Map<
string, // event type
{
expression: null | TSESTree.Expression
nodes: AST.SvelteEventHandlerDirective[]
}[]
>()
return {
SvelteDirective(node) {
if (node.kind !== "EventHandler") return

const directiveDataList = directiveDataMap.get(node.key.name.name)
if (!directiveDataList) {
directiveDataMap.set(node.key.name.name, [
{
expression: node.expression,
nodes: [node],
},
])
return
}
const directiveData = directiveDataList.find((data) => {
if (!data.expression || !node.expression) {
return data.expression === node.expression
}
return equalTokens(data.expression, node.expression, sourceCode)
})
if (!directiveData) {
directiveDataList.push({
expression: node.expression,
nodes: [node],
})
return
}

directiveData.nodes.push(node)
},
"SvelteStartTag:exit"() {
for (const [type, directiveDataList] of directiveDataMap) {
for (const { nodes } of directiveDataList) {
if (nodes.length < 2) {
continue
}
for (const node of nodes) {
context.report({
node,
messageId: "duplication",
data: {
type,
lineNo: String(
(nodes[0] !== node ? nodes[0] : nodes[1]).loc.start.line,
),
},
})
}
}
}
directiveDataMap.clear()
},
}
},
})
86 changes: 86 additions & 0 deletions src/rules/no-dupe-use-directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { AST } from "svelte-eslint-parser"
import type { TSESTree } from "@typescript-eslint/types"
import { createRule } from "../utils"
import { equalTokens, getAttributeKeyText } from "../utils/ast-utils"

export default createRule("no-dupe-use-directives", {
meta: {
docs: {
description: "disallow duplicate `use:` directives",
category: "Possible Errors",
recommended: false,
},
schema: [],
messages: {
duplication:
"This `{{keyText}}` directive is the same and duplicate directives in L{{lineNo}}.",
},
type: "problem",
},
create(context) {
const sourceCode = context.getSourceCode()

const directiveDataMap = new Map<
string, // key text
{
expression: null | TSESTree.Expression
nodes: AST.SvelteActionDirective[]
}[]
>()
return {
SvelteDirective(node) {
if (node.kind !== "Action") return

const keyText = getAttributeKeyText(node, context)

const directiveDataList = directiveDataMap.get(keyText)
if (!directiveDataList) {
directiveDataMap.set(keyText, [
{
expression: node.expression,
nodes: [node],
},
])
return
}
const directiveData = directiveDataList.find((data) => {
if (!data.expression || !node.expression) {
return data.expression === node.expression
}
return equalTokens(data.expression, node.expression, sourceCode)
})
if (!directiveData) {
directiveDataList.push({
expression: node.expression,
nodes: [node],
})
return
}

directiveData.nodes.push(node)
},
"SvelteStartTag:exit"() {
for (const [keyText, directiveDataList] of directiveDataMap) {
for (const { nodes } of directiveDataList) {
if (nodes.length < 2) {
continue
}
for (const node of nodes) {
context.report({
node,
messageId: "duplication",
data: {
keyText,
lineNo: String(
(nodes[0] !== node ? nodes[0] : nodes[1]).loc.start.line,
),
},
})
}
}
}
directiveDataMap.clear()
},
}
},
})
6 changes: 0 additions & 6 deletions src/types-for-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,6 @@ export type ASTNodeListener = {
node: TSESTree.TSReadonlyKeyword & ASTNodeWithParent,
) => void
TSRestType?: (node: TSESTree.TSRestType & ASTNodeWithParent) => void
TSSatisfiesExpression?: (
node: TSESTree.TSSatisfiesExpression & ASTNodeWithParent,
) => void
TSStaticKeyword?: (node: TSESTree.TSStaticKeyword & ASTNodeWithParent) => void
TSStringKeyword?: (node: TSESTree.TSStringKeyword & ASTNodeWithParent) => void
TSSymbolKeyword?: (node: TSESTree.TSSymbolKeyword & ASTNodeWithParent) => void
Expand Down Expand Up @@ -666,9 +663,6 @@ export type TSNodeListener = {
node: TSESTree.TSReadonlyKeyword & ASTNodeWithParent,
) => void
TSRestType?: (node: TSESTree.TSRestType & ASTNodeWithParent) => void
TSSatisfiesExpression?: (
node: TSESTree.TSSatisfiesExpression & ASTNodeWithParent,
) => void
TSStaticKeyword?: (node: TSESTree.TSStaticKeyword & ASTNodeWithParent) => void
TSStringKeyword?: (node: TSESTree.TSStringKeyword & ASTNodeWithParent) => void
TSSymbolKeyword?: (node: TSESTree.TSSymbolKeyword & ASTNodeWithParent) => void
Expand Down
4 changes: 4 additions & 0 deletions src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import noAtDebugTags from "../rules/no-at-debug-tags"
import noAtHtmlTags from "../rules/no-at-html-tags"
import noDomManipulating from "../rules/no-dom-manipulating"
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
import noDupeOnDirectives from "../rules/no-dupe-on-directives"
import noDupeStyleProperties from "../rules/no-dupe-style-properties"
import noDupeUseDirectives from "../rules/no-dupe-use-directives"
import noDynamicSlotName from "../rules/no-dynamic-slot-name"
import noExportLoadInSvelteModuleInKitPages from "../rules/no-export-load-in-svelte-module-in-kit-pages"
import noExtraReactiveCurlies from "../rules/no-extra-reactive-curlies"
Expand Down Expand Up @@ -62,7 +64,9 @@ export const rules = [
noAtHtmlTags,
noDomManipulating,
noDupeElseIfBlocks,
noDupeOnDirectives,
noDupeStyleProperties,
noDupeUseDirectives,
noDynamicSlotName,
noExportLoadInSvelteModuleInKitPages,
noExtraReactiveCurlies,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- message: This `on:keydown` directive is the same and duplicate directives in L3.
line: 2
column: 3
suggestions: null
- message: This `on:keydown` directive is the same and duplicate directives in L2.
line: 3
column: 3
suggestions: null
- message: This `on:keydown` directive is the same and duplicate directives in L10.
line: 7
column: 3
suggestions: null
- message: This `on:keydown` directive is the same and duplicate directives in L7.
line: 10
column: 3
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<button
on:keydown={() => console.log("foo")}
on:keydown={() => console.log("foo")}
/>

<button
on:keydown={() =>
// foo
console.log("foo")}
on:keydown={() =>
console
// bar
.log("foo")}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
- message: This `on:click` directive is the same and duplicate directives in L3.
line: 2
column: 3
suggestions: null
- message: This `on:click` directive is the same and duplicate directives in L2.
line: 3
column: 3
suggestions: null
- message: This `on:click` directive is the same and duplicate directives in L2.
line: 4
column: 3
suggestions: null
- message: This `on:click` directive is the same and duplicate directives in L2.
line: 5
column: 3
suggestions: null
- message: This `on:click` directive is the same and duplicate directives in L2.
line: 6
column: 3
suggestions: null
Loading