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

src/utils/ast-utils.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { ASTNode, SourceCode } from "../types"
1+
import type { ASTNode, RuleContext, SourceCode } from "../types"
22
import type * as ESTree from "estree"
33
import type { AST as SvAST } from "svelte-eslint-parser"
4+
import * as eslintUtils from "eslint-utils"
5+
import type { Scope } from "eslint"
46

57
/**
68
* Checks whether or not the tokens of two given nodes are same.
@@ -191,3 +193,39 @@ export function getStaticAttributeValue(
191193
}
192194
return str
193195
}
196+
197+
/**
198+
* Find the variable of a given name.
199+
*/
200+
export function findVariable(
201+
context: RuleContext,
202+
node: ESTree.Identifier,
203+
): Scope.Variable | null {
204+
return eslintUtils.findVariable(getScope(context, node), node)
205+
}
206+
207+
/**
208+
* Gets the scope for the current node
209+
*/
210+
export function getScope(
211+
context: RuleContext,
212+
currentNode: ESTree.Node,
213+
): Scope.Scope {
214+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
215+
const scopeManager = (context.getSourceCode() as any).scopeManager
216+
217+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
218+
let node: any = currentNode
219+
for (; node; node = node.parent || null) {
220+
const scope = scopeManager.acquire(node, false)
221+
222+
if (scope) {
223+
if (scope.type === "function-expression-name") {
224+
return scope.childScopes[0]
225+
}
226+
return scope
227+
}
228+
}
229+
230+
return scopeManager.scopes[0]
231+
}

src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import noAtDebugTags from "../rules/no-at-debug-tags"
88
import noAtHtmlTags from "../rules/no-at-html-tags"
99
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
1010
import noInnerDeclarations from "../rules/no-inner-declarations"
11+
import noNotFunctionHandler from "../rules/no-not-function-handler"
1112
import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
1213
import noTargetBlank from "../rules/no-target-blank"
1314
import noUselessMustaches from "../rules/no-useless-mustaches"
@@ -26,6 +27,7 @@ export const rules = [
2627
noAtHtmlTags,
2728
noDupeElseIfBlocks,
2829
noInnerDeclarations,
30+
noNotFunctionHandler,
2931
noObjectInTextMustaches,
3032
noTargetBlank,
3133
noUselessMustaches,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Unexpected array in event handler.",
4+
"line": 5,
5+
"column": 19
6+
}
7+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const a = "hello!"
3+
</script>
4+
5+
<button on:click={[a]} />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Unexpected class in event handler.",
4+
"line": 4,
5+
"column": 19
6+
}
7+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<script>
2+
</script>
3+
4+
<button on:click={class B {}} />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Unexpected object in event handler.",
4+
"line": 5,
5+
"column": 19
6+
}
7+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
function fn() {}
3+
</script>
4+
5+
<button on:click={{ fn }} />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Unexpected string value in event handler.",
4+
"line": 6,
5+
"column": 19
6+
},
7+
{
8+
"message": "Unexpected string value in event handler.",
9+
"line": 7,
10+
"column": 19
11+
}
12+
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
const a = "hello!"
3+
const b = `${a} world`
4+
</script>
5+
6+
<button on:click={a} />
7+
<button on:click={b} />

0 commit comments

Comments
 (0)