v-on-function-call.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. messages: {
  82. always: "Method calls inside of 'v-on' directives must have parentheses.",
  83. never:
  84. "Method calls without arguments inside of 'v-on' directives must not have parentheses."
  85. },
  86. deprecated: true,
  87. replacedBy: ['v-on-handler-style']
  88. },
  89. /** @param {RuleContext} context */
  90. create(context) {
  91. const always = context.options[0] === 'always'
  92. if (always) {
  93. return utils.defineTemplateBodyVisitor(context, {
  94. /** @param {Identifier} node */
  95. "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
  96. node
  97. ) {
  98. context.report({
  99. node,
  100. messageId: 'always'
  101. })
  102. }
  103. })
  104. }
  105. const option = context.options[1] || {}
  106. const ignoreIncludesComment = !!option.ignoreIncludesComment
  107. /** @type {Set<string>} */
  108. const useArgsMethods = new Set()
  109. return utils.defineTemplateBodyVisitor(
  110. context,
  111. {
  112. /** @param {VOnExpression} node */
  113. "VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"(
  114. node
  115. ) {
  116. const expression = getInvalidNeverCallExpression(node)
  117. if (!expression) {
  118. return
  119. }
  120. const tokenStore = context.parserServices.getTemplateBodyTokenStore()
  121. const tokens = tokenStore.getTokens(node.parent, {
  122. includeComments: true
  123. })
  124. /** @type {Token | undefined} */
  125. let leftQuote
  126. /** @type {Token | undefined} */
  127. let rightQuote
  128. if (isQuote(tokens[0])) {
  129. leftQuote = tokens.shift()
  130. rightQuote = tokens.pop()
  131. }
  132. const hasComment = tokens.some(
  133. (token) => token.type === 'Block' || token.type === 'Line'
  134. )
  135. if (ignoreIncludesComment && hasComment) {
  136. return
  137. }
  138. if (
  139. expression.callee.type === 'Identifier' &&
  140. useArgsMethods.has(expression.callee.name)
  141. ) {
  142. // The behavior of target method can change given the arguments.
  143. return
  144. }
  145. context.report({
  146. node: expression,
  147. messageId: 'never',
  148. fix: hasComment
  149. ? null /* The comment is included and cannot be fixed. */
  150. : (fixer) => {
  151. /** @type {Range} */
  152. const range =
  153. leftQuote && rightQuote
  154. ? [leftQuote.range[1], rightQuote.range[0]]
  155. : [tokens[0].range[0], tokens[tokens.length - 1].range[1]]
  156. return fixer.replaceTextRange(
  157. range,
  158. context.getSourceCode().getText(expression.callee)
  159. )
  160. }
  161. })
  162. }
  163. },
  164. utils.defineVueVisitor(context, {
  165. onVueObjectEnter(node) {
  166. for (const method of utils.iterateProperties(
  167. node,
  168. new Set(['methods'])
  169. )) {
  170. if (useArgsMethods.has(method.name)) {
  171. continue
  172. }
  173. if (method.type !== 'object') {
  174. continue
  175. }
  176. const value = method.property.value
  177. if (
  178. (value.type === 'FunctionExpression' ||
  179. value.type === 'ArrowFunctionExpression') &&
  180. value.params.length > 0
  181. ) {
  182. useArgsMethods.add(method.name)
  183. }
  184. }
  185. }
  186. })
  187. )
  188. }
  189. }