next-tick-style.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /**
  2. * @fileoverview enforce Promise or callback style in `nextTick`
  3. * @author Flo Edelmann
  4. * @copyright 2020 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 {CallExpression|undefined}
  20. */
  21. function getVueNextTickCallExpression(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. identifier.parent.parent.type === 'CallExpression' &&
  28. identifier.parent.parent.callee === identifier.parent
  29. ) {
  30. return identifier.parent.parent
  31. }
  32. // Vue 2 Global API: Vue.nextTick()
  33. if (
  34. identifier.name === 'nextTick' &&
  35. identifier.parent.type === 'MemberExpression' &&
  36. identifier.parent.object.type === 'Identifier' &&
  37. identifier.parent.object.name === 'Vue' &&
  38. identifier.parent.parent.type === 'CallExpression' &&
  39. identifier.parent.parent.callee === identifier.parent
  40. ) {
  41. return identifier.parent.parent
  42. }
  43. // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
  44. if (
  45. identifier.parent.type === 'CallExpression' &&
  46. identifier.parent.callee === identifier
  47. ) {
  48. const variable = findVariable(context.getScope(), identifier)
  49. if (variable != null && variable.defs.length === 1) {
  50. const def = variable.defs[0]
  51. if (
  52. def.type === 'ImportBinding' &&
  53. def.node.type === 'ImportSpecifier' &&
  54. def.node.imported.type === 'Identifier' &&
  55. def.node.imported.name === 'nextTick' &&
  56. def.node.parent.type === 'ImportDeclaration' &&
  57. def.node.parent.source.value === 'vue'
  58. ) {
  59. return identifier.parent
  60. }
  61. }
  62. }
  63. return undefined
  64. }
  65. /**
  66. * @param {CallExpression} callExpression
  67. * @returns {boolean}
  68. */
  69. function isAwaitedPromise(callExpression) {
  70. return (
  71. callExpression.parent.type === 'AwaitExpression' ||
  72. (callExpression.parent.type === 'MemberExpression' &&
  73. callExpression.parent.property.type === 'Identifier' &&
  74. callExpression.parent.property.name === 'then')
  75. )
  76. }
  77. // ------------------------------------------------------------------------------
  78. // Rule Definition
  79. // ------------------------------------------------------------------------------
  80. module.exports = {
  81. meta: {
  82. type: 'suggestion',
  83. docs: {
  84. description: 'enforce Promise or callback style in `nextTick`',
  85. categories: undefined,
  86. url: 'https://eslint.vuejs.org/rules/next-tick-style.html'
  87. },
  88. fixable: 'code',
  89. schema: [{ enum: ['promise', 'callback'] }]
  90. },
  91. /** @param {RuleContext} context */
  92. create(context) {
  93. const preferredStyle =
  94. /** @type {string|undefined} */ (context.options[0]) || 'promise'
  95. return utils.defineVueVisitor(context, {
  96. /** @param {Identifier} node */
  97. Identifier(node) {
  98. const callExpression = getVueNextTickCallExpression(node, context)
  99. if (!callExpression) {
  100. return
  101. }
  102. if (preferredStyle === 'callback') {
  103. if (
  104. callExpression.arguments.length !== 1 ||
  105. isAwaitedPromise(callExpression)
  106. ) {
  107. context.report({
  108. node,
  109. message:
  110. 'Pass a callback function to `nextTick` instead of using the returned Promise.'
  111. })
  112. }
  113. return
  114. }
  115. if (
  116. callExpression.arguments.length !== 0 ||
  117. !isAwaitedPromise(callExpression)
  118. ) {
  119. context.report({
  120. node,
  121. message:
  122. 'Use the Promise returned by `nextTick` instead of passing a callback function.',
  123. fix(fixer) {
  124. return fixer.insertTextAfter(node, '().then')
  125. }
  126. })
  127. }
  128. }
  129. })
  130. }
  131. }