next-tick-style.js 3.8 KB

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