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.
206 lines
5.5 KiB
206 lines
5.5 KiB
/**
|
|
* @author Niklas Higi
|
|
*/
|
|
'use strict'
|
|
|
|
const utils = require('../utils')
|
|
|
|
/**
|
|
* @typedef { import('../utils').ComponentPropertyData } ComponentPropertyData
|
|
*/
|
|
|
|
/**
|
|
* 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 === "'")
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @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 > 0) {
|
|
return null
|
|
}
|
|
if (expression.optional) {
|
|
// Allow optional chaining
|
|
return null
|
|
}
|
|
const callee = expression.callee
|
|
if (callee.type !== 'Identifier') {
|
|
return null
|
|
}
|
|
return expression
|
|
}
|
|
|
|
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',
|
|
deprecated: true,
|
|
replacedBy: ['v-on-handler-style'],
|
|
schema: [
|
|
{ enum: ['always', 'never'] },
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
ignoreIncludesComment: {
|
|
type: 'boolean'
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
],
|
|
messages: {
|
|
always: "Method calls inside of 'v-on' directives must have parentheses.",
|
|
never:
|
|
"Method calls without arguments inside of 'v-on' directives must not have parentheses."
|
|
}
|
|
},
|
|
/** @param {RuleContext} context */
|
|
create(context) {
|
|
const always = context.options[0] === 'always'
|
|
|
|
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,
|
|
messageId: 'always'
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
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 sourceCode = context.getSourceCode()
|
|
const tokenStore =
|
|
sourceCode.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' &&
|
|
useArgsMethods.has(expression.callee.name)
|
|
) {
|
|
// The behavior of target method can change given the arguments.
|
|
return
|
|
}
|
|
|
|
context.report({
|
|
node: expression,
|
|
messageId: 'never',
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
)
|
|
}
|
|
}
|