You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
6.0 KiB
209 lines
6.0 KiB
/** |
|
* @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<string>} */ |
|
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) |
|
} |
|
} |
|
} |
|
}) |
|
) |
|
} |
|
}
|
|
|