Skip to content

Commit

Permalink
Fixed wrong autofix in vue/v-on-function-call rule and `ignoreInclu…
Browse files Browse the repository at this point in the history
…desComment` option to `vue/v-on-function-call` rule (#1204)
  • Loading branch information
ota-meshi committed Jun 12, 2020
1 parent 6def98a commit 6f8acee
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 51 deletions.
35 changes: 33 additions & 2 deletions docs/rules/v-on-function-call.md
Expand Up @@ -37,10 +37,19 @@ Default is set to `never`.

```json
{
"vue/v-on-function-call": ["error", "always"|"never"]
"vue/v-on-function-call": ["error",
"always"|"never",
{
"ignoreIncludesComment": false
}
]
}
```

- `"always"` ... Always use parentheses in `v-on` directives.
- `"never"` ... Never use parentheses in `v-on` directives for method calls without arguments. this is default.
- `ignoreIncludesComment` ... If `true`, do not report expressions containing comments. default `false`.

### `"always"` - Always use parentheses in `v-on` directives

<eslint-code-block fix :rules="{'vue/v-on-function-call': ['error', 'always']}">
Expand All @@ -63,7 +72,6 @@ Default is set to `never`.

### `"never"` - Never use parentheses in `v-on` directives for method calls without arguments


<eslint-code-block fix :rules="{'vue/v-on-function-call': ['error', 'never']}">

```vue
Expand All @@ -85,6 +93,29 @@ Default is set to `never`.

</eslint-code-block>

### `"never", { "ignoreIncludesComment": true }`

<eslint-code-block fix :rules="{'vue/v-on-function-call': ['error', 'never', {ignoreIncludesComment: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
<button v-on:click="closeModal">
Close
</button>
<button v-on:click="closeModal() /* comment */">
Close
</button>
<!-- ✗ BAD -->
<button v-on:click="closeModal()">
Close
</button>
</template>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-on-function-call.js)
Expand Down
1 change: 0 additions & 1 deletion lib/rules/no-restricted-v-bind.js
Expand Up @@ -2,7 +2,6 @@
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
// @ts-check
'use strict'

const utils = require('../utils')
Expand Down
168 changes: 120 additions & 48 deletions lib/rules/v-on-function-call.js
Expand Up @@ -9,17 +9,28 @@

const utils = require('../utils')

/**
* @typedef {import('vue-eslint-parser').AST.VOnExpression} VOnExpression
* @typedef {import('vue-eslint-parser').AST.Token} Token
* @typedef {import('vue-eslint-parser').AST.ESLintExpressionStatement} ExpressionStatement
* @typedef {import('vue-eslint-parser').AST.ESLintCallExpression} CallExpression
*/

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* Check whether the given token is a left parenthesis.
* Check whether the given token is a quote.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left parenthesis.
* @returns {boolean} `true` if the token is a quote.
*/
function isLeftParen(token) {
return token != null && token.type === 'Punctuator' && token.value === '('
function isQuote(token) {
return (
token != null &&
token.type === 'Punctuator' &&
(token.value === '"' || token.value === "'")
)
}

// ------------------------------------------------------------------------------
Expand All @@ -36,64 +47,125 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/v-on-function-call.html'
},
fixable: 'code',
schema: [{ enum: ['always', 'never'] }]
schema: [
{ enum: ['always', 'never'] },
{
type: 'object',
properties: {
ignoreIncludesComment: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},

create(context) {
const always = context.options[0] === 'always'

/**
* @param {VOnExpression} node
* @returns {CallExpression | null}
*/
function getInvalidNeverCallExpression(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 (expression.type !== 'CallExpression' || expression.arguments.length) {
return null
}
const callee = expression.callee
if (callee.type !== 'Identifier') {
return null
}
return expression
}

return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
node
) {
if (!always) return
context.report({
node,
loc: node.loc,
message:
"Method calls inside of 'v-on' directives must have parentheses."
})
},
...(always
? {
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
node
) {
context.report({
node,
message:
"Method calls inside of 'v-on' directives must have parentheses."
})
}
}
: {
/** @param {VOnExpression} node */
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"(
node
) {
const expression = getInvalidNeverCallExpression(node)
if (!expression) {
return
}
const option = context.options[1] || {}
const ignoreIncludesComment = option.ignoreIncludesComment

"VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression > ExpressionStatement > CallExpression"(
node
) {
if (
!always &&
node.arguments.length === 0 &&
node.callee.type === 'Identifier'
) {
context.report({
node,
loc: node.loc,
message:
"Method calls without arguments inside of 'v-on' directives must not have parentheses.",
fix: (fixer) => {
const tokenStore = context.parserServices.getTemplateBodyTokenStore()
const rightToken = tokenStore.getLastToken(node)
const leftToken = tokenStore.getTokenAfter(
node.callee,
isLeftParen
)
const tokens = tokenStore.getTokensBetween(
leftToken,
rightToken,
{ includeComments: true }
/** @type {Token[]} */
const tokens = tokenStore.getTokens(node.parent, {
includeComments: true
})
let leftQuote
let rightQuote
if (isQuote(tokens[0])) {
leftQuote = tokens.shift()
rightQuote = tokens.pop()
}

const hasComment = tokens.some(
(token) => token.type === 'Block' || token.type === 'Line'
)

if (tokens.length) {
// The comment is included and cannot be fixed.
return null
if (ignoreIncludesComment && hasComment) {
return
}

return fixer.removeRange([
leftToken.range[0],
rightToken.range[1]
])
context.report({
node: expression,
message:
"Method calls without arguments inside of 'v-on' directives must not have parentheses.",
fix: hasComment
? null /* The comment is included and cannot be fixed. */
: (fixer) => {
const range = leftQuote
? [leftQuote.range[1], rightQuote.range[0]]
: [
tokens[0].range[0],
tokens[tokens.length - 1].range[1]
]

return fixer.replaceTextRange(
range,
context.getSourceCode().getText(expression.callee)
)
}
})
}
})
}
}
})
}
}

0 comments on commit 6f8acee

Please sign in to comment.