valid-next-tick.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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 === 'MemberExpression' &&
  71. callExpression.parent.property.type === 'Identifier' &&
  72. callExpression.parent.property.name === 'then'
  73. ) {
  74. // cases like `nextTick().then()`
  75. return true
  76. }
  77. if (
  78. callExpression.parent.type === 'VariableDeclarator' ||
  79. callExpression.parent.type === 'AssignmentExpression'
  80. ) {
  81. // cases like `let foo = nextTick()` or `foo = nextTick()`
  82. return true
  83. }
  84. if (
  85. callExpression.parent.type === 'ArrayExpression' &&
  86. callExpression.parent.parent.type === 'CallExpression' &&
  87. callExpression.parent.parent.callee.type === 'MemberExpression' &&
  88. callExpression.parent.parent.callee.object.type === 'Identifier' &&
  89. callExpression.parent.parent.callee.object.name === 'Promise' &&
  90. callExpression.parent.parent.callee.property.type === 'Identifier'
  91. ) {
  92. // cases like `Promise.all([nextTick()])`
  93. return true
  94. }
  95. return false
  96. }
  97. // ------------------------------------------------------------------------------
  98. // Rule Definition
  99. // ------------------------------------------------------------------------------
  100. module.exports = {
  101. meta: {
  102. hasSuggestions: true,
  103. type: 'problem',
  104. docs: {
  105. description: 'enforce valid `nextTick` function calls',
  106. // categories: ['vue3-essential', 'essential'],
  107. categories: undefined,
  108. url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
  109. },
  110. fixable: 'code',
  111. schema: []
  112. },
  113. /** @param {RuleContext} context */
  114. create(context) {
  115. return utils.defineVueVisitor(context, {
  116. /** @param {Identifier} node */
  117. Identifier(node) {
  118. const nextTickNode = getVueNextTickNode(node, context)
  119. if (!nextTickNode || !nextTickNode.parent) {
  120. return
  121. }
  122. const parentNode = nextTickNode.parent
  123. if (
  124. parentNode.type === 'CallExpression' &&
  125. parentNode.callee !== nextTickNode
  126. ) {
  127. // cases like `foo.then(nextTick)` are allowed
  128. return
  129. }
  130. if (
  131. parentNode.type === 'VariableDeclarator' ||
  132. parentNode.type === 'AssignmentExpression'
  133. ) {
  134. // cases like `let foo = nextTick` or `foo = nextTick` are allowed
  135. return
  136. }
  137. if (parentNode.type !== 'CallExpression') {
  138. context.report({
  139. node,
  140. message: '`nextTick` is a function.',
  141. fix(fixer) {
  142. return fixer.insertTextAfter(node, '()')
  143. }
  144. })
  145. return
  146. }
  147. if (parentNode.arguments.length === 0) {
  148. if (!isAwaitedPromise(parentNode)) {
  149. context.report({
  150. node,
  151. message:
  152. 'Await the Promise returned by `nextTick` or pass a callback function.',
  153. suggest: [
  154. {
  155. desc: 'Add missing `await` statement.',
  156. fix(fixer) {
  157. return fixer.insertTextBefore(parentNode, 'await ')
  158. }
  159. }
  160. ]
  161. })
  162. }
  163. return
  164. }
  165. if (parentNode.arguments.length > 1) {
  166. context.report({
  167. node,
  168. message: '`nextTick` expects zero or one parameters.'
  169. })
  170. return
  171. }
  172. if (isAwaitedPromise(parentNode)) {
  173. context.report({
  174. node,
  175. message:
  176. 'Either await the Promise or pass a callback function to `nextTick`.'
  177. })
  178. }
  179. }
  180. })
  181. }
  182. }