Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed wrong autofix in vue/v-on-function-call rule and ignoreIncludesComment option to vue/v-on-function-call rule #1204

Merged
merged 1 commit into from Jun 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
)
}
})
}
})
}
}
})
}
}