no-lifecycle-after-await.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const { ReferenceTracker } = require('eslint-utils')
  7. const utils = require('../utils')
  8. /**
  9. * @typedef {import('eslint-utils').TYPES.TraceMap} TraceMap
  10. */
  11. const LIFECYCLE_HOOKS = [
  12. 'onBeforeMount',
  13. 'onBeforeUnmount',
  14. 'onBeforeUpdate',
  15. 'onErrorCaptured',
  16. 'onMounted',
  17. 'onRenderTracked',
  18. 'onRenderTriggered',
  19. 'onUnmounted',
  20. 'onUpdated',
  21. 'onActivated',
  22. 'onDeactivated'
  23. ]
  24. module.exports = {
  25. meta: {
  26. type: 'suggestion',
  27. docs: {
  28. description: 'disallow asynchronously registered lifecycle hooks',
  29. categories: ['vue3-essential'],
  30. url: 'https://eslint.vuejs.org/rules/no-lifecycle-after-await.html'
  31. },
  32. fixable: null,
  33. schema: [],
  34. messages: {
  35. forbidden: 'The lifecycle hooks after `await` expression are forbidden.'
  36. }
  37. },
  38. /** @param {RuleContext} context */
  39. create(context) {
  40. /**
  41. * @typedef {object} SetupFunctionData
  42. * @property {Property} setupProperty
  43. * @property {boolean} afterAwait
  44. */
  45. /**
  46. * @typedef {object} ScopeStack
  47. * @property {ScopeStack | null} upper
  48. * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} functionNode
  49. */
  50. /** @type {Set<ESNode>} */
  51. const lifecycleHookCallNodes = new Set()
  52. /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupFunctionData>} */
  53. const setupFunctions = new Map()
  54. /** @type {ScopeStack | null} */
  55. let scopeStack = null
  56. return Object.assign(
  57. {
  58. Program() {
  59. const tracker = new ReferenceTracker(context.getScope())
  60. const traceMap = {
  61. /** @type {TraceMap} */
  62. vue: {
  63. [ReferenceTracker.ESM]: true
  64. }
  65. }
  66. for (const lifecycleHook of LIFECYCLE_HOOKS) {
  67. traceMap.vue[lifecycleHook] = {
  68. [ReferenceTracker.CALL]: true
  69. }
  70. }
  71. for (const { node } of tracker.iterateEsmReferences(traceMap)) {
  72. lifecycleHookCallNodes.add(node)
  73. }
  74. }
  75. },
  76. utils.defineVueVisitor(context, {
  77. ':function'(node) {
  78. scopeStack = {
  79. upper: scopeStack,
  80. functionNode: node
  81. }
  82. },
  83. onSetupFunctionEnter(node) {
  84. setupFunctions.set(node, {
  85. setupProperty: node.parent,
  86. afterAwait: false
  87. })
  88. },
  89. AwaitExpression() {
  90. if (!scopeStack) {
  91. return
  92. }
  93. const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
  94. if (!setupFunctionData) {
  95. return
  96. }
  97. setupFunctionData.afterAwait = true
  98. },
  99. CallExpression(node) {
  100. if (!scopeStack) {
  101. return
  102. }
  103. const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
  104. if (!setupFunctionData || !setupFunctionData.afterAwait) {
  105. return
  106. }
  107. if (lifecycleHookCallNodes.has(node)) {
  108. if (node.arguments.length >= 2) {
  109. // Has target instance. e.g. `onMounted(() => {}, instance)`
  110. return
  111. }
  112. context.report({
  113. node,
  114. messageId: 'forbidden'
  115. })
  116. }
  117. },
  118. ':function:exit'(node) {
  119. scopeStack = scopeStack && scopeStack.upper
  120. setupFunctions.delete(node)
  121. }
  122. })
  123. )
  124. }
  125. }