From 5361f747ed4bef69609d340ad99b0fe7664fa7b1 Mon Sep 17 00:00:00 2001 From: ota Date: Mon, 8 Jun 2020 14:39:28 +0900 Subject: [PATCH] Fixed wrong autofix in `vue/v-on-function-call` rule and `ignoreIncludesComment` option to `vue/v-on-function-call` rule --- docs/rules/v-on-function-call.md | 35 +++++- lib/rules/no-restricted-v-bind.js | 1 - lib/rules/v-on-function-call.js | 168 ++++++++++++++++++-------- tests/lib/rules/v-on-function-call.js | 132 ++++++++++++++++++++ 4 files changed, 285 insertions(+), 51 deletions(-) diff --git a/docs/rules/v-on-function-call.md b/docs/rules/v-on-function-call.md index 7e858eb7f..9ca7ec2af 100644 --- a/docs/rules/v-on-function-call.md +++ b/docs/rules/v-on-function-call.md @@ -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 @@ -63,7 +72,6 @@ Default is set to `never`. ### `"never"` - Never use parentheses in `v-on` directives for method calls without arguments - ```vue @@ -85,6 +93,29 @@ Default is set to `never`. +### `"never", { "ignoreIncludesComment": true }` + + + +```vue + +``` + + + ## :mag: Implementation - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-on-function-call.js) diff --git a/lib/rules/no-restricted-v-bind.js b/lib/rules/no-restricted-v-bind.js index a87be4d2d..7ec586dd5 100644 --- a/lib/rules/no-restricted-v-bind.js +++ b/lib/rules/no-restricted-v-bind.js @@ -2,7 +2,6 @@ * @author Yosuke Ota * See LICENSE file in root directory for full license. */ -// @ts-check 'use strict' const utils = require('../utils') diff --git a/lib/rules/v-on-function-call.js b/lib/rules/v-on-function-call.js index fe19f52da..1897b65f0 100644 --- a/lib/rules/v-on-function-call.js +++ b/lib/rules/v-on-function-call.js @@ -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 === "'") + ) } // ------------------------------------------------------------------------------ @@ -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) + ) + } + }) } }) - } - } }) } } diff --git a/tests/lib/rules/v-on-function-call.js b/tests/lib/rules/v-on-function-call.js index bf56f1453..b78a92370 100644 --- a/tests/lib/rules/v-on-function-call.js +++ b/tests/lib/rules/v-on-function-call.js @@ -71,6 +71,41 @@ tester.run('v-on-function-call', rule, { filename: 'test.vue', code: '', options: ['always'] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + options: ['never', { ignoreIncludesComment: true }] } ], invalid: [ @@ -109,6 +144,103 @@ tester.run('v-on-function-call', rule, { "Method calls without arguments inside of 'v-on' directives must not have parentheses." ], options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + output: null, + errors: [ + "Method calls without arguments inside of 'v-on' directives must not have parentheses.", + "Method calls without arguments inside of 'v-on' directives must not have parentheses.", + "Method calls without arguments inside of 'v-on' directives must not have parentheses.", + "Method calls without arguments inside of 'v-on' directives must not have parentheses." + ], + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + output: ` + `, + errors: [ + "Method calls without arguments inside of 'v-on' directives must not have parentheses." + ], + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + output: ` + `, + errors: [ + "Method calls without arguments inside of 'v-on' directives must not have parentheses." + ], + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + output: ` + `, + errors: [ + "Method calls without arguments inside of 'v-on' directives must not have parentheses.", + "Method calls without arguments inside of 'v-on' directives must not have parentheses." + ], + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + output: ` + `, + errors: [ + "Method calls without arguments inside of 'v-on' directives must not have parentheses." + ], + options: ['never'] + }, + { + filename: 'test.vue', + code: ` + `, + output: ` + `, + errors: [ + "Method calls without arguments inside of 'v-on' directives must not have parentheses." + ], + options: ['never'] } ] })