no-setup-props-destructure.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const { findVariable } = require('@eslint-community/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', '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. /**
  26. * @param {RuleContext} context
  27. * @returns {RuleListener}
  28. **/
  29. create(context) {
  30. /**
  31. * @typedef {object} ScopePropsReferences
  32. * @property {Set<Identifier>} refs
  33. * @property {string} scopeName
  34. */
  35. /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences>} */
  36. const setupScopePropsReferenceIds = new Map()
  37. const wrapperExpressionTypes = new Set([
  38. 'ArrayExpression',
  39. 'ObjectExpression'
  40. ])
  41. /**
  42. * @param {ESNode} node
  43. * @param {string} messageId
  44. * @param {string} scopeName
  45. */
  46. function report(node, messageId, scopeName) {
  47. context.report({
  48. node,
  49. messageId,
  50. data: {
  51. scopeName
  52. }
  53. })
  54. }
  55. /**
  56. * @param {Pattern} left
  57. * @param {Expression | null} right
  58. * @param {ScopePropsReferences} propsReferences
  59. */
  60. function verify(left, right, propsReferences) {
  61. if (!right) {
  62. return
  63. }
  64. const rightNode = utils.skipChainExpression(right)
  65. if (
  66. wrapperExpressionTypes.has(rightNode.type) &&
  67. isPropsMemberAccessed(rightNode, propsReferences)
  68. ) {
  69. return report(rightNode, 'getProperty', propsReferences.scopeName)
  70. }
  71. if (
  72. left.type !== 'ArrayPattern' &&
  73. left.type !== 'ObjectPattern' &&
  74. rightNode.type !== 'MemberExpression'
  75. ) {
  76. return
  77. }
  78. /** @type {Expression | Super} */
  79. let rightId = rightNode
  80. while (rightId.type === 'MemberExpression') {
  81. rightId = utils.skipChainExpression(rightId.object)
  82. }
  83. if (rightId.type === 'Identifier' && propsReferences.refs.has(rightId)) {
  84. report(left, 'getProperty', propsReferences.scopeName)
  85. }
  86. }
  87. /**
  88. * @param {Expression} node
  89. * @param {ScopePropsReferences} propsReferences
  90. */
  91. function isPropsMemberAccessed(node, propsReferences) {
  92. const propRefs = [...propsReferences.refs.values()]
  93. return propRefs.some((props) => {
  94. const isPropsInExpressionRange = utils.inRange(node.range, props)
  95. const isPropsMemberExpression =
  96. props.parent.type === 'MemberExpression' &&
  97. props.parent.object === props
  98. return isPropsInExpressionRange && isPropsMemberExpression
  99. })
  100. }
  101. /**
  102. * @typedef {object} ScopeStack
  103. * @property {ScopeStack | null} upper
  104. * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
  105. */
  106. /**
  107. * @type {ScopeStack | null}
  108. */
  109. let scopeStack = null
  110. /**
  111. * @param {Pattern | null} node
  112. * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
  113. * @param {string} scopeName
  114. */
  115. function processPattern(node, scopeNode, scopeName) {
  116. if (!node) {
  117. // no arguments
  118. return
  119. }
  120. if (
  121. node.type === 'RestElement' ||
  122. node.type === 'AssignmentPattern' ||
  123. node.type === 'MemberExpression'
  124. ) {
  125. // cannot check
  126. return
  127. }
  128. if (node.type === 'ArrayPattern' || node.type === 'ObjectPattern') {
  129. report(node, 'destructuring', scopeName)
  130. return
  131. }
  132. const variable = findVariable(context.getScope(), node)
  133. if (!variable) {
  134. return
  135. }
  136. const propsReferenceIds = new Set()
  137. for (const reference of variable.references) {
  138. // If reference is in another scope, we can't check it.
  139. if (reference.from !== context.getScope()) {
  140. continue
  141. }
  142. if (!reference.isRead()) {
  143. continue
  144. }
  145. propsReferenceIds.add(reference.identifier)
  146. }
  147. setupScopePropsReferenceIds.set(scopeNode, {
  148. refs: propsReferenceIds,
  149. scopeName
  150. })
  151. }
  152. return utils.compositingVisitors(
  153. {
  154. /**
  155. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
  156. */
  157. 'Program, :function'(node) {
  158. scopeStack = {
  159. upper: scopeStack,
  160. scopeNode: node
  161. }
  162. },
  163. /**
  164. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
  165. */
  166. 'Program, :function:exit'(node) {
  167. scopeStack = scopeStack && scopeStack.upper
  168. setupScopePropsReferenceIds.delete(node)
  169. },
  170. /**
  171. * @param {CallExpression} node
  172. */
  173. CallExpression(node) {
  174. if (!scopeStack) {
  175. return
  176. }
  177. const propsReferenceIds = setupScopePropsReferenceIds.get(
  178. scopeStack.scopeNode
  179. )
  180. if (!propsReferenceIds) {
  181. return
  182. }
  183. if (isPropsMemberAccessed(node, propsReferenceIds)) {
  184. report(node, 'getProperty', propsReferenceIds.scopeName)
  185. }
  186. },
  187. /**
  188. * @param {VariableDeclarator} node
  189. */
  190. VariableDeclarator(node) {
  191. if (!scopeStack) {
  192. return
  193. }
  194. const propsReferenceIds = setupScopePropsReferenceIds.get(
  195. scopeStack.scopeNode
  196. )
  197. if (!propsReferenceIds) {
  198. return
  199. }
  200. verify(node.id, node.init, propsReferenceIds)
  201. },
  202. /**
  203. * @param {AssignmentExpression} node
  204. */
  205. AssignmentExpression(node) {
  206. if (!scopeStack) {
  207. return
  208. }
  209. const propsReferenceIds = setupScopePropsReferenceIds.get(
  210. scopeStack.scopeNode
  211. )
  212. if (!propsReferenceIds) {
  213. return
  214. }
  215. verify(node.left, node.right, propsReferenceIds)
  216. }
  217. },
  218. utils.defineScriptSetupVisitor(context, {
  219. onDefinePropsEnter(node) {
  220. let target = node
  221. if (
  222. target.parent &&
  223. target.parent.type === 'CallExpression' &&
  224. target.parent.arguments[0] === target &&
  225. target.parent.callee.type === 'Identifier' &&
  226. target.parent.callee.name === 'withDefaults'
  227. ) {
  228. target = target.parent
  229. }
  230. if (!target.parent) {
  231. return
  232. }
  233. /** @type {Pattern|null} */
  234. let id = null
  235. if (target.parent.type === 'VariableDeclarator') {
  236. id = target.parent.init === target ? target.parent.id : null
  237. } else if (target.parent.type === 'AssignmentExpression') {
  238. id = target.parent.right === target ? target.parent.left : null
  239. }
  240. processPattern(id, context.getSourceCode().ast, '<script setup>')
  241. }
  242. }),
  243. utils.defineVueVisitor(context, {
  244. onSetupFunctionEnter(node) {
  245. const propsParam = utils.skipDefaultParamValue(node.params[0])
  246. processPattern(propsParam, node, 'setup()')
  247. }
  248. })
  249. )
  250. }
  251. }