diff --git a/README.md b/README.md
index fb31df855..c511f7f07 100644
--- a/README.md
+++ b/README.md
@@ -246,6 +246,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-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: |
## Security Vulnerability
diff --git a/docs/rules/README.md b/docs/rules/README.md
index d3baf4bdc..6a2d2cb02 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-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: |
## Security Vulnerability
diff --git a/docs/rules/no-not-function-handler.md b/docs/rules/no-not-function-handler.md
new file mode 100644
index 000000000..99b977e3c
--- /dev/null
+++ b/docs/rules/no-not-function-handler.md
@@ -0,0 +1,61 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "@ota-meshi/svelte/no-not-function-handler"
+description: "disallow use of not function in event handler"
+---
+
+# @ota-meshi/svelte/no-not-function-handler
+
+> disallow use of not function in event handler
+
+- :exclamation: **_This rule has not been released yet._**
+- :gear: This rule is included in `"plugin:@ota-meshi/svelte/recommended"`.
+
+## :book: Rule Details
+
+This rule reports where you used not function value in event handlers.
+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.
+
+
+
+
+
+```svelte
+
+
+
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [@ota-meshi/svelte/no-object-in-text-mustaches]
+
+[@ota-meshi/svelte/no-object-in-text-mustaches]: ./no-object-in-text-mustaches.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-not-function-handler.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-not-function-handler.ts)
diff --git a/docs/rules/no-object-in-text-mustaches.md b/docs/rules/no-object-in-text-mustaches.md
index 8cd8f0c6a..1a5dde235 100644
--- a/docs/rules/no-object-in-text-mustaches.md
+++ b/docs/rules/no-object-in-text-mustaches.md
@@ -42,6 +42,12 @@ When you use an object for text interpolation, it is drawn as `[object Object]`.
Nothing.
+## :couple: Related Rules
+
+- [@ota-meshi/svelte/no-invalid-handler]
+
+[@ota-meshi/svelte/no-invalid-handler]: ./no-invalid-handler.md
+
## :mag: Implementation
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-object-in-text-mustaches.ts)
diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts
index 870a83f21..f466ee787 100644
--- a/src/configs/recommended.ts
+++ b/src/configs/recommended.ts
@@ -11,6 +11,7 @@ export = {
"@ota-meshi/svelte/no-at-html-tags": "error",
"@ota-meshi/svelte/no-dupe-else-if-blocks": "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",
"@ota-meshi/svelte/system": "error",
},
diff --git a/src/rules/indent-helpers/es.ts b/src/rules/indent-helpers/es.ts
index 4f2dcf99f..d8dd87f04 100644
--- a/src/rules/indent-helpers/es.ts
+++ b/src/rules/indent-helpers/es.ts
@@ -21,12 +21,11 @@ type NodeWithParent =
| (Exclude & { parent: ASTNode })
| AST.SvelteProgram
| AST.SvelteReactiveStatement
-type NodeListenerMap = {
- [key in NodeWithParent["type"]]: T extends { type: key } ? T : never
-}
type NodeListener = {
- [T in keyof NodeListenerMap]: (node: NodeListenerMap[T]) => void
+ [key in NodeWithParent["type"]]: (
+ node: NodeWithParent & { type: key },
+ ) => void
}
/**
diff --git a/src/rules/indent-helpers/svelte.ts b/src/rules/indent-helpers/svelte.ts
index cdfcdac99..3d7afcd8f 100644
--- a/src/rules/indent-helpers/svelte.ts
+++ b/src/rules/indent-helpers/svelte.ts
@@ -8,12 +8,9 @@ type NodeWithoutES = Exclude<
AST.SvelteNode,
AST.SvelteProgram | AST.SvelteReactiveStatement
>
-type NodeListenerMap = {
- [key in NodeWithoutES["type"]]: T extends { type: key } ? T : never
-}
type NodeListener = {
- [T in keyof NodeListenerMap]: (node: NodeListenerMap[T]) => void
+ [key in NodeWithoutES["type"]]: (node: NodeWithoutES & { type: key }) => void
}
const PREFORMATTED_ELEMENT_NAMES = ["pre", "textarea"]
diff --git a/src/rules/indent-helpers/ts.ts b/src/rules/indent-helpers/ts.ts
index 7939b2703..fcd494bab 100644
--- a/src/rules/indent-helpers/ts.ts
+++ b/src/rules/indent-helpers/ts.ts
@@ -32,11 +32,8 @@ type NodeWithoutES = Exclude<
| TSESTree.JSXSpreadChild
| TSESTree.JSXText
>
-type NodeListenerMap = {
- [key in NodeWithoutES["type"]]: T extends { type: key } ? T : never
-}
type NodeListener = {
- [T in keyof NodeListenerMap]: (node: NodeListenerMap[T]) => void
+ [key in NodeWithoutES["type"]]: (node: NodeWithoutES & { type: key }) => void
}
/**
diff --git a/src/rules/no-not-function-handler.ts b/src/rules/no-not-function-handler.ts
new file mode 100644
index 000000000..94936588c
--- /dev/null
+++ b/src/rules/no-not-function-handler.ts
@@ -0,0 +1,103 @@
+import type { AST } from "svelte-eslint-parser"
+import type * as ESTree from "estree"
+import { createRule } from "../utils"
+import { findVariable } from "../utils/ast-utils"
+
+const PHRASES = {
+ ObjectExpression: "object",
+ ArrayExpression: "array",
+ ClassExpression: "class",
+ Literal(node: ESTree.Literal): string | null {
+ if ("regex" in node) {
+ return "regex value"
+ }
+ if ("bigint" in node) {
+ return "bigint value"
+ }
+ if (node.value == null) {
+ return null
+ }
+ return `${typeof node.value} value`
+ },
+ TemplateLiteral: "string value",
+}
+export default createRule("no-not-function-handler", {
+ meta: {
+ docs: {
+ description: "disallow use of not function in event handler",
+ category: "Possible Errors",
+ recommended: true,
+ },
+ schema: [],
+ messages: {
+ unexpected: "Unexpected {{phrase}} in event handler.",
+ },
+ type: "problem", // "problem", or "layout",
+ },
+ create(context) {
+ /** 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
+ }
+
+ /** Verify for `on:` directive value */
+ function verify(node: AST.SvelteEventHandlerDirective["expression"]) {
+ if (!node) {
+ return
+ }
+ const expression = findRootExpression(node)
+
+ if (
+ expression.type !== "ObjectExpression" &&
+ expression.type !== "ArrayExpression" &&
+ expression.type !== "ClassExpression" &&
+ expression.type !== "Literal" &&
+ expression.type !== "TemplateLiteral"
+ ) {
+ return
+ }
+ const phraseValue = PHRASES[expression.type]
+ const phrase =
+ typeof phraseValue === "function"
+ ? phraseValue(expression as never)
+ : phraseValue
+ if (phrase == null) {
+ return
+ }
+ context.report({
+ node,
+ messageId: "unexpected",
+ data: {
+ phrase,
+ },
+ })
+ }
+
+ return {
+ SvelteDirective(node) {
+ if (node.kind !== "EventHandler") {
+ return
+ }
+ verify(node.expression)
+ },
+ }
+ },
+})
diff --git a/src/types.ts b/src/types.ts
index 33d1a83c6..75fd389bc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -16,12 +16,11 @@ export type ASTNode =
type ASTNodeWithParent =
| (Exclude & { parent: ASTNode })
| AST.SvelteProgram
-type ASTNodeListenerMap = {
- [key in ASTNodeWithParent["type"]]: T extends { type: key } ? T : never
-}
export type ASTNodeListener = {
- [T in keyof ASTNodeListenerMap]?: (node: ASTNodeListenerMap[T]) => void
+ [key in ASTNodeWithParent["type"]]?: (
+ node: ASTNodeWithParent & { type: key },
+ ) => void
}
export interface RuleListener extends ASTNodeListener {
onCodePathStart?(codePath: Rule.CodePath, node: never): void
diff --git a/src/utils/ast-utils.ts b/src/utils/ast-utils.ts
index 1cfa94873..770995e23 100644
--- a/src/utils/ast-utils.ts
+++ b/src/utils/ast-utils.ts
@@ -1,6 +1,8 @@
-import type { ASTNode, SourceCode } from "../types"
+import type { ASTNode, RuleContext, SourceCode } from "../types"
import type * as ESTree from "estree"
import type { AST as SvAST } from "svelte-eslint-parser"
+import * as eslintUtils from "eslint-utils"
+import type { Scope } from "eslint"
/**
* Checks whether or not the tokens of two given nodes are same.
@@ -191,3 +193,39 @@ export function getStaticAttributeValue(
}
return str
}
+
+/**
+ * Find the variable of a given name.
+ */
+export function findVariable(
+ context: RuleContext,
+ node: ESTree.Identifier,
+): Scope.Variable | null {
+ return eslintUtils.findVariable(getScope(context, node), node)
+}
+
+/**
+ * Gets the scope for the current node
+ */
+export function getScope(
+ context: RuleContext,
+ currentNode: ESTree.Node,
+): Scope.Scope {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
+ const scopeManager = (context.getSourceCode() as any).scopeManager
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
+ let node: any = currentNode
+ for (; node; node = node.parent || null) {
+ const scope = scopeManager.acquire(node, false)
+
+ if (scope) {
+ if (scope.type === "function-expression-name") {
+ return scope.childScopes[0]
+ }
+ return scope
+ }
+ }
+
+ return scopeManager.scopes[0]
+}
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 4ec015b87..a9fa6e79b 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -8,6 +8,7 @@ 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 noInnerDeclarations from "../rules/no-inner-declarations"
+import noNotFunctionHandler from "../rules/no-not-function-handler"
import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
import noTargetBlank from "../rules/no-target-blank"
import noUselessMustaches from "../rules/no-useless-mustaches"
@@ -26,6 +27,7 @@ export const rules = [
noAtHtmlTags,
noDupeElseIfBlocks,
noInnerDeclarations,
+ noNotFunctionHandler,
noObjectInTextMustaches,
noTargetBlank,
noUselessMustaches,
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/array01-errors.json b/tests/fixtures/rules/no-not-function-handler/invalid/array01-errors.json
new file mode 100644
index 000000000..a2406b698
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/array01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "Unexpected array in event handler.",
+ "line": 5,
+ "column": 19
+ }
+]
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/array01-input.svelte b/tests/fixtures/rules/no-not-function-handler/invalid/array01-input.svelte
new file mode 100644
index 000000000..d6374bf1c
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/array01-input.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/class01-errors.json b/tests/fixtures/rules/no-not-function-handler/invalid/class01-errors.json
new file mode 100644
index 000000000..d2d13e902
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/class01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "Unexpected class in event handler.",
+ "line": 4,
+ "column": 19
+ }
+]
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/class01-input.svelte b/tests/fixtures/rules/no-not-function-handler/invalid/class01-input.svelte
new file mode 100644
index 000000000..edb41ed1e
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/class01-input.svelte
@@ -0,0 +1,4 @@
+
+
+
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/object01-errors.json b/tests/fixtures/rules/no-not-function-handler/invalid/object01-errors.json
new file mode 100644
index 000000000..4dae8a793
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/object01-errors.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "Unexpected object in event handler.",
+ "line": 5,
+ "column": 19
+ }
+]
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/object01-input.svelte b/tests/fixtures/rules/no-not-function-handler/invalid/object01-input.svelte
new file mode 100644
index 000000000..09ef9b95f
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/object01-input.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/string01-errors.json b/tests/fixtures/rules/no-not-function-handler/invalid/string01-errors.json
new file mode 100644
index 000000000..66595af9c
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/string01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Unexpected string value in event handler.",
+ "line": 6,
+ "column": 19
+ },
+ {
+ "message": "Unexpected string value in event handler.",
+ "line": 7,
+ "column": 19
+ }
+]
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/string01-input.svelte b/tests/fixtures/rules/no-not-function-handler/invalid/string01-input.svelte
new file mode 100644
index 000000000..563cedfaa
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/string01-input.svelte
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/value01-errors.json b/tests/fixtures/rules/no-not-function-handler/invalid/value01-errors.json
new file mode 100644
index 000000000..f783fb68f
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/value01-errors.json
@@ -0,0 +1,17 @@
+[
+ {
+ "message": "Unexpected number value in event handler.",
+ "line": 7,
+ "column": 19
+ },
+ {
+ "message": "Unexpected bigint value in event handler.",
+ "line": 8,
+ "column": 19
+ },
+ {
+ "message": "Unexpected regex value in event handler.",
+ "line": 9,
+ "column": 19
+ }
+]
diff --git a/tests/fixtures/rules/no-not-function-handler/invalid/value01-input.svelte b/tests/fixtures/rules/no-not-function-handler/invalid/value01-input.svelte
new file mode 100644
index 000000000..fdbf9c556
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/invalid/value01-input.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/tests/fixtures/rules/no-not-function-handler/valid/bind01-input.svelte b/tests/fixtures/rules/no-not-function-handler/valid/bind01-input.svelte
new file mode 100644
index 000000000..e6c2db22d
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/valid/bind01-input.svelte
@@ -0,0 +1,4 @@
+
+
+
diff --git a/tests/fixtures/rules/no-not-function-handler/valid/function01-input.svelte b/tests/fixtures/rules/no-not-function-handler/valid/function01-input.svelte
new file mode 100644
index 000000000..046c7b144
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/valid/function01-input.svelte
@@ -0,0 +1,7 @@
+
+
+ a} />
+
diff --git a/tests/fixtures/rules/no-not-function-handler/valid/null01-input.svelte b/tests/fixtures/rules/no-not-function-handler/valid/null01-input.svelte
new file mode 100644
index 000000000..6732bc586
--- /dev/null
+++ b/tests/fixtures/rules/no-not-function-handler/valid/null01-input.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/tests/src/rules/no-not-function-handler.ts b/tests/src/rules/no-not-function-handler.ts
new file mode 100644
index 000000000..3de1bfa86
--- /dev/null
+++ b/tests/src/rules/no-not-function-handler.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-not-function-handler"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "no-not-function-handler",
+ rule as any,
+ loadTestCases("no-not-function-handler"),
+)
diff --git a/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts
index 63c2a768b..5a4112cdc 100644
--- a/typings/eslint-utils/index.d.ts
+++ b/typings/eslint-utils/index.d.ts
@@ -1,4 +1,6 @@
import type { AST } from "svelte-eslint-parser"
+import type { Scope } from "eslint"
+import type * as ESTree from "estree"
type Token = { type: string; value: string }
export function isArrowToken(token: Token): boolean
export function isCommaToken(token: Token): boolean
@@ -22,3 +24,8 @@ export function isNotClosingBracketToken(token: Token): boolean
export function isNotOpeningBraceToken(token: Token): boolean
export function isNotClosingBraceToken(token: Token): boolean
export function isNotCommentToken(token: Token): boolean
+
+export function findVariable(
+ initialScope: Scope.Scope,
+ nameOrNode: ESTree.Identifier | string,
+): Scope.Variable