diff --git a/docs/rules/README.md b/docs/rules/README.md
index f0bd7578e..f704c1a61 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: |
@@ -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 6a21408c5..9fdd80829 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 edd48f084..c80dafc60 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 000000000..18494e105
--- /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: ***This rule has not been released yet.***
+- :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
+
+
+
+
+
+
+
+
+```
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :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)
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `["method", "inline"]`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `["inline-function"]`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `["inline"]`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `["method", "inline-function"], { "ignoreIncludesComment": true }`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `["method", "inline"], { "ignoreIncludesComment": true }`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :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 73e36ce33..89c135fc2 100644
--- a/docs/rules/v-on-style.md
+++ b/docs/rules/v-on-style.md
@@ -59,6 +59,10 @@ Default is set to `shorthand`.
+## :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 645669c4c..6f5452cc8 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 b6274d12e..e2409794b 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 000000000..aa1214e38
--- /dev/null
+++ b/lib/rules/v-on-handler-style.js
@@ -0,0 +1,578 @@
+/**
+ * @author Yosuke Ota
+ * 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} */
+ const upperElements = new Set()
+ /** @type {Map} */
+ 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.
+ 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.
+ 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/tests/lib/rules/v-on-handler-style.js b/tests/lib/rules/v-on-handler-style.js
new file mode 100644
index 000000000..e3afd05d5
--- /dev/null
+++ b/tests/lib/rules/v-on-handler-style.js
@@ -0,0 +1,1138 @@
+/**
+ * @author Yosuke Ota
+ * 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: `
+
+
+
+ `,
+ options: [['method', 'inline-function']]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ options: [['method', 'inline']]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: ['inline']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ `,
+ options: ['inline-function']
+ },
+ {
+ filename: 'test.vue',
+ code: ''
+ },
+ {
+ filename: 'test.vue',
+ code: ''
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+
+
+ `,
+ 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: `
+
+
+ `,
+ output: `
+
+
+
+ `,
+ 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: `
+
+
+ `,
+ output: `
+
+
+
+ `,
+ 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: `
+
+
+ `,
+ options: ['inline-function'],
+ output: `
+
+ `,
+ 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: '',
+ output: ``,
+ options: [['method', 'inline-function']],
+ errors: [
+ {
+ message: 'Prefer method handler over inline handler in v-on.',
+ line: 1,
+ column: 24
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ 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: `
+
+ `,
+ options: [['method', 'inline-function'], { ignoreIncludesComment: true }],
+ output: `
+ foo() /* comment */" />
+ `,
+ errors: [
+ {
+ message: 'Prefer inline function over inline handler in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+ `,
+ 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: `
+
+
+
+
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+
+
+
+
+ `,
+ 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: `
+
+
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+
+
+ `,
+ 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: `
+
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+
+ `,
+ errors: [
+ {
+ message: 'Prefer method handler over inline handler in v-on.',
+ line: 3,
+ column: 26
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+ `,
+ errors: [
+ {
+ message: 'Prefer method handler over inline handler in v-on.',
+ line: 2,
+ column: 33
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+ `,
+ errors: [
+ {
+ message: 'Prefer method handler over inline handler in v-on.',
+ line: 2,
+ column: 33
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ 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: `
+
+ `,
+ 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: `
+
+
+
+
+
+ `,
+ 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: `
+
+
+
+
+
+ `,
+ 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: `
+
+
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Prefer method handler over inline handler in v-on.',
+ line: 3,
+ column: 27
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+
+ `,
+ options: [['method', 'inline-function']],
+ output: `
+
+ handler(e)" />
+ handlers[e]()" />
+ handler(a(b), c(d), e + f)" />
+ e.foo()" />
+
+ `,
+ 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: `
+
+
+ `,
+ output: `
+ foo( )" />
+ count++" />
+ `,
+ 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: '',
+ output: null,
+ options: ['inline-function'],
+ errors: [
+ {
+ message: 'Prefer inline function over inline handler in v-on.',
+ line: 1,
+ column: 24
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: null,
+ options: ['inline-function'],
+ errors: [
+ {
+ message: 'Prefer inline function over inline handler in v-on.',
+ line: 1,
+ column: 23
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: ' {foo();foo();}" />',
+ options: ['inline-function'],
+ errors: [
+ {
+ message: 'Prefer inline function over inline handler in v-on.',
+ line: 1,
+ column: 27
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: ' {foo();foo();}" />',
+ options: ['inline-function'],
+ errors: [
+ {
+ message: 'Prefer inline function over inline handler in v-on.',
+ line: 1,
+ column: 27
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ output: ' {foo();}" />',
+ 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: `
+
+ `,
+ 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: `
+ foo()" />
+ foo(a, b)" />
+ { foo() }" />
+ `,
+ options: [['method', 'inline']],
+ output: `
+
+
+
+ `,
+ 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: `
+ foo() /* comment */" />
+ `,
+ options: [['method', 'inline']],
+ output: null,
+ errors: [
+ {
+ message: 'Prefer method handler over inline function in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ foo() /* comment */" />
+ `,
+ options: [['method', 'inline'], { ignoreIncludesComment: true }],
+ output: `
+
+ `,
+ errors: [
+ {
+ message: 'Prefer inline handler over inline function in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ foo(a, b)" />
+
+ `,
+ options: [['method', 'inline']],
+ output: `
+
+
+ `,
+ errors: [
+ {
+ message: 'Prefer method handler over inline function in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ foo(a, b)" />
+
+ `,
+ options: [['method', 'inline']],
+ output: `
+
+
+ `,
+ errors: [
+ {
+ message: 'Prefer method handler over inline function in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ handler(foo)" />
+ `,
+ options: [['method', 'inline']],
+ output: null,
+ errors: [
+ {
+ message: 'Prefer method handler over inline function in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ foo?.()" />
+ `,
+ 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: `
+ count++" />
+ `,
+ 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: `
+ { count++ }" />
+ `,
+ 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: `
+ foo(b, a)" />
+ foo(...a, b)" />
+ foo(a)" />
+ `,
+ 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: `
+
+ foo(a)" />
+ bar(a)" />
+ baz(a, b)" />
+
+ `,
+ 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: `
+
+ foo(a)" />
+ bar(a)" />
+ baz(a, b)" />
+
+ `,
+ 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: `
+
+ e()" />
+ e(a)" />
+ e(a, b)" />
+
+ `,
+ options: [['method', 'inline']],
+ output: `
+
+
+
+
+
+ `,
+ 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: `
+
+ handler(e)" />
+ handlers[e]()" />
+ handler(a(b), c(d), e + f)" />
+ e.foo()" />
+
+ `,
+ options: [['method', 'inline']],
+ output: `
+
+
+
+
+
+
+ `,
+ 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: `
+ foo(a, b)" />
+ count++" />
+ { count++; foo(); }" />
+ `,
+ options: ['inline'],
+ output: `
+
+
+
+ `,
+ 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: `
+ foo(a, b)" />
+ `,
+ options: ['inline'],
+ output: null,
+ errors: [
+ {
+ message: 'Prefer inline handler over inline function in v-on.',
+ line: 2,
+ column: 25
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ foo(a, b)" />
+ `,
+ 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
+ }
+ ]
+ }
+ ]
+})
diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts
index 949d56f6d..7d85f665f 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[]
interface ReportDescriptorOptionsBase {
data?: {
[key: string]: string | number
}
- fix?:
- | null
- | ((
- fixer: Rule.RuleFixer
- ) => null | Rule.Fix | IterableIterator | Rule.Fix[])
+ fix?: null | ReportDescriptorFix
}
interface SuggestionReportDescriptor1 extends ReportDescriptorOptionsBase {