/** * @author Niklas Higi */ 'use strict' // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ const utils = require('../utils') /** * @typedef { import('../utils').ComponentPropertyData } ComponentPropertyData */ // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ /** * 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 === "'") ) } // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { type: 'suggestion', docs: { description: 'enforce or forbid parentheses after method calls without arguments in `v-on` directives', categories: undefined, url: 'https://eslint.vuejs.org/rules/v-on-function-call.html' }, fixable: 'code', schema: [ { enum: ['always', 'never'] }, { type: 'object', properties: { ignoreIncludesComment: { type: 'boolean' } }, additionalProperties: false } ] }, /** @param {RuleContext} context */ 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 } if (expression.optional) { // Allow optional chaining return null } const callee = expression.callee if (callee.type !== 'Identifier') { return null } return expression } if (always) { return utils.defineTemplateBodyVisitor(context, { /** @param {Identifier} node */ "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." }) } }) } const option = context.options[1] || {} const ignoreIncludesComment = !!option.ignoreIncludesComment /** @type {Set} */ const useArgsMethods = new Set() return utils.defineTemplateBodyVisitor( context, { /** @param {VOnExpression} node */ "VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"( node ) { const expression = getInvalidNeverCallExpression(node) if (!expression) { return } const tokenStore = context.parserServices.getTemplateBodyTokenStore() const tokens = tokenStore.getTokens(node.parent, { includeComments: true }) /** @type {Token | undefined} */ let leftQuote /** @type {Token | undefined} */ let rightQuote if (isQuote(tokens[0])) { leftQuote = tokens.shift() rightQuote = tokens.pop() } const hasComment = tokens.some( (token) => token.type === 'Block' || token.type === 'Line' ) if (ignoreIncludesComment && hasComment) { return } if (expression.callee.type === 'Identifier') { if (useArgsMethods.has(expression.callee.name)) { // The behavior of target method can change given the arguments. return } } 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) => { /** @type {Range} */ const range = leftQuote && rightQuote ? [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) ) } }) } }, utils.defineVueVisitor(context, { onVueObjectEnter(node) { for (const method of utils.iterateProperties( node, new Set(['methods']) )) { if (useArgsMethods.has(method.name)) { continue } if (method.type !== 'object') { continue } const value = method.property.value if ( (value.type === 'FunctionExpression' || value.type === 'ArrowFunctionExpression') && value.params.length > 0 ) { useArgsMethods.add(method.name) } } } }) ) } }