Skip to content

Commit

Permalink
Fix false positives for methods whose arguments should not be changed…
Browse files Browse the repository at this point in the history
… in the `vue/v-on-function-call` rule. (#1369)
  • Loading branch information
ota-meshi committed Dec 4, 2020
1 parent b73d7c5 commit d9f72a5
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 69 deletions.
173 changes: 105 additions & 68 deletions lib/rules/v-on-function-call.js
Expand Up @@ -9,6 +9,10 @@

const utils = require('../utils')

/**
* @typedef { import('../utils').ComponentPropertyData } ComponentPropertyData
*/

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -96,77 +100,110 @@ module.exports = {
return expression
}

return utils.defineTemplateBodyVisitor(context, {
...(always
? {
/** @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."
})
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
}
}
: {
/** @param {VOnExpression} node */
"VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"(
node

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
) {
const expression = getInvalidNeverCallExpression(node)
if (!expression) {
return
}
const option = context.options[1] || {}
const ignoreIncludesComment = !!option.ignoreIncludesComment

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
}

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)
)
}
})
useArgsMethods.add(method.name)
}
})
})
}
}
})
)
}
}
103 changes: 102 additions & 1 deletion tests/lib/rules/v-on-function-call.js
Expand Up @@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/v-on-function-call')

const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2020 }
parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
})

tester.run('v-on-function-call', rule, {
Expand Down Expand Up @@ -111,6 +111,57 @@ tester.run('v-on-function-call', rule, {
filename: 'test.vue',
code: '<template><div @click="foo?.()"></div></template>',
options: ['never']
},
{
filename: 'test.vue',
code: `
<template><div @click="foo()" /></template>
<script>
export default {
methods: {
foo(a) {}
}
}
</script>`,
options: ['never']
},
{
filename: 'test.vue',
code: `
<template>
<div @click="foo()" />
<div @click="bar()" />
<div @click="baz()" />
</template>
<script>
export default {
methods: {
foo(a,b) {},
bar(...a) {},
baz(a = 42) {},
}
}
</script>`,
options: ['never']
},
{
filename: 'test.vue',
code: `
<template>
<div @click="foo()" />
<div @click="bar()" />
<div @click="baz()" />
</template>
<script>
export default {
methods: {
foo: (a,b) => {},
bar: (...a) => {},
baz: (a = 42) => {},
}
}
</script>`,
options: ['never']
}
],
invalid: [
Expand Down Expand Up @@ -255,6 +306,56 @@ 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: `
<template><div @click="foo()" /></template>
<script>
export default {
methods: {
foo() {}
}
}
</script>`,
output: `
<template><div @click="foo" /></template>
<script>
export default {
methods: {
foo() {}
}
}
</script>`,
errors: [
"Method calls without arguments inside of 'v-on' directives must not have parentheses."
],
options: ['never']
},
{
filename: 'test.vue',
code: `
<template><div @click="foo()" /></template>
<script>
export default {
methods: {
foo: () => {}
}
}
</script>`,
output: `
<template><div @click="foo" /></template>
<script>
export default {
methods: {
foo: () => {}
}
}
</script>`,
errors: [
"Method calls without arguments inside of 'v-on' directives must not have parentheses."
],
options: ['never']
}
]
})

0 comments on commit d9f72a5

Please sign in to comment.