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

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