Skip to content

Commit 4442509

Browse files
authored
Update vue/no-side-effects-in-computed-properties rule to support <script setup> (#1534)
1 parent 2d4c49c commit 4442509

File tree

3 files changed

+262
-116
lines changed

3 files changed

+262
-116
lines changed

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

Lines changed: 121 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const utils = require('../utils')
88

99
/**
1010
* @typedef {import('../utils').VueObjectData} VueObjectData
11+
* @typedef {import('../utils').VueVisitor} VueVisitor
1112
* @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty
1213
*/
1314

@@ -32,8 +33,8 @@ module.exports = {
3233
const computedPropertiesMap = new Map()
3334
/** @type {Array<FunctionExpression | ArrowFunctionExpression>} */
3435
const computedCallNodes = []
35-
/** @type {Array<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration>} */
36-
const setupFunctions = []
36+
/** @type {[number, number][]} */
37+
const setupRanges = []
3738

3839
/**
3940
* @typedef {object} ScopeStack
@@ -57,7 +58,114 @@ module.exports = {
5758
scopeStack = scopeStack && scopeStack.upper
5859
}
5960

60-
return Object.assign(
61+
const nodeVisitor = {
62+
':function': onFunctionEnter,
63+
':function:exit': onFunctionExit,
64+
65+
/**
66+
* @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node
67+
* @param {VueObjectData|undefined} [info]
68+
*/
69+
'MemberExpression > :matches(Identifier, ThisExpression)'(node, info) {
70+
if (!scopeStack) {
71+
return
72+
}
73+
const targetBody = scopeStack.body
74+
75+
const computedProperty = (
76+
info ? computedPropertiesMap.get(info.node) || [] : []
77+
).find((cp) => {
78+
return (
79+
cp.value &&
80+
cp.value.range[0] <= node.range[0] &&
81+
node.range[1] <= cp.value.range[1] &&
82+
targetBody === cp.value
83+
)
84+
})
85+
if (computedProperty) {
86+
if (!utils.isThis(node, context)) {
87+
return
88+
}
89+
const mem = node.parent
90+
if (mem.object !== node) {
91+
return
92+
}
93+
94+
const invalid = utils.findMutating(mem)
95+
if (invalid) {
96+
context.report({
97+
node: invalid.node,
98+
message: 'Unexpected side effect in "{{key}}" computed property.',
99+
data: { key: computedProperty.key || 'Unknown' }
100+
})
101+
}
102+
return
103+
}
104+
105+
// ignore `this` for computed functions
106+
if (node.type === 'ThisExpression') {
107+
return
108+
}
109+
110+
const computedFunction = computedCallNodes.find(
111+
(c) =>
112+
c.range[0] <= node.range[0] &&
113+
node.range[1] <= c.range[1] &&
114+
targetBody === c.body
115+
)
116+
if (!computedFunction) {
117+
return
118+
}
119+
120+
const mem = node.parent
121+
if (mem.object !== node) {
122+
return
123+
}
124+
125+
const variable = findVariable(context.getScope(), node)
126+
if (!variable || variable.defs.length !== 1) {
127+
return
128+
}
129+
130+
const def = variable.defs[0]
131+
if (
132+
def.type === 'ImplicitGlobalVariable' ||
133+
def.type === 'TDZ' ||
134+
def.type === 'ImportBinding'
135+
) {
136+
return
137+
}
138+
139+
const isDeclaredInsideSetup = setupRanges.some(
140+
([start, end]) =>
141+
start <= def.node.range[0] && def.node.range[1] <= end
142+
)
143+
if (!isDeclaredInsideSetup) {
144+
return
145+
}
146+
147+
if (
148+
computedFunction.range[0] <= def.node.range[0] &&
149+
def.node.range[1] <= computedFunction.range[1]
150+
) {
151+
// mutating local variables are accepted
152+
return
153+
}
154+
155+
const invalid = utils.findMutating(node)
156+
if (invalid) {
157+
context.report({
158+
node: invalid.node,
159+
message: 'Unexpected side effect in computed function.'
160+
})
161+
}
162+
}
163+
}
164+
const scriptSetupNode = utils.getScriptSetupElement(context)
165+
if (scriptSetupNode) {
166+
setupRanges.push(scriptSetupNode.range)
167+
}
168+
return utils.compositingVisitors(
61169
{
62170
Program() {
63171
const tracker = new ReferenceTracker(context.getScope())
@@ -80,120 +188,17 @@ module.exports = {
80188
}
81189
}
82190
},
83-
utils.defineVueVisitor(context, {
84-
onVueObjectEnter(node) {
85-
computedPropertiesMap.set(node, utils.getComputedProperties(node))
86-
},
87-
':function': onFunctionEnter,
88-
':function:exit': onFunctionExit,
89-
onSetupFunctionEnter(node) {
90-
setupFunctions.push(node)
91-
},
92-
93-
/**
94-
* @param {(Identifier | ThisExpression) & {parent: MemberExpression}} node
95-
* @param {VueObjectData} data
96-
*/
97-
'MemberExpression > :matches(Identifier, ThisExpression)'(
98-
node,
99-
{ node: vueNode }
100-
) {
101-
if (!scopeStack) {
102-
return
103-
}
104-
const targetBody = scopeStack.body
105-
106-
const computedProperty = /** @type {ComponentComputedProperty[]} */ (
107-
computedPropertiesMap.get(vueNode)
108-
).find((cp) => {
109-
return (
110-
cp.value &&
111-
node.loc.start.line >= cp.value.loc.start.line &&
112-
node.loc.end.line <= cp.value.loc.end.line &&
113-
targetBody === cp.value
114-
)
191+
scriptSetupNode
192+
? utils.defineScriptSetupVisitor(context, nodeVisitor)
193+
: utils.defineVueVisitor(context, {
194+
onVueObjectEnter(node) {
195+
computedPropertiesMap.set(node, utils.getComputedProperties(node))
196+
},
197+
onSetupFunctionEnter(node) {
198+
setupRanges.push(node.body.range)
199+
},
200+
...nodeVisitor
115201
})
116-
if (computedProperty) {
117-
if (!utils.isThis(node, context)) {
118-
return
119-
}
120-
const mem = node.parent
121-
if (mem.object !== node) {
122-
return
123-
}
124-
125-
const invalid = utils.findMutating(mem)
126-
if (invalid) {
127-
context.report({
128-
node: invalid.node,
129-
message:
130-
'Unexpected side effect in "{{key}}" computed property.',
131-
data: { key: computedProperty.key || 'Unknown' }
132-
})
133-
}
134-
return
135-
}
136-
137-
// ignore `this` for computed functions
138-
if (node.type === 'ThisExpression') {
139-
return
140-
}
141-
142-
const computedFunction = computedCallNodes.find(
143-
(c) =>
144-
node.loc.start.line >= c.loc.start.line &&
145-
node.loc.end.line <= c.loc.end.line &&
146-
targetBody === c.body
147-
)
148-
if (!computedFunction) {
149-
return
150-
}
151-
152-
const mem = node.parent
153-
if (mem.object !== node) {
154-
return
155-
}
156-
157-
const variable = findVariable(context.getScope(), node)
158-
if (!variable || variable.defs.length !== 1) {
159-
return
160-
}
161-
162-
const def = variable.defs[0]
163-
if (
164-
def.type === 'ImplicitGlobalVariable' ||
165-
def.type === 'TDZ' ||
166-
def.type === 'ImportBinding'
167-
) {
168-
return
169-
}
170-
171-
const isDeclaredInsideSetup = setupFunctions.some(
172-
(setupFn) =>
173-
def.node.loc.start.line >= setupFn.loc.start.line &&
174-
def.node.loc.end.line <= setupFn.loc.end.line
175-
)
176-
if (!isDeclaredInsideSetup) {
177-
return
178-
}
179-
180-
if (
181-
def.node.loc.start.line >= computedFunction.loc.start.line &&
182-
def.node.loc.end.line <= computedFunction.loc.end.line
183-
) {
184-
// mutating local variables are accepted
185-
return
186-
}
187-
188-
const invalid = utils.findMutating(node)
189-
if (invalid) {
190-
context.report({
191-
node: invalid.node,
192-
message: 'Unexpected side effect in computed function.'
193-
})
194-
}
195-
}
196-
})
197202
)
198203
}
199204
}

lib/utils/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,12 @@ module.exports = {
908908
* @param {RuleContext} context The ESLint rule context object.
909909
*/
910910
isScriptSetup,
911+
/**
912+
* Gets the element of `<script setup>`
913+
* @param {RuleContext} context The ESLint rule context object.
914+
* @returns {VElement | null} the element of `<script setup>`
915+
*/
916+
getScriptSetupElement,
911917

912918
/**
913919
* Check if current file is a Vue instance or component and call callback

0 commit comments

Comments
 (0)