no-setup-props-reactivity-loss.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. /**
  9. * @typedef {'props'|'prop'} PropIdKind
  10. * - `'props'`: A node is a container object that has props.
  11. * - `'prop'`: A node is a variable with one prop.
  12. */
  13. /**
  14. * @typedef {object} PropId
  15. * @property {Pattern} node
  16. * @property {PropIdKind} kind
  17. */
  18. /**
  19. * Iterates over Prop identifiers by parsing the given pattern
  20. * in the left operand of defineProps().
  21. * @param {Pattern} node
  22. * @returns {IterableIterator<PropId>}
  23. */
  24. function* iteratePropIds(node) {
  25. switch (node.type) {
  26. case 'ObjectPattern': {
  27. for (const prop of node.properties) {
  28. yield prop.type === 'Property'
  29. ? {
  30. // e.g. `const { prop } = defineProps()`
  31. node: unwrapAssignment(prop.value),
  32. kind: 'prop'
  33. }
  34. : {
  35. // RestElement
  36. // e.g. `const { x, ...prop } = defineProps()`
  37. node: unwrapAssignment(prop.argument),
  38. kind: 'props'
  39. }
  40. }
  41. break
  42. }
  43. default: {
  44. // e.g. `const props = defineProps()`
  45. yield { node: unwrapAssignment(node), kind: 'props' }
  46. }
  47. }
  48. }
  49. /**
  50. * @template {Pattern} T
  51. * @param {T} node
  52. * @returns {Pattern}
  53. */
  54. function unwrapAssignment(node) {
  55. if (node.type === 'AssignmentPattern') {
  56. return node.left
  57. }
  58. return node
  59. }
  60. module.exports = {
  61. meta: {
  62. type: 'suggestion',
  63. docs: {
  64. description:
  65. 'disallow usages that lose the reactivity of `props` passed to `setup`',
  66. categories: undefined,
  67. url: 'https://eslint.vuejs.org/rules/no-setup-props-reactivity-loss.html'
  68. },
  69. fixable: null,
  70. schema: [],
  71. messages: {
  72. destructuring:
  73. 'Destructuring the `props` will cause the value to lose reactivity.',
  74. getProperty:
  75. 'Getting a value from the `props` in root scope of `{{scopeName}}` will cause the value to lose reactivity.'
  76. }
  77. },
  78. /**
  79. * @param {RuleContext} context
  80. * @returns {RuleListener}
  81. **/
  82. create(context) {
  83. /**
  84. * @typedef {object} ScopePropsReferences
  85. * @property {object} refs
  86. * @property {Set<Identifier>} refs.props A set of references to container objects with multiple props.
  87. * @property {Set<Identifier>} refs.prop A set of references a variable with one property.
  88. * @property {string} scopeName
  89. */
  90. /** @type {Map<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program, ScopePropsReferences>} */
  91. const setupScopePropsReferenceIds = new Map()
  92. const wrapperExpressionTypes = new Set([
  93. 'ArrayExpression',
  94. 'ObjectExpression'
  95. ])
  96. /**
  97. * @param {ESNode} node
  98. * @param {string} messageId
  99. * @param {string} scopeName
  100. */
  101. function report(node, messageId, scopeName) {
  102. context.report({
  103. node,
  104. messageId,
  105. data: {
  106. scopeName
  107. }
  108. })
  109. }
  110. /**
  111. * @param {Pattern} left
  112. * @param {Expression | null} right
  113. * @param {ScopePropsReferences} propsReferences
  114. */
  115. function verify(left, right, propsReferences) {
  116. if (!right) {
  117. return
  118. }
  119. const rightNode = utils.skipChainExpression(right)
  120. if (
  121. wrapperExpressionTypes.has(rightNode.type) &&
  122. isPropsMemberAccessed(rightNode, propsReferences)
  123. ) {
  124. // e.g. `const foo = { x: props.x }`
  125. report(rightNode, 'getProperty', propsReferences.scopeName)
  126. return
  127. }
  128. // Get the expression that provides the value.
  129. /** @type {Expression | Super} */
  130. let expression = rightNode
  131. while (expression.type === 'MemberExpression') {
  132. expression = utils.skipChainExpression(expression.object)
  133. }
  134. /** A list of expression nodes to verify */
  135. const expressions =
  136. expression.type === 'TemplateLiteral'
  137. ? expression.expressions
  138. : expression.type === 'ConditionalExpression'
  139. ? [expression.test, expression.consequent, expression.alternate]
  140. : expression.type === 'Identifier'
  141. ? [expression]
  142. : []
  143. if (
  144. (left.type === 'ArrayPattern' || left.type === 'ObjectPattern') &&
  145. expressions.some(
  146. (expr) =>
  147. expr.type === 'Identifier' && propsReferences.refs.props.has(expr)
  148. )
  149. ) {
  150. // e.g. `const {foo} = props`
  151. report(left, 'getProperty', propsReferences.scopeName)
  152. return
  153. }
  154. const reportNode = expressions.find((expr) =>
  155. isPropsMemberAccessed(expr, propsReferences)
  156. )
  157. if (reportNode) {
  158. report(reportNode, 'getProperty', propsReferences.scopeName)
  159. }
  160. }
  161. /**
  162. * @param {Expression | Super} node
  163. * @param {ScopePropsReferences} propsReferences
  164. */
  165. function isPropsMemberAccessed(node, propsReferences) {
  166. for (const props of propsReferences.refs.props) {
  167. const isPropsInExpressionRange = utils.inRange(node.range, props)
  168. const isPropsMemberExpression =
  169. props.parent.type === 'MemberExpression' &&
  170. props.parent.object === props
  171. if (isPropsInExpressionRange && isPropsMemberExpression) {
  172. return true
  173. }
  174. }
  175. // Checks for actual member access using prop destructuring.
  176. for (const prop of propsReferences.refs.prop) {
  177. const isPropsInExpressionRange = utils.inRange(node.range, prop)
  178. if (isPropsInExpressionRange) {
  179. return true
  180. }
  181. }
  182. return false
  183. }
  184. /**
  185. * @typedef {object} ScopeStack
  186. * @property {ScopeStack | null} upper
  187. * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
  188. */
  189. /**
  190. * @type {ScopeStack | null}
  191. */
  192. let scopeStack = null
  193. /**
  194. * @param {PropId} propId
  195. * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | Program} scopeNode
  196. * @param {import('eslint').Scope.Scope} currentScope
  197. * @param {string} scopeName
  198. */
  199. function processPropId({ node, kind }, scopeNode, currentScope, scopeName) {
  200. if (
  201. node.type === 'RestElement' ||
  202. node.type === 'AssignmentPattern' ||
  203. node.type === 'MemberExpression'
  204. ) {
  205. // cannot check
  206. return
  207. }
  208. if (node.type === 'ArrayPattern' || node.type === 'ObjectPattern') {
  209. report(node, 'destructuring', scopeName)
  210. return
  211. }
  212. const variable = findVariable(currentScope, node)
  213. if (!variable) {
  214. return
  215. }
  216. let scopePropsReferences = setupScopePropsReferenceIds.get(scopeNode)
  217. if (!scopePropsReferences) {
  218. scopePropsReferences = {
  219. refs: {
  220. props: new Set(),
  221. prop: new Set()
  222. },
  223. scopeName
  224. }
  225. setupScopePropsReferenceIds.set(scopeNode, scopePropsReferences)
  226. }
  227. const propsReferenceIds = scopePropsReferences.refs[kind]
  228. for (const reference of variable.references) {
  229. // If reference is in another scope, we can't check it.
  230. if (reference.from !== currentScope) {
  231. continue
  232. }
  233. if (!reference.isRead()) {
  234. continue
  235. }
  236. propsReferenceIds.add(reference.identifier)
  237. }
  238. }
  239. return utils.compositingVisitors(
  240. {
  241. /**
  242. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
  243. */
  244. 'Program, :function'(node) {
  245. scopeStack = {
  246. upper: scopeStack,
  247. scopeNode: node
  248. }
  249. },
  250. /**
  251. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | Program} node
  252. */
  253. 'Program, :function:exit'(node) {
  254. scopeStack = scopeStack && scopeStack.upper
  255. setupScopePropsReferenceIds.delete(node)
  256. },
  257. /**
  258. * @param {CallExpression} node
  259. */
  260. CallExpression(node) {
  261. if (!scopeStack) {
  262. return
  263. }
  264. const propsReferenceIds = setupScopePropsReferenceIds.get(
  265. scopeStack.scopeNode
  266. )
  267. if (!propsReferenceIds) {
  268. return
  269. }
  270. if (isPropsMemberAccessed(node, propsReferenceIds)) {
  271. report(node, 'getProperty', propsReferenceIds.scopeName)
  272. }
  273. },
  274. /**
  275. * @param {VariableDeclarator} node
  276. */
  277. VariableDeclarator(node) {
  278. if (!scopeStack) {
  279. return
  280. }
  281. const propsReferenceIds = setupScopePropsReferenceIds.get(
  282. scopeStack.scopeNode
  283. )
  284. if (!propsReferenceIds) {
  285. return
  286. }
  287. verify(node.id, node.init, propsReferenceIds)
  288. },
  289. /**
  290. * @param {AssignmentExpression} node
  291. */
  292. AssignmentExpression(node) {
  293. if (!scopeStack) {
  294. return
  295. }
  296. const propsReferenceIds = setupScopePropsReferenceIds.get(
  297. scopeStack.scopeNode
  298. )
  299. if (!propsReferenceIds) {
  300. return
  301. }
  302. verify(node.left, node.right, propsReferenceIds)
  303. }
  304. },
  305. utils.defineScriptSetupVisitor(context, {
  306. onDefinePropsEnter(node) {
  307. let target = node
  308. if (
  309. target.parent &&
  310. target.parent.type === 'CallExpression' &&
  311. target.parent.arguments[0] === target &&
  312. target.parent.callee.type === 'Identifier' &&
  313. target.parent.callee.name === 'withDefaults'
  314. ) {
  315. target = target.parent
  316. }
  317. if (!target.parent) {
  318. return
  319. }
  320. /** @type {Pattern|null} */
  321. let id = null
  322. if (target.parent.type === 'VariableDeclarator') {
  323. id = target.parent.init === target ? target.parent.id : null
  324. } else if (target.parent.type === 'AssignmentExpression') {
  325. id = target.parent.right === target ? target.parent.left : null
  326. }
  327. if (!id) return
  328. const currentScope = utils.getScope(context, node)
  329. for (const propId of iteratePropIds(id)) {
  330. processPropId(
  331. propId,
  332. context.getSourceCode().ast,
  333. currentScope,
  334. '<script setup>'
  335. )
  336. }
  337. }
  338. }),
  339. utils.defineVueVisitor(context, {
  340. onSetupFunctionEnter(node) {
  341. const currentScope = utils.getScope(context, node)
  342. const propsParam = utils.skipDefaultParamValue(node.params[0])
  343. if (!propsParam) return
  344. processPropId(
  345. { node: propsParam, kind: 'props' },
  346. node,
  347. currentScope,
  348. 'setup()'
  349. )
  350. }
  351. })
  352. )
  353. }
  354. }