Skip to content

Commit 3f20a08

Browse files
authored
Add composition api's computed function support to vue/no-side-effects-in-computed-properties close #1393 (#1407)
* Add composition api's computed function support to vue/no-side-effects-in-computed-properties close #1393 * check targetBody * fix false negative with arr.reverse() * only report variables declared inside setup
1 parent d7eacd6 commit 3f20a08

File tree

3 files changed

+431
-73
lines changed

3 files changed

+431
-73
lines changed

docs/rules/no-side-effects-in-computed-properties.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
pageClass: rule-details
33
sidebarDepth: 0
44
title: vue/no-side-effects-in-computed-properties
5-
description: disallow side effects in computed properties
5+
description: disallow side effects in computed properties and functions
66
since: v3.6.0
77
---
88
# vue/no-side-effects-in-computed-properties
99

10-
> disallow side effects in computed properties
10+
> disallow side effects in computed properties and functions
1111
1212
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
1313

1414
## :book: Rule Details
1515

16-
This rule is aimed at preventing the code which makes side effects in computed properties.
16+
This rule is aimed at preventing the code which makes side effects in computed properties and functions.
1717

18-
It is considered a very bad practice to introduce side effects inside computed properties. It makes the code not predictable and hard to understand.
18+
It is considered a very bad practice to introduce side effects inside computed properties and functions. It makes the code not predictable and hard to understand.
1919

2020
<eslint-code-block :rules="{'vue/no-side-effects-in-computed-properties': ['error']}">
2121

