no-setup-props-destructure.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const { findVariable } = require('eslint-utils')
  7. const utils = require('../utils')
  8. module.exports = {
  9. meta: {
  10. type: 'suggestion',
  11. docs: {
  12. description: 'disallow destructuring of `props` passed to `setup`',
  13. categories: ['vue3-essential'],
  14. url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html'
  15. },
  16. fixable: null,
  17. schema: [],
  18. messages: {
  19. destructuring:
  20. 'Destructuring the `props` will cause the value to lose reactivity.',
  21. getProperty:
  22. 'Getting a value from the `props` in root scope of `{{scopeName}}` will cause the value to lose reactivity.'
  23. }
  24. },
  25. /** @param {RuleContext} context */
  26. create(context) {
  27. /**
  28. * @typedef {object} ScopePropsReferences
  29. * @property {Set<Identifier>} refs
  30. * @property {string} scopeName
  31. */
  32. /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences>} */
  33. const setupScopePropsReferenceIds = new Map()
  34. /**
  35. * @param {ESNode} node
  36. * @param {string} messageId
  37. * @param {string} scopeName
  38. */
  39. function report(node, messageId, scopeName) {
  40. context.report({
  41. node,
  42. messageId,
  43. data: {
  44. scopeName
  45. }
  46. })
  47. }
  48. /**
  49. * @param {Pattern} left
  50. * @param {Expression | null} right
  51. * @param {ScopePropsReferences} propsReferences
  52. */
  53. function verify(left, right, propsReferences) {
  54. if (!right) {
  55. return
  56. }
  57. const rightNode = utils.skipChainExpression(right)
  58. if (
  59. left.type !== 'ArrayPattern' &&
  60. left.type !== 'ObjectPattern' &&
  61. rightNode.type !== 'MemberExpression'
  62. ) {
  63. return
  64. }
  65. /** @type {Expression | Super} */
  66. let rightId = rightNode
  67. while (rightId.type === 'MemberExpression') {
  68. rightId = utils.skipChainExpression(rightId.object)
  69. }
  70. if (rightId.type === 'Identifier' && propsReferences.refs.has(rightId)) {
  71. report(left, 'getProperty', propsReferences.scopeName)
  72. }
  73. }
  74. /**
  75. * @typedef {object} ScopeStack
  76. * @property {ScopeStack | null} upper
  77. * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
  78. */
  79. /**
  80. * @type {ScopeStack | null}
  81. */
  82. let scopeStack = null
  83. /**
  84. * @param {Pattern | null} node
  85. * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
  86. * @param {string} scopeName
  87. */
  88. function processPattern(node, scopeNode, scopeName) {
  89. if (!node) {
  90. // no arguments
  91. return
  92. }
  93. if (
  94. node.type === 'RestElement' ||
  95. node.type === 'AssignmentPattern' ||
  96. node.type === 'MemberExpression'
  97. ) {
  98. // cannot check
  99. return
  100. }
  101. if (node.type === 'ArrayPattern' || node.type === 'ObjectPattern') {
  102. report(node, 'destructuring', scopeName)
  103. return
  104. }
  105. const variable = findVariable(context.getScope(), node)
  106. if (!variable) {
  107. return
  108. }
  109. const propsReferenceIds = new Set()
  110. for (const reference of variable.references) {
  111. if (!reference.isRead()) {
  112. continue
  113. }
  114. propsReferenceIds.add(reference.identifier)
  115. }
  116. setupScopePropsReferenceIds.set(scopeNode, {
  117. refs: propsReferenceIds,
  118. scopeName
  119. })
  120. }
  121. return utils.compositingVisitors(
  122. {
  123. /**
  124. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
  125. */
  126. 'Program, :function'(node) {
  127. scopeStack = {
  128. upper: scopeStack,
  129. scopeNode: node
  130. }
  131. },
  132. /**
  133. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
  134. */
  135. 'Program, :function:exit'(node) {
  136. scopeStack = scopeStack && scopeStack.upper
  137. setupScopePropsReferenceIds.delete(node)
  138. },
  139. /**
  140. * @param {VariableDeclarator} node
  141. */
  142. VariableDeclarator(node) {
  143. if (!scopeStack) {
  144. return
  145. }
  146. const propsReferenceIds = setupScopePropsReferenceIds.get(
  147. scopeStack.scopeNode
  148. )
  149. if (!propsReferenceIds) {
  150. return
  151. }
  152. verify(node.id, node.init, propsReferenceIds)
  153. },
  154. /**
  155. * @param {AssignmentExpression} node
  156. */
  157. AssignmentExpression(node) {
  158. if (!scopeStack) {
  159. return
  160. }
  161. const propsReferenceIds = setupScopePropsReferenceIds.get(
  162. scopeStack.scopeNode
  163. )
  164. if (!propsReferenceIds) {
  165. return
  166. }
  167. verify(node.left, node.right, propsReferenceIds)
  168. }
  169. },
  170. utils.defineScriptSetupVisitor(context, {
  171. onDefinePropsEnter(node) {
  172. let target = node
  173. if (
  174. target.parent &&
  175. target.parent.type === 'CallExpression' &&
  176. target.parent.arguments[0] === target &&
  177. target.parent.callee.type === 'Identifier' &&
  178. target.parent.callee.name === 'withDefaults'
  179. ) {
  180. target = target.parent
  181. }
  182. if (!target.parent) {
  183. return
  184. }
  185. /** @type {Pattern|null} */
  186. let id = null
  187. if (target.parent.type === 'VariableDeclarator') {
  188. id = target.parent.init === target ? target.parent.id : null
  189. } else if (target.parent.type === 'AssignmentExpression') {
  190. id = target.parent.right === target ? target.parent.left : null
  191. }
  192. processPattern(id, context.getSourceCode().ast, '<script setup>')
  193. }
  194. }),
  195. utils.defineVueVisitor(context, {
  196. onSetupFunctionEnter(node) {
  197. const propsParam = utils.skipDefaultParamValue(node.params[0])
  198. processPattern(propsParam, node, 'setup()')
  199. }
  200. })
  201. )
  202. }
  203. }