v-on-function-call.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. /**
  2. * @author Niklas Higi
  3. */
  4. 'use strict'
  5. const utils = require('../utils')
  6. /**
  7. * @typedef { import('../utils').ComponentPropertyData } ComponentPropertyData
  8. */
  9. /**
  10. * Check whether the given token is a quote.
  11. * @param {Token} token The token to check.
  12. * @returns {boolean} `true` if the token is a quote.
  13. */
  14. function isQuote(token) {
  15. return (
  16. token != null &&
  17. token.type === 'Punctuator' &&
  18. (token.value === '"' || token.value === "'")
  19. )
  20. }
  21. /**
  22. * @param {VOnExpression} node
  23. * @returns {CallExpression | null}
  24. */
  25. function getInvalidNeverCallExpression(node) {
  26. /** @type {ExpressionStatement} */
  27. let exprStatement
  28. let body = node.body
  29. while (true) {
  30. const statements = body.filter((st) => st.type !== 'EmptyStatement')
  31. if (statements.length !== 1) {
  32. return null
  33. }
  34. const statement = statements[0]
  35. if (statement.type === 'ExpressionStatement') {
  36. exprStatement = statement
  37. break
  38. }
  39. if (statement.type === 'BlockStatement') {
  40. body = statement.body
  41. continue
  42. }
  43. return null
  44. }
  45. const expression = exprStatement.expression
  46. if (expression.type !== 'CallExpression' || expression.arguments.length > 0) {
  47. return null
  48. }
  49. if (expression.optional) {
  50. // Allow optional chaining
  51. return null
  52. }
  53. const callee = expression.callee
  54. if (callee.type !== 'Identifier') {
  55. return null
  56. }
  57. return expression
  58. }
  59. module.exports = {
  60. meta: {
  61. type: 'suggestion',
  62. docs: {
  63. description:
  64. 'enforce or forbid parentheses after method calls without arguments in `v-on` directives',
  65. categories: undefined,
  66. url: 'https://eslint.vuejs.org/rules/v-on-function-call.html'
  67. },
  68. fixable: 'code',
  69. schema: [
  70. { enum: ['always', 'never'] },
  71. {
  72. type: 'object',
  73. properties: {
  74. ignoreIncludesComment: {
  75. type: 'boolean'
  76. }
  77. },
  78. additionalProperties: false
  79. }
  80. ],
  81. deprecated: true,
  82. replacedBy: ['v-on-handler-style']
  83. },
  84. /** @param {RuleContext} context */
  85. create(context) {
  86. const always = context.options[0] === 'always'
  87. if (always) {
  88. return utils.defineTemplateBodyVisitor(context, {
  89. /** @param {Identifier} node */
  90. "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
  91. node
  92. ) {
  93. context.report({
  94. node,
  95. message:
  96. "Method calls inside of 'v-on' directives must have parentheses."
  97. })
  98. }
  99. })
  100. }
  101. const option = context.options[1] || {}
  102. const ignoreIncludesComment = !!option.ignoreIncludesComment
  103. /** @type {Set<string>} */
  104. const useArgsMethods = new Set()
  105. return utils.defineTemplateBodyVisitor(
  106. context,
  107. {
  108. /** @param {VOnExpression} node */
  109. "VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"(
  110. node
  111. ) {
  112. const expression = getInvalidNeverCallExpression(node)
  113. if (!expression) {
  114. return
  115. }
  116. const tokenStore = context.parserServices.getTemplateBodyTokenStore()
  117. const tokens = tokenStore.getTokens(node.parent, {
  118. includeComments: true
  119. })
  120. /** @type {Token | undefined} */
  121. let leftQuote
  122. /** @type {Token | undefined} */
  123. let rightQuote
  124. if (isQuote(tokens[0])) {
  125. leftQuote = tokens.shift()
  126. rightQuote = tokens.pop()
  127. }
  128. const hasComment = tokens.some(
  129. (token) => token.type === 'Block' || token.type === 'Line'
  130. )
  131. if (ignoreIncludesComment && hasComment) {
  132. return
  133. }
  134. if (
  135. expression.callee.type === 'Identifier' &&
  136. useArgsMethods.has(expression.callee.name)
  137. ) {
  138. // The behavior of target method can change given the arguments.
  139. return
  140. }
  141. context.report({
  142. node: expression,
  143. message:
  144. "Method calls without arguments inside of 'v-on' directives must not have parentheses.",
  145. fix: hasComment
  146. ? null /* The comment is included and cannot be fixed. */
  147. : (fixer) => {
  148. /** @type {Range} */
  149. const range =
  150. leftQuote && rightQuote
  151. ? [leftQuote.range[1], rightQuote.range[0]]
  152. : [tokens[0].range[0], tokens[tokens.length - 1].range[1]]
  153. return fixer.replaceTextRange(
  154. range,
  155. context.getSourceCode().getText(expression.callee)
  156. )
  157. }
  158. })
  159. }
  160. },
  161. utils.defineVueVisitor(context, {
  162. onVueObjectEnter(node) {
  163. for (const method of utils.iterateProperties(
  164. node,
  165. new Set(['methods'])
  166. )) {
  167. if (useArgsMethods.has(method.name)) {
  168. continue
  169. }
  170. if (method.type !== 'object') {
  171. continue
  172. }
  173. const value = method.property.value
  174. if (
  175. (value.type === 'FunctionExpression' ||
  176. value.type === 'ArrowFunctionExpression') &&
  177. value.params.length > 0
  178. ) {
  179. useArgsMethods.add(method.name)
  180. }
  181. }
  182. }
  183. })
  184. )
  185. }
  186. }