@@ -58,6 +58,51 @@ export default {
5858

5959
</eslint-code-block>
6060

61+
<eslint-code-block :rules="{'vue/no-side-effects-in-computed-properties': ['error']}">
62+
63+
```vue
64+
<script>
65+
import {computed} from 'vue'
66+
/* ✓ GOOD */
67+
export default {
68+
setup() {
69+
const foo = useFoo()
70+
71+
const fullName = computed(() => `${foo.firstName} ${foo.lastName}`)
72+
const reversedArray = computed(() => {
73+
return foo.array.slice(0).reverse() // .slice makes a copy of the array, instead of mutating the orginal
74+
})
75+
}
76+
}
77+
</script>
78+
```
79+
80+
</eslint-code-block>
81+
82+
<eslint-code-block :rules="{'vue/no-side-effects-in-computed-properties': ['error']}">
83+
84+
```vue
85+
<script>
86+
import {computed} from 'vue'
87+
/* ✗ BAD */
88+
export default {
89+
setup() {
90+
const foo = useFoo()
91+
92+
const fullName = computed(() => {
93+
foo.firstName = 'lorem' // <- side effect
94+
return `${foo.firstName} ${foo.lastName}`
95+
})
96+
const reversedArray = computed(() => {
97+
return foo.array.reverse() // <- side effect - orginal array is being mutated
98+
})
99+
}
100+
}
101+
</script>
102+
```
103+
104+
</eslint-code-block>
105+
61106
## :wrench: Options
62107

63108
Nothing.

lib/rules/no-side-effects-in-computed-properties.js

Lines changed: 138 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @author Michał Sajnóg
44
*/
55
'use strict'
6-
6+
const { ReferenceTracker, findVariable } = require('eslint-utils')
77
const utils = require('../utils')
88

99
/**
@@ -31,6 +31,10 @@ module.exports = {
3131
create(context) {
3232
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
3333
const computedPropertiesMap = new Map()
34+
/** @type {Array<FunctionExpression | ArrowFunctionExpression>} */
35+
const computedCallNodes = []
36+
/** @type {Array<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration>} */
37+
const setupFunctions = []
3438

3539
/**
3640
* @typedef {object} ScopeStack
@@ -54,56 +58,143 @@ module.exports = {
5458
scopeStack = scopeStack && scopeStack.upper
5559
}
5660

57-
return utils.defineVueVisitor(context, {
58-
onVueObjectEnter(node) {
59-
computedPropertiesMap.set(node, utils.getComputedProperties(node))
60-
},
61-
':function': onFunctionEnter,
62-
':function:exit': onFunctionExit,
63-
64-
/**
65-
* @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node
66-
* @param {VueObjectData} data
67-
*/
68-
'MemberExpression > :matches(Identifier, ThisExpression)'(
69-
node,
70-
{ node: vueNode }
71-
) {
72-
if (!scopeStack) {
73-
return
74-
}
75-
const targetBody = scopeStack.body
76-
const computedProperty = /** @type {ComponentComputedProperty[]} */ (computedPropertiesMap.get(
77-
vueNode
78-
)).find((cp) => {
79-
return (
80-
cp.value &&
81-
node.loc.start.line >= cp.value.loc.start.line &&
82-
node.loc.end.line <= cp.value.loc.end.line &&
83-
targetBody === cp.value
84-
)
85-
})
86-
if (!computedProperty) {
87-
return
88-
}
61+
return Object.assign(
62+
{
63+
Program() {
64+
const tracker = new ReferenceTracker(context.getScope())
65+
const traceMap = utils.createCompositionApiTraceMap({
66+
[ReferenceTracker.ESM]: true,
67+
computed: {
68+
[ReferenceTracker.CALL]: true
69+
}
70+
})
8971

90-
if (!utils.isThis(node, context)) {
91-
return
92-
}
93-
const mem = node.parent
94-
if (mem.object !== node) {
95-
return
72+
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
73+
if (node.type !== 'CallExpression') {
74+
continue
75+
}
76+
77+
const getterBody = utils.getGetterBodyFromComputedFunction(node)
78+
if (getterBody) {
79+
computedCallNodes.push(getterBody)
80+
}
81+
}
9682
}
83+
},
84+
utils.defineVueVisitor(context, {
85+
onVueObjectEnter(node) {
86+
computedPropertiesMap.set(node, utils.getComputedProperties(node))
87+
},
88+
':function': onFunctionEnter,
89+
':function:exit': onFunctionExit,
90+
onSetupFunctionEnter(node) {
91+
setupFunctions.push(node)
92+
},
9793

98-
const invalid = utils.findMutating(mem)
99-
if (invalid) {
100-
context.report({
101-
node: invalid.node,
102-
message: 'Unexpected side effect in "{{key}}" computed property.',
103-
data: { key: computedProperty.key || 'Unknown' }
94+
/**
95+
* @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node
96+
* @param {VueObjectData} data
97+
*/
98+
'MemberExpression > :matches(Identifier, ThisExpression)'(
99+
node,
100+
{ node: vueNode }
101+
) {
102+
if (!scopeStack) {
103+
return
104+
}
105+
const targetBody = scopeStack.body
106+
107+
const computedProperty = /** @type {ComponentComputedProperty[]} */ (computedPropertiesMap.get(
108+
vueNode
109+
)).find((cp) => {
110+
return (
111+
cp.value &&
112+
node.loc.start.line >= cp.value.loc.start.line &&
113+
node.loc.end.line <= cp.value.loc.end.line &&
114+
targetBody === cp.value
115+
)
104116
})
117+
if (computedProperty) {
118+
if (!utils.isThis(node, context)) {
119+
return
120+
}
121+
const mem = node.parent
122+
if (mem.object !== node) {
123+
return
124+
}
125+
126+
const invalid = utils.findMutating(mem)
127+
if (invalid) {
128+
context.report({
129+
node: invalid.node,
130+
message:
131+
'Unexpected side effect in "{{key}}" computed property.',
132+
data: { key: computedProperty.key || 'Unknown' }
133+
})
134+
}
135+
return
136+
}
137+
138+
// ignore `this` for computed functions
139+
if (node.type === 'ThisExpression') {
140+
return
141+
}
142+
143+
const computedFunction = computedCallNodes.find(
144+
(c) =>
145+
node.loc.start.line >= c.loc.start.line &&
146+
node.loc.end.line <= c.loc.end.line &&
147+
targetBody === c.body
148+
)
149+
if (!computedFunction) {
150+
return
151+
}
152+
153+
const mem = node.parent
154+
if (mem.object !== node) {
155+
return
156+
}
157+
158+
const variable = findVariable(context.getScope(), node)
159+
if (!variable || variable.defs.length !== 1) {
160+
return
161+
}
162+
163+
const def = variable.defs[0]
164+
if (
165+
def.type === 'ImplicitGlobalVariable' ||
166+
def.type === 'TDZ' ||
167+
def.type === 'ImportBinding'
168+
) {
169+
return
170+
}
171+
172+
const isDeclaredInsideSetup = setupFunctions.some(
173+
(setupFn) =>
174+
def.node.loc.start.line >= setupFn.loc.start.line &&
175+
def.node.loc.end.line <= setupFn.loc.end.line
176+
)
177+
if (!isDeclaredInsideSetup) {
178+
return
179+
}
180+
181+
if (
182+
def.node.loc.start.line >= computedFunction.loc.start.line &&
183+
def.node.loc.end.line <= computedFunction.loc.end.line
184+
) {
185+
// mutating local variables are accepted
186+
return
187+
}
188+
189+
const invalid = utils.findMutating(node)
190+
if (invalid) {
191+
context.report({
192+
node: invalid.node,
193+
message: 'Unexpected side effect in computed function.'
194+
})
195+
}
105196
}
106-
}
107-
})
197+
})
198+
)
108199
}
109200
}

0 commit comments

Comments
 (0)