no-side-effects-in-computed-properties.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /**
  2. * @fileoverview Don't introduce side effects in computed properties
  3. * @author Michał Sajnóg
  4. */
  5. 'use strict'
  6. const { ReferenceTracker, findVariable } = require('eslint-utils')
  7. const utils = require('../utils')
  8. /**
  9. * @typedef {import('../utils').VueObjectData} VueObjectData
  10. * @typedef {import('../utils').VueVisitor} VueVisitor
  11. * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty
  12. */
  13. // ------------------------------------------------------------------------------
  14. // Rule Definition
  15. // ------------------------------------------------------------------------------
  16. module.exports = {
  17. meta: {
  18. type: 'problem',
  19. docs: {
  20. description: 'disallow side effects in computed properties',
  21. categories: ['vue3-essential', 'essential'],
  22. url: 'https://eslint.vuejs.org/rules/no-side-effects-in-computed-properties.html'
  23. },
  24. fixable: null,
  25. schema: []
  26. },
  27. /** @param {RuleContext} context */
  28. create(context) {
  29. /** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
  30. const computedPropertiesMap = new Map()
  31. /** @type {Array<FunctionExpression | ArrowFunctionExpression>} */
  32. const computedCallNodes = []
  33. /** @type {[number, number][]} */
  34. const setupRanges = []
  35. /**
  36. * @typedef {object} ScopeStack
  37. * @property {ScopeStack | null} upper
  38. * @property {BlockStatement | Expression | null} body
  39. */
  40. /**
  41. * @type {ScopeStack | null}
  42. */
  43. let scopeStack = null
  44. /** @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node */
  45. function onFunctionEnter(node) {
  46. scopeStack = {
  47. upper: scopeStack,
  48. body: node.body
  49. }
  50. }
  51. function onFunctionExit() {
  52. scopeStack = scopeStack && scopeStack.upper
  53. }
  54. const nodeVisitor = {
  55. ':function': onFunctionEnter,
  56. ':function:exit': onFunctionExit,
  57. /**
  58. * @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node
  59. * @param {VueObjectData|undefined} [info]
  60. */
  61. 'MemberExpression > :matches(Identifier, ThisExpression)'(node, info) {
  62. if (!scopeStack) {
  63. return
  64. }
  65. const targetBody = scopeStack.body
  66. const computedProperty = (
  67. info ? computedPropertiesMap.get(info.node) || [] : []
  68. ).find((cp) => {
  69. return (
  70. cp.value &&
  71. cp.value.range[0] <= node.range[0] &&
  72. node.range[1] <= cp.value.range[1] &&
  73. targetBody === cp.value
  74. )
  75. })
  76. if (computedProperty) {
  77. if (!utils.isThis(node, context)) {
  78. return
  79. }
  80. const mem = node.parent
  81. if (mem.object !== node) {
  82. return
  83. }
  84. const invalid = utils.findMutating(mem)
  85. if (invalid) {
  86. context.report({
  87. node: invalid.node,
  88. message: 'Unexpected side effect in "{{key}}" computed property.',
  89. data: { key: computedProperty.key || 'Unknown' }
  90. })
  91. }
  92. return
  93. }
  94. // ignore `this` for computed functions
  95. if (node.type === 'ThisExpression') {
  96. return
  97. }
  98. const computedFunction = computedCallNodes.find(
  99. (c) =>
  100. c.range[0] <= node.range[0] &&
  101. node.range[1] <= c.range[1] &&
  102. targetBody === c.body
  103. )
  104. if (!computedFunction) {
  105. return
  106. }
  107. const mem = node.parent
  108. if (mem.object !== node) {
  109. return
  110. }
  111. const variable = findVariable(context.getScope(), node)
  112. if (!variable || variable.defs.length !== 1) {
  113. return
  114. }
  115. const def = variable.defs[0]
  116. if (
  117. def.type === 'ImplicitGlobalVariable' ||
  118. def.type === 'TDZ' ||
  119. def.type === 'ImportBinding'
  120. ) {
  121. return
  122. }
  123. const isDeclaredInsideSetup = setupRanges.some(
  124. ([start, end]) =>
  125. start <= def.node.range[0] && def.node.range[1] <= end
  126. )
  127. if (!isDeclaredInsideSetup) {
  128. return
  129. }
  130. if (
  131. computedFunction.range[0] <= def.node.range[0] &&
  132. def.node.range[1] <= computedFunction.range[1]
  133. ) {
  134. // mutating local variables are accepted
  135. return
  136. }
  137. const invalid = utils.findMutating(node)
  138. if (invalid) {
  139. context.report({
  140. node: invalid.node,
  141. message: 'Unexpected side effect in computed function.'
  142. })
  143. }
  144. }
  145. }
  146. const scriptSetupNode = utils.getScriptSetupElement(context)
  147. if (scriptSetupNode) {
  148. setupRanges.push(scriptSetupNode.range)
  149. }
  150. return utils.compositingVisitors(
  151. {
  152. Program() {
  153. const tracker = new ReferenceTracker(context.getScope())
  154. const traceMap = utils.createCompositionApiTraceMap({
  155. [ReferenceTracker.ESM]: true,
  156. computed: {
  157. [ReferenceTracker.CALL]: true
  158. }
  159. })
  160. for (const { node } of tracker.iterateEsmReferences(traceMap)) {
  161. if (node.type !== 'CallExpression') {
  162. continue
  163. }
  164. const getterBody = utils.getGetterBodyFromComputedFunction(node)
  165. if (getterBody) {
  166. computedCallNodes.push(getterBody)
  167. }
  168. }
  169. }
  170. },
  171. scriptSetupNode
  172. ? utils.defineScriptSetupVisitor(context, nodeVisitor)
  173. : utils.defineVueVisitor(context, {
  174. onVueObjectEnter(node) {
  175. computedPropertiesMap.set(node, utils.getComputedProperties(node))
  176. },
  177. onSetupFunctionEnter(node) {
  178. setupRanges.push(node.body.range)
  179. },
  180. ...nodeVisitor
  181. })
  182. )
  183. }
  184. }