v-on-function-call.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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. deprecated: true,
  70. replacedBy: ['v-on-handler-style'],
  71. schema: [
  72. { enum: ['always', 'never'] },
  73. {
  74. type: 'object',
  75. properties: {
  76. ignoreIncludesComment: {
  77. type: 'boolean'
  78. }
  79. },
  80. additionalProperties: false
  81. }
  82. ],
  83. messages: {
  84. always: "Method calls inside of 'v-on' directives must have parentheses.",
  85. never:
  86. "Method calls without arguments inside of 'v-on' directives must not have parentheses."
  87. }
  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 sourceCode = context.getSourceCode()
  121. const tokenStore =
  122. sourceCode.parserServices.getTemplateBodyTokenStore()
  123. const tokens = tokenStore.getTokens(node.parent, {
  124. includeComments: true
  125. })
  126. /** @type {Token | undefined} */
  127. let leftQuote
  128. /** @type {Token | undefined} */
  129. let rightQuote
  130. if (isQuote(tokens[0])) {
  131. leftQuote = tokens.shift()
  132. rightQuote = tokens.pop()
  133. }
  134. const hasComment = tokens.some(
  135. (token) => token.type === 'Block' || token.type === 'Line'
  136. )
  137. if (ignoreIncludesComment && hasComment) {
  138. return
  139. }
  140. if (
  141. expression.callee.type === 'Identifier' &&
  142. useArgsMethods.has(expression.callee.name)
  143. ) {
  144. // The behavior of target method can change given the arguments.
  145. return
  146. }
  147. context.report({
  148. node: expression,
  149. messageId: 'never',
  150. fix: hasComment
  151. ? null /* The comment is included and cannot be fixed. */
  152. : (fixer) => {
  153. /** @type {Range} */
  154. const range =
  155. leftQuote && rightQuote
  156. ? [leftQuote.range[1], rightQuote.range[0]]
  157. : [tokens[0].range[0], tokens[tokens.length - 1].range[1]]
  158. return fixer.replaceTextRange(
  159. range,
  160. context.getSourceCode().getText(expression.callee)
  161. )
  162. }
  163. })
  164. }
  165. },
  166. utils.defineVueVisitor(context, {
  167. onVueObjectEnter(node) {
  168. for (const method of utils.iterateProperties(
  169. node,
  170. new Set(['methods'])
  171. )) {
  172. if (useArgsMethods.has(method.name)) {
  173. continue
  174. }
  175. if (method.type !== 'object') {
  176. continue
  177. }
  178. const value = method.property.value
  179. if (
  180. (value.type === 'FunctionExpression' ||
  181. value.type === 'ArrowFunctionExpression') &&
  182. value.params.length > 0
  183. ) {
  184. useArgsMethods.add(method.name)
  185. }
  186. }
  187. }
  188. })
  189. )
  190. }
  191. }