no-ref-object-reactivity-loss.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /**
  2. * @author Yosuke Ota <https://github.com/ota-meshi>
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const {
  8. extractRefObjectReferences,
  9. extractReactiveVariableReferences
  10. } = require('../utils/ref-object-references')
  11. /**
  12. * @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
  13. * @typedef {import('../utils/ref-object-references').RefObjectReference} RefObjectReference
  14. */
  15. /**
  16. * Checks whether writing assigns a value to the given pattern.
  17. * @param {Pattern | AssignmentProperty | Property} node
  18. * @returns {boolean}
  19. */
  20. function isUpdate(node) {
  21. const parent = node.parent
  22. if (parent.type === 'UpdateExpression' && parent.argument === node) {
  23. // e.g. `pattern++`
  24. return true
  25. }
  26. if (parent.type === 'AssignmentExpression' && parent.left === node) {
  27. // e.g. `pattern = 42`
  28. return true
  29. }
  30. if (
  31. (parent.type === 'Property' && parent.value === node) ||
  32. parent.type === 'ArrayPattern' ||
  33. (parent.type === 'ObjectPattern' &&
  34. parent.properties.includes(/** @type {any} */ (node))) ||
  35. (parent.type === 'AssignmentPattern' && parent.left === node) ||
  36. parent.type === 'RestElement' ||
  37. (parent.type === 'MemberExpression' && parent.object === node)
  38. ) {
  39. return isUpdate(parent)
  40. }
  41. return false
  42. }
  43. module.exports = {
  44. meta: {
  45. type: 'problem',
  46. docs: {
  47. description:
  48. 'disallow usages of ref objects that can lead to loss of reactivity',
  49. categories: undefined,
  50. url: 'https://eslint.vuejs.org/rules/no-ref-object-reactivity-loss.html'
  51. },
  52. fixable: null,
  53. schema: [],
  54. messages: {
  55. getValueInSameScope:
  56. 'Getting a value from the ref object in the same scope will cause the value to lose reactivity.',
  57. getReactiveVariableInSameScope:
  58. 'Getting a reactive variable in the same scope will cause the value to lose reactivity.'
  59. }
  60. },
  61. /**
  62. * @param {RuleContext} context
  63. * @returns {RuleListener}
  64. */
  65. create(context) {
  66. /**
  67. * @typedef {object} ScopeStack
  68. * @property {ScopeStack | null} upper
  69. * @property {Program | FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  70. */
  71. /** @type {ScopeStack} */
  72. let scopeStack = { upper: null, node: context.getSourceCode().ast }
  73. /** @type {Map<CallExpression, ScopeStack>} */
  74. const scopes = new Map()
  75. const refObjectReferences = extractRefObjectReferences(context)
  76. const reactiveVariableReferences =
  77. extractReactiveVariableReferences(context)
  78. /**
  79. * Verify the given ref object value. `refObj = ref(); refObj.value;`
  80. * @param {Expression | Super | ObjectPattern} node
  81. */
  82. function verifyRefObjectValue(node) {
  83. const ref = refObjectReferences.get(node)
  84. if (!ref) {
  85. return
  86. }
  87. if (scopes.get(ref.define) !== scopeStack) {
  88. // Not in the same scope
  89. return
  90. }
  91. context.report({
  92. node,
  93. messageId: 'getValueInSameScope'
  94. })
  95. }
  96. /**
  97. * Verify the given reactive variable. `refVal = $ref(); refVal;`
  98. * @param {Identifier} node
  99. */
  100. function verifyReactiveVariable(node) {
  101. const ref = reactiveVariableReferences.get(node)
  102. if (!ref || ref.escape) {
  103. return
  104. }
  105. if (scopes.get(ref.define) !== scopeStack) {
  106. // Not in the same scope
  107. return
  108. }
  109. context.report({
  110. node,
  111. messageId: 'getReactiveVariableInSameScope'
  112. })
  113. }
  114. return {
  115. ':function'(node) {
  116. scopeStack = { upper: scopeStack, node }
  117. },
  118. ':function:exit'() {
  119. scopeStack = scopeStack.upper || scopeStack
  120. },
  121. CallExpression(node) {
  122. scopes.set(node, scopeStack)
  123. },
  124. /**
  125. * Check for `refObj.value`.
  126. */
  127. 'MemberExpression:exit'(node) {
  128. if (isUpdate(node)) {
  129. // e.g. `refObj.value = 42`, `refObj.value++`
  130. return
  131. }
  132. const name = utils.getStaticPropertyName(node)
  133. if (name !== 'value') {
  134. return
  135. }
  136. verifyRefObjectValue(node.object)
  137. },
  138. /**
  139. * Check for `{value} = refObj`.
  140. */
  141. 'ObjectPattern:exit'(node) {
  142. const prop = utils.findAssignmentProperty(node, 'value')
  143. if (!prop) {
  144. return
  145. }
  146. verifyRefObjectValue(node)
  147. },
  148. /**
  149. * Check for reactive variable`.
  150. * @param {Identifier} node
  151. */
  152. 'Identifier:exit'(node) {
  153. if (isUpdate(node)) {
  154. // e.g. `reactiveVariable = 42`, `reactiveVariable++`
  155. return
  156. }
  157. verifyReactiveVariable(node)
  158. }
  159. }
  160. }
  161. }