valid-next-tick.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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(utils.getScope(context, identifier), 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. type: 'problem',
  101. docs: {
  102. description: 'enforce valid `nextTick` function calls',
  103. categories: ['vue3-essential', 'vue2-essential'],
  104. url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
  105. },
  106. fixable: 'code',
  107. hasSuggestions: true,
  108. schema: [],
  109. messages: {
  110. shouldBeFunction: '`nextTick` is a function.',
  111. missingCallbackOrAwait:
  112. 'Await the Promise returned by `nextTick` or pass a callback function.',
  113. addAwait: 'Add missing `await` statement.',
  114. tooManyParameters: '`nextTick` expects zero or one parameters.',
  115. eitherAwaitOrCallback:
  116. 'Either await the Promise or pass a callback function to `nextTick`.'
  117. }
  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. messageId: 'shouldBeFunction',
  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. messageId: 'missingCallbackOrAwait',
  162. suggest: [
  163. {
  164. messageId: 'addAwait',
  165. fix(fixer) {
  166. return fixer.insertTextBefore(parentNode, 'await ')
  167. }
  168. }
  169. ]
  170. })
  171. }
  172. return
  173. }
  174. if (parentNode.arguments.length > 1) {
  175. context.report({
  176. node,
  177. messageId: 'tooManyParameters'
  178. })
  179. return
  180. }
  181. if (isAwaitedPromise(parentNode)) {
  182. context.report({
  183. node,
  184. messageId: 'eitherAwaitOrCallback'
  185. })
  186. }
  187. }
  188. })
  189. }
  190. }