valid-next-tick.js 6.0 KB

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