Skip to content

Commit 0321d4c

Browse files
authored
Add no-not-function-handler rule (#35)
* Add no-not-function-handler rule * add test
1 parent bed9624 commit 0321d4c

27 files changed

+342
-17
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
246246
| Rule ID | Description | |
247247
|:--------|:------------|:---|
248248
| [@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: |
249+
| [@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: |
249250
| [@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: |
250251

251252
## Security Vulnerability

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
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-not-function-handler](./no-not-function-handler.md) | disallow use of not function in event handler | :star: |
1920
| [@ota-meshi/svelte/no-object-in-text-mustaches](./no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
2021

2122
## Security Vulnerability

docs/rules/no-not-function-handler.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/no-not-function-handler"
5+
description: "disallow use of not function in event handler"
6+
---
7+
8+
# @ota-meshi/svelte/no-not-function-handler
9+
10+
> disallow use of not function in event handler
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 reports where you used not function value in event handlers.
18+
If you use a non-function value for the event handler, it event handler will not be called. It's almost always a mistake. You may have written a lot of unnecessary curly braces.
19+
20+
<eslint-code-block>
21+
22+
<!--eslint-skip-->
23+
24+
```svelte
25+
<script>
26+
/* eslint @ota-meshi/svelte/no-not-function-handler: "error" */
27+
function foo() {
28+
/* */
29+
}
30+
const bar = 42
31+
</script>
32+
33+
<!-- ✓ GOOD -->
34+
<button on:click={foo} />
35+
<button
36+
on:click={() => {
37+
/* */
38+
}}
39+
/>
40+
41+
<!-- ✗ BAD -->
42+
<button on:click={{ foo }} />
43+
<button on:click={bar} />
44+
```
45+
46+
</eslint-code-block>
47+
48+
## :wrench: Options
49+
50+
Nothing.
51+
52+
## :couple: Related Rules
53+
54+
- [@ota-meshi/svelte/no-object-in-text-mustaches]
55+
56+
[@ota-meshi/svelte/no-object-in-text-mustaches]: ./no-object-in-text-mustaches.md
57+
58+
## :mag: Implementation
59+
60+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-not-function-handler.ts)
61+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-not-function-handler.ts)

docs/rules/no-object-in-text-mustaches.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ When you use an object for text interpolation, it is drawn as `[object Object]`.
4242

4343
Nothing.
4444

45+
## :couple: Related Rules
46+
47+
- [@ota-meshi/svelte/no-invalid-handler]
48+
49+
[@ota-meshi/svelte/no-invalid-handler]: ./no-invalid-handler.md
50+
4551
## :mag: Implementation
4652

4753
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-object-in-text-mustaches.ts)

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export = {
1111
"@ota-meshi/svelte/no-at-html-tags": "error",
1212
"@ota-meshi/svelte/no-dupe-else-if-blocks": "error",
1313
"@ota-meshi/svelte/no-inner-declarations": "error",
14+
"@ota-meshi/svelte/no-not-function-handler": "error",
1415
"@ota-meshi/svelte/no-object-in-text-mustaches": "error",
1516
"@ota-meshi/svelte/system": "error",
1617
},

src/rules/indent-helpers/es.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ type NodeWithParent =
2121
| (Exclude<ESTree.Node, ESTree.Program> & { parent: ASTNode })
2222
| AST.SvelteProgram
2323
| AST.SvelteReactiveStatement
24-
type NodeListenerMap<T extends NodeWithParent = NodeWithParent> = {
25-
[key in NodeWithParent["type"]]: T extends { type: key } ? T : never
26-
}
2724

2825
type NodeListener = {
29-
[T in keyof NodeListenerMap]: (node: NodeListenerMap[T]) => void
26+
[key in NodeWithParent["type"]]: (
27+
node: NodeWithParent & { type: key },
28+
) => void
3029
}
3130

3231
/**

src/rules/indent-helpers/svelte.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,9 @@ type NodeWithoutES = Exclude<
88
AST.SvelteNode,
99
AST.SvelteProgram | AST.SvelteReactiveStatement
1010
>
11-
type NodeListenerMap<T extends NodeWithoutES = NodeWithoutES> = {
12-
[key in NodeWithoutES["type"]]: T extends { type: key } ? T : never
13-
}
1411

1512
type NodeListener = {
16-
[T in keyof NodeListenerMap]: (node: NodeListenerMap[T]) => void
13+
[key in NodeWithoutES["type"]]: (node: NodeWithoutES & { type: key }) => void
1714
}
1815
const PREFORMATTED_ELEMENT_NAMES = ["pre", "textarea"]
1916

src/rules/indent-helpers/ts.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,8 @@ type NodeWithoutES = Exclude<
3232
| TSESTree.JSXSpreadChild
3333
| TSESTree.JSXText
3434
>
35-
type NodeListenerMap<T extends NodeWithoutES = NodeWithoutES> = {
36-
[key in NodeWithoutES["type"]]: T extends { type: key } ? T : never
37-
}
3835
type NodeListener = {
39-
[T in keyof NodeListenerMap]: (node: NodeListenerMap[T]) => void
36+
[key in NodeWithoutES["type"]]: (node: NodeWithoutES & { type: key }) => void
4037
}
4138

4239
/**

src/rules/no-not-function-handler.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import type * as ESTree from "estree"
3+
import { createRule } from "../utils"
4+
import { findVariable } from "../utils/ast-utils"
5+
6+
const PHRASES = {
7+
ObjectExpression: "object",
8+
ArrayExpression: "array",
9+
ClassExpression: "class",
10+
Literal(node: ESTree.Literal): string | null {
11+
if ("regex" in node) {
12+
return "regex value"
13+
}
14+
if ("bigint" in node) {
15+
return "bigint value"
16+
}
17+
if (node.value == null) {
18+
return null
19+
}
20+
return `${typeof node.value} value`
21+
},
22+
TemplateLiteral: "string value",
23+
}
24+
export default createRule("no-not-function-handler", {
25+
meta: {
26+
docs: {
27+
description: "disallow use of not function in event handler",
28+
category: "Possible Errors",
29+
recommended: true,
30+
},
31+
schema: [],
32+
messages: {
33+
unexpected: "Unexpected {{phrase}} in event handler.",
34+
},
35+
type: "problem", // "problem", or "layout",
36+
},
37+
create(context) {
38+
/** Find data expression */
39+
function findRootExpression(
40+
node: ESTree.Expression,
41+
already = new Set<ESTree.Identifier>(),
42+
): ESTree.Expression {
43+
if (node.type !== "Identifier" || already.has(node)) {
44+
return node
45+
}
46+
already.add(node)
47+
const variable = findVariable(context, node)
48+
if (!variable || variable.defs.length !== 1) {
49+
return node
50+
}
51+
const def = variable.defs[0]
52+
if (def.type === "Variable") {
53+
if (def.parent.kind === "const" && def.node.init) {
54+
const init = def.node.init
55+
return findRootExpression(init, already)
56+
}
57+
}
58+
return node
59+
}
60+
61+
/** Verify for `on:` directive value */
62+
function verify(node: AST.SvelteEventHandlerDirective["expression"]) {
63+
if (!node) {
64+
return
65+
}
66+
const expression = findRootExpression(node)
67+
68+
if (
69+
expression.type !== "ObjectExpression" &&
70+
expression.type !== "ArrayExpression" &&
71+
expression.type !== "ClassExpression" &&
72+
expression.type !== "Literal" &&
73+
expression.type !== "TemplateLiteral"
74+
) {
75+
return
76+
}
77+
const phraseValue = PHRASES[expression.type]
78+
const phrase =
79+
typeof phraseValue === "function"
80+
? phraseValue(expression as never)
81+
: phraseValue
82+
if (phrase == null) {
83+
return
84+
}
85+
context.report({
86+
node,
87+
messageId: "unexpected",
88+
data: {
89+
phrase,
90+
},
91+
})
92+
}
93+
94+
return {
95+
SvelteDirective(node) {
96+
if (node.kind !== "EventHandler") {
97+
return
98+
}
99+
verify(node.expression)
100+
},
101+
}
102+
},
103+
})

src/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ export type ASTNode =
1616
type ASTNodeWithParent =
1717
| (Exclude<ASTNode, ESTree.Program> & { parent: ASTNode })
1818
| AST.SvelteProgram
19-
type ASTNodeListenerMap<T extends ASTNodeWithParent = ASTNodeWithParent> = {
20-
[key in ASTNodeWithParent["type"]]: T extends { type: key } ? T : never
21-
}
2219

2320
export type ASTNodeListener = {
24-
[T in keyof ASTNodeListenerMap]?: (node: ASTNodeListenerMap[T]) => void
21+
[key in ASTNodeWithParent["type"]]?: (
22+
node: ASTNodeWithParent & { type: key },
23+
) => void
2524
}
2625
export interface RuleListener extends ASTNodeListener {
2726
onCodePathStart?(codePath: Rule.CodePath, node: never): void

0 commit comments

Comments
 (0)