Repository: vuejs/eslint-plugin-vue
Pull request #2009 (2022-10-12 04:50:22)

Add `vue/v-on-handler-style` rule and deprecate `vue/v-on-function-call` rule
This PR adds `vue/v-on-handler-style` rule.
And deprecates `vue/v-on-function-call` rule.

close https://github.com/vuejs/eslint-plugin-vue/issues/2001

ota-meshi: Thank you for your review.
I made a change to this PR other than ignoreIncludesComment.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index f0bd7578..f704c1a6 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -264,7 +264,7 @@ For example:
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  | :hammer: |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
 | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
-| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: | :hammer: |
+| [vue/v-on-handler-style](./v-on-handler-style.md) | enforce writing style for handlers in `v-on` directives | :wrench: | :hammer: |
 
 </rules-table>
 
@@ -324,6 +324,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 |:--------|:------------|
 | [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | [vue/valid-model-definition](./valid-model-definition.md) |
 | [vue/script-setup-uses-vars](./script-setup-uses-vars.md) | (no replacement) |
+| [vue/v-on-function-call](./v-on-function-call.md) | [vue/v-on-handler-style](./v-on-handler-style.md) |
 
 ## Removed
 
diff --git a/docs/rules/v-on-event-hyphenation.md b/docs/rules/v-on-event-hyphenation.md
index 6a21408c..9fdd8082 100644
--- a/docs/rules/v-on-event-hyphenation.md
+++ b/docs/rules/v-on-event-hyphenation.md
@@ -107,6 +107,7 @@ Don't use hyphenated name but allow custom event names
 
 - [vue/custom-event-name-casing](./custom-event-name-casing.md)
 - [vue/attribute-hyphenation](./attribute-hyphenation.md)
+- [vue/v-on-handler-style](./v-on-handler-style.md)
 
 ## :books: Further Reading
 
diff --git a/docs/rules/v-on-function-call.md b/docs/rules/v-on-function-call.md
index edd48f08..c80dafc6 100644
--- a/docs/rules/v-on-function-call.md
+++ b/docs/rules/v-on-function-call.md
@@ -9,6 +9,7 @@ since: v5.2.0
 
 > enforce or forbid parentheses after method calls without arguments in `v-on` directives
 
+- :warning: This rule was **deprecated** and replaced by [vue/v-on-handler-style](v-on-handler-style.md) rule.
 - :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
diff --git a/docs/rules/v-on-handler-style.md b/docs/rules/v-on-handler-style.md
new file mode 100644
index 00000000..18494e10
--- /dev/null
+++ b/docs/rules/v-on-handler-style.md
@@ -0,0 +1,219 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/v-on-handler-style
+description: enforce writing style for handlers in `v-on` directives
+---
+# vue/v-on-handler-style
+
+> enforce writing style for handlers in `v-on` directives
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :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 aims to enforce a consistent style in `v-on` event handlers:
+
+```vue
+<!-- Method handler: -->
+<button v-on:click="handler" />
+
+<!-- Inline handler: -->
+<button v-on:click="handler()" />
+
+<!-- Inline function: -->
+<button v-on:click="() => handler()" />
+```
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="handler" />
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler()" />
+  <button v-on:click="() => handler()" />
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/v-on-handler-style": ["error",
+    ["method", "inline-function"], // ["method", "inline-function"] | ["method", "inline"] | "inline-function" | "inline"
+    {
+      "ignoreIncludesComment": false
+    }
+  ]
+}
+```
+
+- First option ... Specifies the name of an allowed style. Default is `["method", "inline-function"]`.
+  - `["method", "inline-function"]` ... Allow handlers by method binding. e.g. `v-on:click="handler"`. Allow inline functions where method handlers cannot be used. e.g. `v-on:click="() => handler(listItem)"`.
+  - `["method", "inline"]` ... Allow handlers by method binding. e.g. `v-on:click="handler"`. Allow inline handlers where method handlers cannot be used. e.g. `v-on:click="handler(listItem)"`.
+  - `"inline-function"` ... Allow inline functions. e.g. `v-on:click="() => handler()"`
+  - `"inline"` ... Allow inline handlers. e.g. `v-on:click="handler()"`
+- Second option
+  - `ignoreIncludesComment` ... If `true`, do not report inline handlers or inline functions containing comments, even if the preferred style is `"method"`. Default is `false`.
+
+### `["method", "inline-function"]` (Default)
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error', ['method', 'inline-function']]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="handler" />
+  <template v-for="e in list">
+    <button v-on:click="e" />
+    <button v-on:click="() => handler(e)" />
+  </template>
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler()" />
+  <button v-on:click="count++" />
+  <button v-on:click="() => handler()" />
+  <button v-on:click="() => count++" />
+  <button v-on:click="(a, b) => handler(a, b)" />
+  <template v-for="e in list">
+    <button v-on:click="e()" />
+    <button v-on:click="() => e()" />
+    <button v-on:click="handler(e)" />
+  </template>
+</template>
+```
+
+</eslint-code-block>
+
+### `["method", "inline"]`
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error', ['method', 'inline']]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="handler" />
+  <template v-for="e in list">
+    <button v-on:click="e" />
+    <button v-on:click="handler(e)" />
+  </template>
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler()" />
+  <button v-on:click="count++" />
+  <button v-on:click="() => handler()" />
+  <button v-on:click="() => count++" />
+  <button v-on:click="(a, b) => handler(a, b)" />
+  <template v-for="e in list">
+    <button v-on:click="e()" />
+    <button v-on:click="() => e()" />
+    <button v-on:click="() => handler(e)" />
+  </template>
+</template>
+```
+
+</eslint-code-block>
+
+### `["inline-function"]`
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error', ['inline-function']]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="() => handler()" />
+  <button v-on:click="() => count++" />
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler" />
+  <button v-on:click="handler()" />
+  <button v-on:click="handler($event)" />
+  <button v-on:click="count++" />
+</template>
+```
+
+</eslint-code-block>
+
+### `["inline"]`
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error', ['inline']]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="handler()" />
+  <button v-on:click="handler($event)" />
+  <button v-on:click="count++" />
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler" />
+  <button v-on:click="() => handler()" />
+  <button v-on:click="(foo) => handler(foo)" />
+  <button v-on:click="(foo, bar) => handler(foo, bar)" />
+  <button v-on:click="() => count++" />
+</template>
+```
+
+</eslint-code-block>
+
+### `["method", "inline-function"], { "ignoreIncludesComment": true }`
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error', ['method', 'inline-function'], {ignoreIncludesComment: true}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="handler" />
+  <button v-on:click="() => handler() /* comment */" />
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler()" />
+  <button v-on:click="() => handler()" />
+  <button v-on:click="handler() /* comment */" />
+</template>
+```
+
+</eslint-code-block>
+
+### `["method", "inline"], { "ignoreIncludesComment": true }`
+
+<eslint-code-block fix :rules="{'vue/v-on-handler-style': ['error', ['method', 'inline'], {ignoreIncludesComment: true}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button v-on:click="handler" />
+  <button v-on:click="handler() /* comment */" />
+
+  <!-- ✗ BAD -->
+  <button v-on:click="handler()" />
+  <button v-on:click="() => handler()" />
+  <button v-on:click="() => handler() /* comment */" />
+</template>
+```
+
+</eslint-code-block>
+
+## :couple: Related Rules
+
+- [vue/v-on-style](./v-on-style.md)
+- [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md)
+
+## :books: Further Reading
+
+- [Guide - Inline Handlers]
+- [Guide - Method Handlers]
+
+[Guide - Inline Handlers]: https://vuejs.org/guide/essentials/event-handling.html#inline-handlers
+[Guide - Method Handlers]: https://vuejs.org/guide/essentials/event-handling.html#method-handlers
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-on-handler-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-on-handler-style.js)
diff --git a/docs/rules/v-on-style.md b/docs/rules/v-on-style.md
index 73e36ce3..89c135fc 100644
--- a/docs/rules/v-on-style.md
+++ b/docs/rules/v-on-style.md
@@ -59,6 +59,10 @@ Default is set to `shorthand`.
 
 </eslint-code-block>
 
+## :couple: Related Rules
+
+- [vue/v-on-handler-style](./v-on-handler-style.md)
+
 ## :books: Further Reading
 
 - [Style guide - Directive shorthands](https://vuejs.org/style-guide/rules-strongly-recommended.html#directive-shorthands)
diff --git a/lib/index.js b/lib/index.js
index 645669c4..6f5452cc 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -200,6 +200,7 @@ module.exports = {
     'v-for-delimiter-style': require('./rules/v-for-delimiter-style'),
     'v-on-event-hyphenation': require('./rules/v-on-event-hyphenation'),
     'v-on-function-call': require('./rules/v-on-function-call'),
+    'v-on-handler-style': require('./rules/v-on-handler-style'),
     'v-on-style': require('./rules/v-on-style'),
     'v-slot-style': require('./rules/v-slot-style'),
     'valid-attribute-name': require('./rules/valid-attribute-name'),
diff --git a/lib/rules/v-on-function-call.js b/lib/rules/v-on-function-call.js
index b6274d12..e2409794 100644
--- a/lib/rules/v-on-function-call.js
+++ b/lib/rules/v-on-function-call.js
@@ -82,7 +82,9 @@ module.exports = {
         },
         additionalProperties: false
       }
-    ]
+    ],
+    deprecated: true,
+    replacedBy: ['v-on-handler-style']
   },
   /** @param {RuleContext} context */
   create(context) {
diff --git a/lib/rules/v-on-handler-style.js b/lib/rules/v-on-handler-style.js
new file mode 100644
index 00000000..aa1214e3
--- /dev/null
+++ b/lib/rules/v-on-handler-style.js
@@ -0,0 +1,578 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/**
+ * @typedef {import('eslint').ReportDescriptorFix} ReportDescriptorFix
+ * @typedef {'method' | 'inline' | 'inline-function'} HandlerKind
+ * @typedef {object} ObjectOption
+ * @property {boolean} [ignoreIncludesComment]
+ */
+
+/**
+ * @param {RuleContext} context
+ */
+function parseOptions(context) {
+  /** @type {[HandlerKind | HandlerKind[] | undefined, ObjectOption | undefined]} */
+  const options = /** @type {any} */ (context.options)
+  /** @type {HandlerKind[]} */
+  const allows = []
+  if (options[0]) {
+    if (Array.isArray(options[0])) {
+      allows.push(...options[0])
+    } else {
+      allows.push(options[0])
+    }
+  } else {
+    allows.push('method', 'inline-function')
+  }
+
+  const option = options[1] || {}
+  const ignoreIncludesComment = !!option.ignoreIncludesComment
+
+  return { allows, ignoreIncludesComment }
+}
+
+/**
+ * Check whether the given token is a quote.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a quote.
+ */
+function isQuote(token) {
+  return (
+    token != null &&
+    token.type === 'Punctuator' &&
+    (token.value === '"' || token.value === "'")
+  )
+}
+/**
+ * Check whether the given node is an identifier call expression. e.g. `foo()`
+ * @param {Expression} node The node to check.
+ * @returns {node is CallExpression & {callee: Identifier}}
+ */
+function isIdentifierCallExpression(node) {
+  if (node.type !== 'CallExpression') {
+    return false
+  }
+  if (node.optional) {
+    // optional chaining
+    return false
+  }
+  const callee = node.callee
+  return callee.type === 'Identifier'
+}
+
+/**
+ * Returns a call expression node if the given VOnExpression or BlockStatement consists
+ * of only a single identifier call expression.
+ * e.g.
+ *   @click="foo()"
+ *   @click="{ foo() }"
+ *   @click="foo();;"
+ * @param {VOnExpression | BlockStatement} node
+ * @returns {CallExpression & {callee: Identifier} | null}
+ */
+function getIdentifierCallExpression(node) {
+  /** @type {ExpressionStatement} */
+  let exprStatement
+  let body = node.body
+  while (true) {
+    const statements = body.filter((st) => st.type !== 'EmptyStatement')
+    if (statements.length !== 1) {
+      return null
+    }
+    const statement = statements[0]
+    if (statement.type === 'ExpressionStatement') {
+      exprStatement = statement
+      break
+    }
+    if (statement.type === 'BlockStatement') {
+      body = statement.body
+      continue
+    }
+    return null
+  }
+  const expression = exprStatement.expression
+  if (!isIdentifierCallExpression(expression)) {
+    return null
+  }
+  return expression
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce writing style for handlers in `v-on` directives',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/v-on-handler-style.html'
+    },
+    fixable: 'code',
+    schema: [
+      {
+        oneOf: [
+          { enum: ['inline', 'inline-function'] },
+          {
+            type: 'array',
+            items: [
+              { const: 'method' },
+              { enum: ['inline', 'inline-function'] }
+            ],
+            uniqueItems: true,
+            additionalItems: false,
+            minItems: 2,
+            maxItems: 2
+          }
+        ]
+      },
+      {
+        type: 'object',
+        properties: {
+          ignoreIncludesComment: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      preferMethodOverInline:
+        'Prefer method handler over inline handler in v-on.',
+      preferMethodOverInlineWithoutIdCall:
+        'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
+      preferMethodOverInlineFunction:
+        'Prefer method handler over inline function in v-on.',
+      preferMethodOverInlineFunctionWithoutIdCall:
+        'Prefer method handler over inline function in v-on. Note that you may need to create a new method.',
+      preferInlineOverMethod:
+        'Prefer inline handler over method handler in v-on.',
+      preferInlineOverInlineFunction:
+        'Prefer inline handler over inline function in v-on.',
+      preferInlineOverInlineFunctionWithMultipleParams:
+        'Prefer inline handler over inline function in v-on. Note that the custom event must be changed to a single payload.',
+      preferInlineFunctionOverMethod:
+        'Prefer inline function over method handler in v-on.',
+      preferInlineFunctionOverInline:
+        'Prefer inline function over inline handler in v-on.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const { allows, ignoreIncludesComment } = parseOptions(context)
+
+    /** @type {Set<VElement>} */
+    const upperElements = new Set()
+    /** @type {Map<string, number>} */
+    const methodParamCountMap = new Map()
+    /** @type {Identifier[]} */
+    const $eventIdentifiers = []
+
+    /**
+     * Verify for inline handler.
+     * @param {VOnExpression} node
+     * @param {HandlerKind} kind
+     * @returns {boolean} Returns `true` if reported.
+     */
+    function verifyForInlineHandler(node, kind) {
+      switch (kind) {
+        case 'method':
+          return verifyCanUseMethodHandlerForInlineHandler(node)
+        case 'inline-function':
+          reportCanUseInlineFunctionForInlineHandler(node)
+          return true
+      }
+      return false
+    }
+    /**
+     * Report for method handler.
+     * @param {Identifier} node
+     * @param {HandlerKind} kind
+     * @returns {boolean} Returns `true` if reported.
+     */
+    function reportForMethodHandler(node, kind) {
+      switch (kind) {
+        case 'inline':
+        case 'inline-function':
+          context.report({
+            node,
+            messageId:
+              kind === 'inline'
+                ? 'preferInlineOverMethod'
+                : 'preferInlineFunctionOverMethod'
+          })
+          return true
+      }
+      // This path is currently not taken.
+      return false
+    }
+    /**
+     * Verify for inline function handler.
+     * @param {ArrowFunctionExpression | FunctionExpression} node
+     * @param {HandlerKind} kind
+     * @returns {boolean} Returns `true` if reported.
+     */
+    function verifyForInlineFunction(node, kind) {
+      switch (kind) {
+        case 'method':
+          return verifyCanUseMethodHandlerForInlineFunction(node)
+        case 'inline':
+          reportCanUseInlineHandlerForInlineFunction(node)
+          return true
+      }
+      return false
+    }
+
+    /**
+     * Get token information for the given VExpressionContainer node.
+     * @param {VExpressionContainer} node
+     */
+    function getVExpressionContainerTokenInfo(node) {
+      const tokenStore = context.parserServices.getTemplateBodyTokenStore()
+      const tokens = tokenStore.getTokens(node, {
+        includeComments: true
+      })
+      const firstToken = tokens[0]
+      const lastToken = tokens[tokens.length - 1]
+
+      const hasQuote = isQuote(firstToken)
+      /** @type {Range} */
+      const rangeWithoutQuotes = hasQuote
+        ? [firstToken.range[1], lastToken.range[0]]
+        : [firstToken.range[0], lastToken.range[1]]
+
+      return {
+        rangeWithoutQuotes,
+        get hasComment() {
+          return tokens.some(
+            (token) => token.type === 'Block' || token.type === 'Line'
+          )
+        },
+        hasQuote
+      }
+    }
+
+    /**
+     * Checks whether the given node refers to a variable of the element.
+     * @param {Expression | VOnExpression} node
+     */
+    function hasReferenceUpperElementVariable(node) {
+      for (const element of upperElements) {
+        for (const vv of element.variables) {
+          for (const reference of vv.references) {
+            const { range } = reference.id
+            if (node.range[0] <= range[0] && range[1] <= node.range[1]) {
+              return true
+            }
+          }
+        }
+      }
+      return false
+    }
+    /**
+     * Check if `v-on:click="foo()"` can be converted to `v-on:click="foo"` and report if it can.
+     * @param {VOnExpression} node
+     * @returns {boolean} Returns `true` if reported.
+     */
+    function verifyCanUseMethodHandlerForInlineHandler(node) {
+      const { rangeWithoutQuotes, hasComment } =
+        getVExpressionContainerTokenInfo(node.parent)
+      if (ignoreIncludesComment && hasComment) {
+        return false
+      }
+
+      const idCallExpr = getIdentifierCallExpression(node)
+      if (
+        (!idCallExpr || idCallExpr.arguments.length > 0) &&
+        hasReferenceUpperElementVariable(node)
+      ) {
+        // It cannot be converted to method because it refers to the variable of the element.
+        // e.g. <template v-for="e in list"><button @click="foo(e)" /></template>
+        return false
+      }
+
+      context.report({
+        node,
+        messageId: idCallExpr
+          ? 'preferMethodOverInline'
+          : 'preferMethodOverInlineWithoutIdCall',
+        fix: (fixer) => {
+          if (
+            hasComment /* The statement contains comment and cannot be fixed. */ ||
+            !idCallExpr /* The statement is not a simple identifier call and cannot be fixed. */ ||
+            idCallExpr.arguments.length > 0
+          ) {
+            return null
+          }
+          const paramCount = methodParamCountMap.get(idCallExpr.callee.name)
+          if (paramCount != null && paramCount > 0) {
+            // The behavior of target method can change given the arguments.
+            return null
+          }
+          return fixer.replaceTextRange(
+            rangeWithoutQuotes,
+            context.getSourceCode().getText(idCallExpr.callee)
+          )
+        }
+      })
+      return true
+    }
+    /**
+     * Check if `v-on:click="() => foo()"` can be converted to `v-on:click="foo"` and report if it can.
+     * @param {ArrowFunctionExpression | FunctionExpression} node
+     * @returns {boolean} Returns `true` if reported.
+     */
+    function verifyCanUseMethodHandlerForInlineFunction(node) {
+      const { rangeWithoutQuotes, hasComment } =
+        getVExpressionContainerTokenInfo(
+          /** @type {VExpressionContainer} */ (node.parent)
+        )
+      if (ignoreIncludesComment && hasComment) {
+        return false
+      }
+
+      /** @type {CallExpression & {callee: Identifier} | null} */
+      let idCallExpr = null
+      if (node.body.type === 'BlockStatement') {
+        idCallExpr = getIdentifierCallExpression(node.body)
+      } else if (isIdentifierCallExpression(node.body)) {
+        idCallExpr = node.body
+      }
+      if (
+        (!idCallExpr || !isSameParamsAndArgs(idCallExpr)) &&
+        hasReferenceUpperElementVariable(node)
+      ) {
+        // It cannot be converted to method because it refers to the variable of the element.
+        // e.g. <template v-for="e in list"><button @click="() => foo(e)" /></template>
+        return false
+      }
+
+      context.report({
+        node,
+        messageId: idCallExpr
+          ? 'preferMethodOverInlineFunction'
+          : 'preferMethodOverInlineFunctionWithoutIdCall',
+        fix: (fixer) => {
+          if (
+            hasComment /* The function contains comment and cannot be fixed. */ ||
+            !idCallExpr /* The function is not a simple identifier call and cannot be fixed. */
+          ) {
+            return null
+          }
+          if (!isSameParamsAndArgs(idCallExpr)) {
+            // It is not a call with the arguments given as is.
+            return null
+          }
+          const paramCount = methodParamCountMap.get(idCallExpr.callee.name)
+          if (
+            paramCount != null &&
+            paramCount !== idCallExpr.arguments.length
+          ) {
+            // The behavior of target method can change given the arguments.
+            return null
+          }
+          return fixer.replaceTextRange(
+            rangeWithoutQuotes,
+            context.getSourceCode().getText(idCallExpr.callee)
+          )
+        }
+      })
+      return true
+
+      /**
+       * Checks whether parameters are passed as arguments as-is.
+       * @param {CallExpression} expression
+       */
+      function isSameParamsAndArgs(expression) {
+        return (
+          node.params.length === expression.arguments.length &&
+          node.params.every((param, index) => {
+            if (param.type !== 'Identifier') {
+              return false
+            }
+            const arg = expression.arguments[index]
+            if (!arg || arg.type !== 'Identifier') {
+              return false
+            }
+            return param.name === arg.name
+          })
+        )
+      }
+    }
+    /**
+     * Report `v-on:click="foo()"` can be converted to `v-on:click="()=>foo()"`.
+     * @param {VOnExpression} node
+     * @returns {void}
+     */
+    function reportCanUseInlineFunctionForInlineHandler(node) {
+      context.report({
+        node,
+        messageId: 'preferInlineFunctionOverInline',
+        *fix(fixer) {
+          const has$Event = $eventIdentifiers.some(
+            ({ range }) =>
+              node.range[0] <= range[0] && range[1] <= node.range[1]
+          )
+          if (has$Event) {
+            /* The statements contains $event and cannot be fixed. */
+            return
+          }
+          const { rangeWithoutQuotes, hasQuote } =
+            getVExpressionContainerTokenInfo(node.parent)
+          if (!hasQuote) {
+            /* The statements is not enclosed in quotes and cannot be fixed. */
+            return
+          }
+          yield fixer.insertTextBeforeRange(rangeWithoutQuotes, '() => ')
+          const tokenStore = context.parserServices.getTemplateBodyTokenStore()
+          const firstToken = tokenStore.getFirstToken(node)
+          const lastToken = tokenStore.getLastToken(node)
+          if (firstToken.value === '{' && lastToken.value === '}') return
+          if (
+            lastToken.value !== ';' &&
+            node.body.length === 1 &&
+            node.body[0].type === 'ExpressionStatement'
+          ) {
+            // it is a single expression
+            return
+          }
+          yield fixer.insertTextBefore(firstToken, '{')
+          yield fixer.insertTextAfter(lastToken, '}')
+        }
+      })
+    }
+    /**
+     * Report `v-on:click="() => foo()"` can be converted to `v-on:click="foo()"`.
+     * @param {ArrowFunctionExpression | FunctionExpression} node
+     * @returns {void}
+     */
+    function reportCanUseInlineHandlerForInlineFunction(node) {
+      // If a function has one parameter, you can turn it into an inline handler using $event.
+      // If a function has two or more parameters, it cannot be easily converted to an inline handler.
+      // However, users can use inline handlers by changing the payload of the component's custom event.
+      // So we report it regardless of the number of parameters.
+
+      context.report({
+        node,
+        messageId:
+          node.params.length > 1
+            ? 'preferInlineOverInlineFunctionWithMultipleParams'
+            : 'preferInlineOverInlineFunction',
+        fix:
+          node.params.length > 0
+            ? null /* The function has parameters and cannot be fixed. */
+            : (fixer) => {
+                let text = context.getSourceCode().getText(node.body)
+                if (node.body.type === 'BlockStatement') {
+                  text = text.slice(1, -1) // strip braces
+                }
+                return fixer.replaceText(node, text)
+              }
+      })
+    }
+
+    return utils.defineTemplateBodyVisitor(
+      context,
+      {
+        VElement(node) {
+          upperElements.add(node)
+        },
+        'VElement:exit'(node) {
+          upperElements.delete(node)
+        },
+        /** @param {VExpressionContainer} node */
+        "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer.value:exit"(
+          node
+        ) {
+          const expression = node.expression
+          if (!expression) {
+            return
+          }
+          switch (expression.type) {
+            case 'VOnExpression': {
+              // e.g. v-on:click="foo()"
+              if (allows[0] === 'inline') {
+                return
+              }
+              for (const allow of allows) {
+                if (verifyForInlineHandler(expression, allow)) {
+                  return
+                }
+              }
+              break
+            }
+            case 'Identifier': {
+              // e.g. v-on:click="foo"
+              if (allows[0] === 'method') {
+                return
+              }
+              for (const allow of allows) {
+                if (reportForMethodHandler(expression, allow)) {
+                  return
+                }
+              }
+              break
+            }
+            case 'ArrowFunctionExpression':
+            case 'FunctionExpression': {
+              // e.g. v-on:click="()=>foo()"
+              if (allows[0] === 'inline-function') {
+                return
+              }
+              for (const allow of allows) {
+                if (verifyForInlineFunction(expression, allow)) {
+                  return
+                }
+              }
+              break
+            }
+            default:
+              return
+          }
+        },
+        ...(allows.includes('inline-function')
+          ? // Collect $event identifiers to check for side effects
+            // when converting from `v-on:click="foo($event)"` to `v-on:click="()=>foo($event)"` .
+            {
+              'Identifier[name="$event"]'(node) {
+                $eventIdentifiers.push(node)
+              }
+            }
+          : {})
+      },
+      allows.includes('method')
+        ? // Collect method definition with params information to check for side effects.
+          // when converting from `v-on:click="foo()"` to `v-on:click="foo"`, or
+          // converting from `v-on:click="() => foo()"` to `v-on:click="foo"`.
+          utils.defineVueVisitor(context, {
+            onVueObjectEnter(node) {
+              for (const method of utils.iterateProperties(
+                node,
+                new Set(['methods'])
+              )) {
+                if (method.type !== 'object') {
+                  // This branch is usually not passed.
+                  continue
+                }
+                const value = method.property.value
+                if (
+                  value.type === 'FunctionExpression' ||
+                  value.type === 'ArrowFunctionExpression'
+                ) {
+                  methodParamCountMap.set(
+                    method.name,
+                    value.params.some((p) => p.type === 'RestElement')
+                      ? Number.POSITIVE_INFINITY
+                      : value.params.length
+                  )
+                }
+              }
+            }
+          })
+        : {}
+    )
+  }
+}
diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts
index 949d56f6..7d85f665 100644
--- a/typings/eslint/index.d.ts
+++ b/typings/eslint/index.d.ts
@@ -370,16 +370,14 @@ export namespace Linter {
   type LintMessage = ESLintLinter.LintMessage
   type LintOptions = ESLintLinter.LintOptions
 }
-
+export type ReportDescriptorFix = (
+  fixer: Rule.RuleFixer
+) => null | Rule.Fix | IterableIterator<Rule.Fix> | Rule.Fix[]
 interface ReportDescriptorOptionsBase {
   data?: {
     [key: string]: string | number
   }
-  fix?:
-    | null
-    | ((
-        fixer: Rule.RuleFixer
-      ) => null | Rule.Fix | IterableIterator<Rule.Fix> | Rule.Fix[])
+  fix?: null | ReportDescriptorFix
 }
 
 interface SuggestionReportDescriptor1 extends ReportDescriptorOptionsBase {


Test patch:
diff --git a/tests/lib/rules/v-on-handler-style.js b/tests/lib/rules/v-on-handler-style.js
new file mode 100644
index 00000000..e3afd05d
--- /dev/null
+++ b/tests/lib/rules/v-on-handler-style.js
@@ -0,0 +1,1138 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/v-on-handler-style')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('v-on-handler-style', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <template v-for="e in list">
+          <button @click="() => foo(e)" />
+        </template>
+      </template>`,
+      options: [['method', 'inline-function']]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <template v-for="e in list">
+          <button @click="() => foo(e)" />
+        </template>
+      </template>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <template v-for="e in list">
+          <button @click="foo(e)" />
+        </template>
+      </template>`,
+      options: [['method', 'inline']]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo()" />
+      </template>`,
+      options: ['inline']
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => foo()" />
+      </template>`,
+      options: ['inline-function']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button @[foo()]="bar" /></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button :click="foo()" /></template>'
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <button @click="foo()" />
+        <button @click="() => foo()" />
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: `<template>
+        <button @click="foo" />
+        <button @click="foo" />
+        <button @click="foo" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <button @click="foo()" />
+        <button @click="() => foo()" />
+      </template>`,
+      output: `<template>
+        <button @click="foo" />
+        <button @click="foo" />
+        <button @click="foo" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <button @click="foo()" />
+        <button @click="() => foo()" />
+      </template>`,
+      output: `<template>
+        <button @click="foo" />
+        <button @click="foo()" />
+        <button @click="foo()" />
+      </template>`,
+      options: ['inline'],
+      errors: [
+        {
+          message: 'Prefer inline handler over method handler in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+        <button @click="foo()" />
+        <button @click="() => foo()" />
+      </template>`,
+      options: ['inline-function'],
+      output: `<template>
+        <button @click="foo" />
+        <button @click="() => foo()" />
+        <button @click="() => foo()" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer inline function over method handler in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 3,
+          column: 25
+        }
+      ]
+    },
+    // ['method', 'inline-function']
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo( )" /></template>',
+      output: `<template><div @click="foo" /></template>`,
+      options: [['method', 'inline-function']],
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 1,
+          column: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo() /* comment */" />
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo() /* comment */" />
+      </template>`,
+      options: [['method', 'inline-function'], { ignoreIncludesComment: true }],
+      output: `<template>
+        <button @click="() => foo() /* comment */" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo(/**/)"/>
+        <button @click="/* comment */foo()"/>
+        <button @click=foo()/*comment*/ />
+        <button @click="foo()// comment
+        "/>
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 38
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 4,
+          column: 24
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 5,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="fn();" />
+        <button @click="fn();;" />
+        <button @click="{ fn(); }" />
+        <button @click="{(fn());;;}" />
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: `
+      <template>
+        <button @click="fn" />
+        <button @click="fn" />
+        <button @click="fn" />
+        <button @click="fn" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 4,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 5,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 6,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div @click=" beforeSpace()" />
+        <div @click='afterSpace() ' />
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: `
+      <template>
+        <div @click="beforeSpace" />
+        <div @click='afterSpace' />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 23
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 4,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click=" &#x66;oo ( ) " />
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: `
+      <template>
+        <button @click="&#x66;oo" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 26
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template><button @click="foo()" /></template>
+      <script>
+      export default {
+        methods: {
+          foo() {}
+        }
+      }
+      </script>`,
+      options: [['method', 'inline-function']],
+      output: `
+      <template><button @click="foo" /></template>
+      <script>
+      export default {
+        methods: {
+          foo() {}
+        }
+      }
+      </script>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 2,
+          column: 33
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template><button @click="foo()" /></template>
+      <script>
+      export default {
+        methods: {
+          foo: () => {}
+        }
+      }
+      </script>`,
+      options: [['method', 'inline-function']],
+      output: `
+      <template><button @click="foo" /></template>
+      <script>
+      export default {
+        methods: {
+          foo: () => {}
+        }
+      }
+      </script>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 2,
+          column: 33
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo(123)" />
+        <button @click="foo.bar()" />
+        <button @click="foo?.()" />
+        <button @click="foo();foo();" />
+        <button @click="{}" />
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message:
+            'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
+          line: 3,
+          column: 25
+        },
+        {
+          message:
+            'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
+          line: 4,
+          column: 25
+        },
+        {
+          message:
+            'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
+          line: 5,
+          column: 25
+        },
+        {
+          message:
+            'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
+          line: 6,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template><button @click="foo()" /></template>
+      <script>
+      export default {
+        methods: {
+          foo(a) {}
+        }
+      }
+      </script>`,
+      options: [['method', 'inline-function']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 2,
+          column: 33
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="foo()" />
+        <button @click="bar()" />
+        <button @click="baz()" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo(a, b) {},
+          bar(...a) {},
+          baz(a = 42) {},
+        }
+      }
+      </script>`,
+      options: [['method', 'inline-function']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 4,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 5,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="foo()" />
+        <button @click="bar()" />
+        <button @click="baz()" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo: (a, b) => {},
+          bar: (...a) => {},
+          baz: (a = 42) => {},
+        }
+      }
+      </script>`,
+      options: [['method', 'inline-function']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 4,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 5,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <template v-for="e in list">
+          <button @click="e()" />
+        </template>
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: `<template>
+        <template v-for="e in list">
+          <button @click="e" />
+        </template>
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline handler in v-on.',
+          line: 3,
+          column: 27
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <template v-for="e in list">
+          <button @click="handler(e)" />
+          <button @click="handlers[e]()" />
+          <button @click="handler(a(b), c(d), e + f)" />
+          <button @click="e.foo()" />
+        </template>
+      </template>`,
+      options: [['method', 'inline-function']],
+      output: `<template>
+        <template v-for="e in list">
+          <button @click="() => handler(e)" />
+          <button @click="() => handlers[e]()" />
+          <button @click="() => handler(a(b), c(d), e + f)" />
+          <button @click="() => e.foo()" />
+        </template>
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 3,
+          column: 27
+        },
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 4,
+          column: 27
+        },
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 5,
+          column: 27
+        },
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 6,
+          column: 27
+        }
+      ]
+    },
+    // 'inline-function'
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo( )" />
+        <button @click="count++" />
+      </template>`,
+      output: `<template>
+        <button @click="() => foo( )" />
+        <button @click="() => count++" />
+      </template>`,
+      options: ['inline-function'],
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 3,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo($event)" /></template>',
+      output: null,
+      options: ['inline-function'],
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 1,
+          column: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click=foo() /></template>',
+      output: null,
+      options: ['inline-function'],
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 1,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button @click="{foo();foo();}" /></template>',
+      output: '<template><button @click="() => {foo();foo();}" /></template>',
+      options: ['inline-function'],
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 1,
+          column: 27
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button @click="foo();foo();" /></template>',
+      output: '<template><button @click="() => {foo();foo();}" /></template>',
+      options: ['inline-function'],
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 1,
+          column: 27
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button @click="foo();" /></template>',
+      output: '<template><button @click="() => {foo();}" /></template>',
+      options: ['inline-function'],
+      errors: [
+        {
+          message: 'Prefer inline function over inline handler in v-on.',
+          line: 1,
+          column: 27
+        }
+      ]
+    },
+    // 'inline' with method
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="foo" />
+      </template>`,
+      options: ['inline'],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer inline handler over method handler in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    // ['method', 'inline']
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => foo()" />
+        <button @click="(a, b) => foo(a, b)" />
+        <button @click="() => { foo() }" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: `<template>
+        <button @click="foo" />
+        <button @click="foo" />
+        <button @click="foo" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => foo() /* comment */" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => foo() /* comment */" />
+      </template>`,
+      options: [['method', 'inline'], { ignoreIncludesComment: true }],
+      output: `<template>
+        <button @click="foo() /* comment */" />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="(a, b) => foo(a, b)" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo(a, b) {}
+        }
+      }
+      </script>`,
+      options: [['method', 'inline']],
+      output: `<template>
+        <button @click="foo" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo(a, b) {}
+        }
+      }
+      </script>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="(a, b) => foo(a, b)" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo
+        }
+      }
+      </script>`,
+      options: [['method', 'inline']],
+      output: `<template>
+        <button @click="foo" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo
+        }
+      }
+      </script>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => handler(foo)" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => foo?.()" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message:
+            'Prefer method handler over inline function in v-on. Note that you may need to create a new method.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => count++" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message:
+            'Prefer method handler over inline function in v-on. Note that you may need to create a new method.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => { count++ }" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message:
+            'Prefer method handler over inline function in v-on. Note that you may need to create a new method.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="(a, b) => foo(b, a)" />
+        <button @click="(a, b) => foo(...a, b)" />
+        <button @click="(...a) => foo(a)" />
+      </template>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="(a) => foo(a)" />
+        <button @click="(a) => bar(a)" />
+        <button @click="(a, b) => baz(a, b)" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo(a, b) {},
+          bar(...a) {},
+          baz(a = 42) {},
+        }
+      }
+      </script>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 5,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="(a) => foo(a)" />
+        <button @click="(a) => bar(a)" />
+        <button @click="(a, b) => baz(a, b)" />
+      </template>
+      <script>
+      export default {
+        methods: {
+          foo: (a, b) => {},
+          bar: (...a) => {},
+          baz: (a = 42) => {},
+        }
+      }
+      </script>`,
+      options: [['method', 'inline']],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 5,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <template v-for="e in list">
+          <button @click="() => e()" />
+          <button @click="(a) => e(a)" />
+          <button @click="(a, b) => e(a, b)" />
+        </template>
+      </template>`,
+      options: [['method', 'inline']],
+      output: `<template>
+        <template v-for="e in list">
+          <button @click="e" />
+          <button @click="e" />
+          <button @click="e" />
+        </template>
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 3,
+          column: 27
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 4,
+          column: 27
+        },
+        {
+          message: 'Prefer method handler over inline function in v-on.',
+          line: 5,
+          column: 27
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <template v-for="e in list">
+          <button @click="() => handler(e)" />
+          <button @click="() => handlers[e]()" />
+          <button @click="() => handler(a(b), c(d), e + f)" />
+          <button @click="() => e.foo()" />
+        </template>
+      </template>`,
+      options: [['method', 'inline']],
+      output: `<template>
+        <template v-for="e in list">
+          <button @click="handler(e)" />
+          <button @click="handlers[e]()" />
+          <button @click="handler(a(b), c(d), e + f)" />
+          <button @click="e.foo()" />
+        </template>
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 3,
+          column: 27
+        },
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 4,
+          column: 27
+        },
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 5,
+          column: 27
+        },
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 6,
+          column: 27
+        }
+      ]
+    },
+    // 'inline' with function
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="() => foo(a, b)" />
+        <button @click="() => count++" />
+        <button @click="() => { count++; foo(); }" />
+      </template>`,
+      options: ['inline'],
+      output: `<template>
+        <button @click="foo(a, b)" />
+        <button @click="count++" />
+        <button @click=" count++; foo(); " />
+      </template>`,
+      errors: [
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        },
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 3,
+          column: 25
+        },
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="(a) => foo(a, b)" />
+      </template>`,
+      options: ['inline'],
+      output: null,
+      errors: [
+        {
+          message: 'Prefer inline handler over inline function in v-on.',
+          line: 2,
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+        <button @click="(a, b) => foo(a, b)" />
+      </template>`,
+      options: ['inline'],
+      output: null,
+      errors: [
+        {
+          message:
+            'Prefer inline handler over inline function in v-on. Note that the custom event must be changed to a single payload.',
+          line: 2,
+          column: 25
+        }
+      ]
+    }
+  ]
+})