no-expose-after-await.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const { findVariable } = require('eslint-utils')
  10. const utils = require('../utils')
  11. // ------------------------------------------------------------------------------
  12. // Helpers
  13. // ------------------------------------------------------------------------------
  14. /**
  15. * Get the callee member node from the given CallExpression
  16. * @param {CallExpression} node CallExpression
  17. */
  18. function getCalleeMemberNode(node) {
  19. const callee = utils.skipChainExpression(node.callee)
  20. if (callee.type === 'MemberExpression') {
  21. const name = utils.getStaticPropertyName(callee)
  22. if (name) {
  23. return { name, member: callee }
  24. }
  25. }
  26. return null
  27. }
  28. // ------------------------------------------------------------------------------
  29. // Rule Definition
  30. // ------------------------------------------------------------------------------
  31. module.exports = {
  32. meta: {
  33. type: 'problem',
  34. docs: {
  35. description: 'disallow asynchronously registered `expose`',
  36. categories: undefined,
  37. // categories: ['vue3-essential'], TODO Change with the major version
  38. url: 'https://eslint.vuejs.org/rules/no-expose-after-await.html'
  39. },
  40. fixable: null,
  41. schema: [],
  42. messages: {
  43. forbidden: 'The `expose` after `await` expression are forbidden.'
  44. }
  45. },
  46. /** @param {RuleContext} context */
  47. create(context) {
  48. /**
  49. * @typedef {object} SetupScopeData
  50. * @property {boolean} afterAwait
  51. * @property {[number,number]} range
  52. * @property {Set<Identifier>} exposeReferenceIds
  53. * @property {Set<Identifier>} contextReferenceIds
  54. */
  55. /**
  56. * @typedef {object} ScopeStack
  57. * @property {ScopeStack | null} upper
  58. * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
  59. */
  60. /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, SetupScopeData>} */
  61. const setupScopes = new Map()
  62. /** @type {ScopeStack | null} */
  63. let scopeStack = null
  64. return utils.defineVueVisitor(context, {
  65. onSetupFunctionEnter(node) {
  66. const contextParam = node.params[1]
  67. if (!contextParam) {
  68. // no arguments
  69. return
  70. }
  71. if (contextParam.type === 'RestElement') {
  72. // cannot check
  73. return
  74. }
  75. if (contextParam.type === 'ArrayPattern') {
  76. // cannot check
  77. return
  78. }
  79. /** @type {Set<Identifier>} */
  80. const contextReferenceIds = new Set()
  81. /** @type {Set<Identifier>} */
  82. const exposeReferenceIds = new Set()
  83. if (contextParam.type === 'ObjectPattern') {
  84. const exposeProperty = utils.findAssignmentProperty(
  85. contextParam,
  86. 'expose'
  87. )
  88. if (!exposeProperty) {
  89. return
  90. }
  91. const exposeParam = exposeProperty.value
  92. // `setup(props, {emit})`
  93. const variable =
  94. exposeParam.type === 'Identifier'
  95. ? findVariable(context.getScope(), exposeParam)
  96. : null
  97. if (!variable) {
  98. return
  99. }
  100. for (const reference of variable.references) {
  101. if (!reference.isRead()) {
  102. continue
  103. }
  104. exposeReferenceIds.add(reference.identifier)
  105. }
  106. } else if (contextParam.type === 'Identifier') {
  107. // `setup(props, context)`
  108. const variable = findVariable(context.getScope(), contextParam)
  109. if (!variable) {
  110. return
  111. }
  112. for (const reference of variable.references) {
  113. if (!reference.isRead()) {
  114. continue
  115. }
  116. contextReferenceIds.add(reference.identifier)
  117. }
  118. }
  119. setupScopes.set(node, {
  120. afterAwait: false,
  121. range: node.range,
  122. exposeReferenceIds,
  123. contextReferenceIds
  124. })
  125. },
  126. /**
  127. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  128. */
  129. ':function'(node) {
  130. scopeStack = {
  131. upper: scopeStack,
  132. scopeNode: node
  133. }
  134. },
  135. ':function:exit'() {
  136. scopeStack = scopeStack && scopeStack.upper
  137. },
  138. /** @param {AwaitExpression} node */
  139. AwaitExpression(node) {
  140. if (!scopeStack) {
  141. return
  142. }
  143. const setupScope = setupScopes.get(scopeStack.scopeNode)
  144. if (!setupScope || !utils.inRange(setupScope.range, node)) {
  145. return
  146. }
  147. setupScope.afterAwait = true
  148. },
  149. /** @param {CallExpression} node */
  150. CallExpression(node) {
  151. if (!scopeStack) {
  152. return
  153. }
  154. const setupScope = setupScopes.get(scopeStack.scopeNode)
  155. if (
  156. !setupScope ||
  157. !setupScope.afterAwait ||
  158. !utils.inRange(setupScope.range, node)
  159. ) {
  160. return
  161. }
  162. const { contextReferenceIds, exposeReferenceIds } = setupScope
  163. if (
  164. node.callee.type === 'Identifier' &&
  165. exposeReferenceIds.has(node.callee)
  166. ) {
  167. // setup(props,{expose}) {expose()}
  168. context.report({
  169. node,
  170. messageId: 'forbidden'
  171. })
  172. } else {
  173. const expose = getCalleeMemberNode(node)
  174. if (
  175. expose &&
  176. expose.name === 'expose' &&
  177. expose.member.object.type === 'Identifier' &&
  178. contextReferenceIds.has(expose.member.object)
  179. ) {
  180. // setup(props,context) {context.emit()}
  181. context.report({
  182. node,
  183. messageId: 'forbidden'
  184. })
  185. }
  186. }
  187. },
  188. onSetupFunctionExit(node) {
  189. setupScopes.delete(node)
  190. }
  191. })
  192. }
  193. }