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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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. const mem = node.parent
  78. if (mem.object !== node) {
  79. return
  80. }
  81. const isThis = utils.isThis(node, context)
  82. const isVue = node.type === 'Identifier' && node.name === 'Vue'
  83. const isVueSet =
  84. mem.parent.type === 'CallExpression' &&
  85. mem.property.type === 'Identifier' &&
  86. ((isThis && mem.property.name === '$set') ||
  87. (isVue && mem.property.name === 'set'))
  88. const invalid = isVueSet
  89. ? { node: mem.property }
  90. : isThis && utils.findMutating(mem)
  91. if (invalid) {
  92. context.report({
  93. node: invalid.node,
  94. message: 'Unexpected side effect in "{{key}}" computed property.',
  95. data: { key: computedProperty.key || 'Unknown' }
  96. })
  97. }
  98. return
  99. }
  100. // ignore `this` for computed functions
  101. if (node.type === 'ThisExpression') {
  102. return
  103. }
  104. const computedFunction = computedCallNodes.find(
  105. (c) =>
  106. c.range[0] <= node.range[0] &&
  107. node.range[1] <= c.range[1] &&
  108. targetBody === c.body
  109. )
  110. if (!computedFunction) {
  111. return
  112. }
  113. const mem = node.parent
  114. if (mem.object !== node) {
  115. return
  116. }
  117. const variable = findVariable(context.getScope(), node)
  118. if (!variable || variable.defs.length !== 1) {
  119. return
  120. }
  121. const def = variable.defs[0]
  122. if (
  123. def.type === 'ImplicitGlobalVariable' ||
  124. def.type === 'TDZ' ||
  125. def.type === 'ImportBinding'
  126. ) {
  127. return
  128. }
  129. const isDeclaredInsideSetup = setupRanges.some(
  130. ([start, end]) =>
  131. start <= def.node.range[0] && def.node.range[1] <= end
  132. )
  133. if (!isDeclaredInsideSetup) {
  134. return
  135. }
  136. if (
  137. computedFunction.range[0] <= def.node.range[0] &&
  138. def.node.range[1] <= computedFunction.range[1]
  139. ) {
  140. // mutating local variables are accepted
  141. return
  142. }
  143. const invalid = utils.findMutating(node)
  144. if (invalid) {
  145. context.report({
  146. node: invalid.node,
  147. message: 'Unexpected side effect in computed function.'
  148. })
  149. }
  150. }
  151. }
  152. const scriptSetupNode = utils.getScriptSetupElement(context)
  153. if (scriptSetupNode) {
  154. setupRanges.push(scriptSetupNode.range)
  155. }
  156. return utils.compositingVisitors(
  157. {
  158. Program() {
  159. const tracker = new ReferenceTracker(context.getScope())
  160. const traceMap = utils.createCompositionApiTraceMap({
  161. [ReferenceTracker.ESM]: true,
  162. computed: {
  163. [ReferenceTracker.CALL]: true
  164. }
  165. })
  166. for (const { node } of tracker.iterateEsmReferences(traceMap)) {
  167. if (node.type !== 'CallExpression') {
  168. continue
  169. }
  170. const getterBody = utils.getGetterBodyFromComputedFunction(node)
  171. if (getterBody) {
  172. computedCallNodes.push(getterBody)
  173. }
  174. }
  175. }
  176. },
  177. scriptSetupNode
  178. ? utils.defineScriptSetupVisitor(context, nodeVisitor)
  179. : utils.defineVueVisitor(context, {
  180. onVueObjectEnter(node) {
  181. computedPropertiesMap.set(node, utils.getComputedProperties(node))
  182. },
  183. onSetupFunctionEnter(node) {
  184. setupRanges.push(node.body.range)
  185. },
  186. ...nodeVisitor
  187. })
  188. )
  189. }
  190. }