Skip to content

Commit 4b0419d

Browse files
committed
Add composition api's computed function support to vue/no-side-effects-in-computed-properties close vuejs#1393
1 parent 3fb52a9 commit 4b0419d

File tree

2 files changed

+345
-69
lines changed

2 files changed

+345
-69
lines changed

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

Lines changed: 123 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,8 @@ module.exports = {
3131
create(context) {
3232
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
3333
const computedPropertiesMap = new Map()
34+
/** @type {Array<FunctionExpression | ArrowFunctionExpression>} */
35+
const computedCallNodes = []
3436

3537
/**
3638
* @typedef {object} ScopeStack
@@ -54,56 +56,130 @@ module.exports = {
5456
scopeStack = scopeStack && scopeStack.upper
5557
}
5658

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-
}
59+
return Object.assign(
60+
{
61+
Program() {
62+
const tracker = new ReferenceTracker(context.getScope())
63+
const traceMap = utils.createCompositionApiTraceMap({
64+
[ReferenceTracker.ESM]: true,
65+
computed: {
66+
[ReferenceTracker.CALL]: true
67+
}
68+
})
8969

90-
if (!utils.isThis(node, context)) {
91-
return
92-
}
93-
const mem = node.parent
94-
if (mem.object !== node) {
95-
return
70+
for (const { node } of tracker.iterateEsmReferences(traceMap)) {
71+
if (node.type !== 'CallExpression') {
72+
continue
73+
}
74+
75+
const getterBody = utils.getGetterBodyFromComputedFunction(node)
76+
if (getterBody) {
77+
computedCallNodes.push(getterBody)
78+
}
79+
}
9680
}
81+
},
82+
utils.defineVueVisitor(context, {
83+
onVueObjectEnter(node) {
84+
computedPropertiesMap.set(node, utils.getComputedProperties(node))
85+
},
86+
':function': onFunctionEnter,
87+
':function:exit': onFunctionExit,
88+
89+
/**
90+
* @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node
91+
* @param {VueObjectData} data
92+
*/
93+
'MemberExpression > :matches(Identifier, ThisExpression)'(
94+
node,
95+
{ node: vueNode }
96+
) {
97+
if (!scopeStack) {
98+
return
99+
}
100+
const targetBody = scopeStack.body
97101

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' }
102+
const computedProperty = /** @type {ComponentComputedProperty[]} */ (computedPropertiesMap.get(
103+
vueNode
104+
)).find((cp) => {
105+
return (
106+
cp.value &&
107+
node.loc.start.line >= cp.value.loc.start.line &&
108+
node.loc.end.line <= cp.value.loc.end.line &&
109+
targetBody === cp.value
110+
)
104111
})
112+
if (computedProperty) {
113+
if (!utils.isThis(node, context)) {
114+
return
115+
}
116+
const mem = node.parent
117+
if (mem.object !== node) {
118+
return
119+
}
120+
121+
const invalid = utils.findMutating(mem)
122+
if (invalid) {
123+
context.report({
124+
node: invalid.node,
125+
message:
126+
'Unexpected side effect in "{{key}}" computed property.',
127+
data: { key: computedProperty.key || 'Unknown' }
128+
})
129+
}
130+
return
131+
}
132+
133+
// ignore `this` for computed functions
134+
if (node.type === 'ThisExpression') {
135+
return
136+
}
137+
138+
const computedFunction = computedCallNodes.find(
139+
(c) =>
140+
node.loc.start.line >= c.loc.start.line &&
141+
node.loc.end.line <= c.loc.end.line
142+
)
143+
if (!computedFunction) {
144+
return
145+
}
146+
147+
const mem = node.parent
148+
if (mem.object !== node) {
149+
return
150+
}
151+
152+
const variable = findVariable(context.getScope(), node)
153+
if (variable) {
154+
if (variable.defs.length !== 1) {
155+
return
156+
}
157+
158+
const def = variable.defs[0]
159+
if (
160+
def.type !== 'ImplicitGlobalVariable' &&
161+
def.type !== 'TDZ' &&
162+
def.type !== 'ImportBinding'
163+
) {
164+
if (
165+
def.node.loc.start.line >= computedFunction.loc.start.line &&
166+
def.node.loc.end.line <= computedFunction.loc.end.line
167+
) {
168+
// mutating local variables are accepted
169+
return
170+
}
171+
}
172+
}
173+
174+
const invalid = utils.findMutating(mem)
175+
if (invalid) {
176+
context.report({
177+
node: invalid.node,
178+
message: 'Unexpected side effect in computed function.'
179+
})
180+
}
105181
}
106-
}
107-
})
182+
})
183+
)
108184
}
109185
}

0 commit comments

Comments
 (0)