valid-next-tick.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /**
  2. * @fileoverview enforce valid `nextTick` function calls
  3. * @author Flo Edelmann
  4. * @copyright 2021 Flo Edelmann. All rights reserved.
  5. * See LICENSE file in root directory for full license.
  6. */
  7. 'use strict'
  8. const utils = require('../utils')
  9. const { findVariable } = require('@eslint-community/eslint-utils')
  10. /**
  11. * @param {Identifier} identifier
  12. * @param {RuleContext} context
  13. * @returns {ASTNode|undefined}
  14. */
  15. function getVueNextTickNode(identifier, context) {
  16. // Instance API: this.$nextTick()
  17. if (
  18. identifier.name === '$nextTick' &&
  19. identifier.parent.type === 'MemberExpression' &&
  20. utils.isThis(identifier.parent.object, context)
  21. ) {
  22. return identifier.parent
  23. }
  24. // Vue 2 Global API: Vue.nextTick()
  25. if (
  26. identifier.name === 'nextTick' &&
  27. identifier.parent.type === 'MemberExpression' &&
  28. identifier.parent.object.type === 'Identifier' &&
  29. identifier.parent.object.name === 'Vue'
  30. ) {
  31. return identifier.parent
  32. }
  33. // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
  34. const variable = findVariable(context.getScope(), identifier)
  35. if (variable != null && variable.defs.length === 1) {
  36. const def = variable.defs[0]
  37. if (
  38. def.type === 'ImportBinding' &&
  39. def.node.type === 'ImportSpecifier' &&
  40. def.node.imported.type === 'Identifier' &&
  41. def.node.imported.name === 'nextTick' &&
  42. def.node.parent.type === 'ImportDeclaration' &&
  43. def.node.parent.source.value === 'vue'
  44. ) {
  45. return identifier
  46. }
  47. }
  48. return undefined
  49. }
  50. /**
  51. * @param {CallExpression} callExpression
  52. * @returns {boolean}
  53. */
  54. function isAwaitedPromise(callExpression) {
  55. if (callExpression.parent.type === 'AwaitExpression') {
  56. // cases like `await nextTick()`
  57. return true
  58. }
  59. if (callExpression.parent.type === 'ReturnStatement') {
  60. // cases like `return nextTick()`
  61. return true
  62. }
  63. if (
  64. callExpression.parent.type === 'ArrowFunctionExpression' &&
  65. callExpression.parent.body === callExpression
  66. ) {
  67. // cases like `() => nextTick()`
  68. return true
  69. }
  70. if (
  71. callExpression.parent.type === 'MemberExpression' &&
  72. callExpression.parent.property.type === 'Identifier' &&
  73. callExpression.parent.property.name === 'then'
  74. ) {
  75. // cases like `nextTick().then()`
  76. return true
  77. }
  78. if (
  79. callExpression.parent.type === 'VariableDeclarator' ||
  80. callExpression.parent.type === 'AssignmentExpression'
  81. ) {
  82. // cases like `let foo = nextTick()` or `foo = nextTick()`
  83. return true
  84. }
  85. if (
  86. callExpression.parent.type === 'ArrayExpression' &&
  87. callExpression.parent.parent.type === 'CallExpression' &&
  88. callExpression.parent.parent.callee.type === 'MemberExpression' &&
  89. callExpression.parent.parent.callee.object.type === 'Identifier' &&
  90. callExpression.parent.parent.callee.object.name === 'Promise' &&
  91. callExpression.parent.parent.callee.property.type === 'Identifier'
  92. ) {
  93. // cases like `Promise.all([nextTick()])`
  94. return true
  95. }
  96. return false
  97. }
  98. module.exports = {
  99. meta: {
  100. hasSuggestions: true,
  101. type: 'problem',
  102. docs: {
  103. description: 'enforce valid `nextTick` function calls',
  104. categories: ['vue3-essential', 'essential'],
  105. url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
  106. },
  107. fixable: 'code',
  108. schema: []
  109. },
  110. /** @param {RuleContext} context */
  111. create(context) {
  112. return utils.defineVueVisitor(context, {
  113. /** @param {Identifier} node */
  114. Identifier(node) {
  115. const nextTickNode = getVueNextTickNode(node, context)
  116. if (!nextTickNode || !nextTickNode.parent) {
  117. return
  118. }
  119. let parentNode = nextTickNode.parent
  120. // skip conditional expressions like `foo ? nextTick : bar`
  121. if (parentNode.type === 'ConditionalExpression') {
  122. parentNode = parentNode.parent
  123. }
  124. if (
  125. parentNode.type === 'CallExpression' &&
  126. parentNode.callee !== nextTickNode
  127. ) {
  128. // cases like `foo.then(nextTick)` are allowed
  129. return
  130. }
  131. if (
  132. parentNode.type === 'VariableDeclarator' ||
  133. parentNode.type === 'AssignmentExpression'
  134. ) {
  135. // cases like `let foo = nextTick` or `foo = nextTick` are allowed
  136. return
  137. }
  138. if (parentNode.type !== 'CallExpression') {
  139. context.report({
  140. node,
  141. message: '`nextTick` is a function.',
  142. fix(fixer) {
  143. return fixer.insertTextAfter(node, '()')
  144. }
  145. })
  146. return
  147. }
  148. if (parentNode.arguments.length === 0) {
  149. if (!isAwaitedPromise(parentNode)) {
  150. context.report({
  151. node,
  152. message:
  153. 'Await the Promise returned by `nextTick` or pass a callback function.',
  154. suggest: [
  155. {
  156. desc: 'Add missing `await` statement.',
  157. fix(fixer) {
  158. return fixer.insertTextBefore(parentNode, 'await ')
  159. }
  160. }
  161. ]
  162. })
  163. }
  164. return
  165. }
  166. if (parentNode.arguments.length > 1) {
  167. context.report({
  168. node,
  169. message: '`nextTick` expects zero or one parameters.'
  170. })
  171. return
  172. }
  173. if (isAwaitedPromise(parentNode)) {
  174. context.report({
  175. node,
  176. message:
  177. 'Either await the Promise or pass a callback function to `nextTick`.'
  178. })
  179. }
  180. }
  181. })
  182. }
  183. }