v-on-function-call.js 6.0 KB

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