Skip to content

Commit af3700f

Browse files
authored
feat(no-ref-as-operand): use ref.value to replace ref when emit (#2680)
1 parent 827ab4b commit af3700f

File tree

3 files changed

+521
-75
lines changed

3 files changed

+521
-75
lines changed

docs/rules/no-ref-as-operand.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ You must use `.value` to access the `Ref` value.
2525
import { ref } from 'vue'
2626
2727
export default {
28-
setup() {
28+
setup(_props, { emit }) {
2929
const count = ref(0)
3030
const ok = ref(true)
3131
@@ -34,12 +34,14 @@ export default {
3434
count.value + 1
3535
1 + count.value
3636
var msg = ok.value ? 'yes' : 'no'
37+
emit('increment', count.value)
3738
3839
/* ✗ BAD */
3940
count++
4041
count + 1
4142
1 + count
4243
var msg = ok ? 'yes' : 'no'
44+
emit('increment', count)
4345
4446
return {
4547
count

lib/rules/no-ref-as-operand.js

+248-74
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
'use strict'
66

7+
const { findVariable } = require('@eslint-community/eslint-utils')
78
const { extractRefObjectReferences } = require('../utils/ref-object-references')
89
const utils = require('../utils')
910

@@ -24,6 +25,40 @@ function isRefInit(data) {
2425
}
2526
return data.defineChain.includes(/** @type {any} */ (init))
2627
}
28+
29+
/**
30+
* Get the callee member node from the given CallExpression
31+
* @param {CallExpression} node CallExpression
32+
*/
33+
function getNameParamNode(node) {
34+
const nameLiteralNode = node.arguments[0]
35+
if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) {
36+
const name = utils.getStringLiteralValue(nameLiteralNode)
37+
if (name != null) {
38+
return { name, loc: nameLiteralNode.loc }
39+
}
40+
}
41+
42+
// cannot check
43+
return null
44+
}
45+
46+
/**
47+
* Get the callee member node from the given CallExpression
48+
* @param {CallExpression} node CallExpression
49+
*/
50+
function getCalleeMemberNode(node) {
51+
const callee = utils.skipChainExpression(node.callee)
52+
53+
if (callee.type === 'MemberExpression') {
54+
const name = utils.getStaticPropertyName(callee)
55+
if (name) {
56+
return { name, member: callee }
57+
}
58+
}
59+
return null
60+
}
61+
2762
module.exports = {
2863
meta: {
2964
type: 'suggestion',
@@ -44,6 +79,22 @@ module.exports = {
4479
create(context) {
4580
/** @type {RefObjectReferences} */
4681
let refReferences
82+
const setupContexts = new Map()
83+
84+
/**
85+
* Collect identifier id
86+
* @param {Identifier} node
87+
* @param {Set<Identifier>} referenceIds
88+
*/
89+
function collectReferenceIds(node, referenceIds) {
90+
const variable = findVariable(utils.getScope(context, node), node)
91+
if (!variable) {
92+
return
93+
}
94+
for (const reference of variable.references) {
95+
referenceIds.add(reference.identifier)
96+
}
97+
}
4798

4899
/**
49100
* @param {Identifier} node
@@ -64,90 +115,213 @@ module.exports = {
64115
}
65116
})
66117
}
67-
return {
68-
Program() {
69-
refReferences = extractRefObjectReferences(context)
70-
},
71-
// if (refValue)
72-
/** @param {Identifier} node */
73-
'IfStatement>Identifier'(node) {
74-
reportIfRefWrapped(node)
75-
},
76-
// switch (refValue)
77-
/** @param {Identifier} node */
78-
'SwitchStatement>Identifier'(node) {
79-
reportIfRefWrapped(node)
80-
},
81-
// -refValue, +refValue, !refValue, ~refValue, typeof refValue
82-
/** @param {Identifier} node */
83-
'UnaryExpression>Identifier'(node) {
84-
reportIfRefWrapped(node)
85-
},
86-
// refValue++, refValue--
87-
/** @param {Identifier} node */
88-
'UpdateExpression>Identifier'(node) {
89-
reportIfRefWrapped(node)
90-
},
91-
// refValue+1, refValue-1
92-
/** @param {Identifier} node */
93-
'BinaryExpression>Identifier'(node) {
118+
119+
/**
120+
* @param {CallExpression} node
121+
*/
122+
function reportWrappedIdentifiers(node) {
123+
const nodes = node.arguments.filter((node) => node.type === 'Identifier')
124+
for (const node of nodes) {
94125
reportIfRefWrapped(node)
95-
},
96-
// refValue+=1, refValue-=1, foo+=refValue, foo-=refValue
97-
/** @param {Identifier & {parent: AssignmentExpression}} node */
98-
'AssignmentExpression>Identifier'(node) {
99-
if (node.parent.operator === '=' && node.parent.left !== node) {
126+
}
127+
}
128+
129+
const programNode = context.getSourceCode().ast
130+
131+
const callVisitor = {
132+
/**
133+
* @param {CallExpression} node
134+
* @param {import('../utils').VueObjectData} [info]
135+
*/
136+
CallExpression(node, info) {
137+
const nameWithLoc = getNameParamNode(node)
138+
if (!nameWithLoc) {
139+
// cannot check
100140
return
101141
}
102-
reportIfRefWrapped(node)
103-
},
104-
// refValue || other, refValue && other. ignore: other || refValue
105-
/** @param {Identifier & {parent: LogicalExpression}} node */
106-
'LogicalExpression>Identifier'(node) {
107-
if (node.parent.left !== node) {
142+
143+
// verify setup context
144+
const setupContext = setupContexts.get(info ? info.node : programNode)
145+
if (!setupContext) {
108146
return
109147
}
110-
// Report only constants.
111-
const data = refReferences.get(node)
148+
149+
const { contextReferenceIds, emitReferenceIds } = setupContext
112150
if (
113-
!data ||
114-
!data.variableDeclaration ||
115-
data.variableDeclaration.kind !== 'const'
151+
node.callee.type === 'Identifier' &&
152+
emitReferenceIds.has(node.callee)
116153
) {
117-
return
154+
// verify setup(props,{emit}) {emit()}
155+
reportWrappedIdentifiers(node)
156+
} else {
157+
const emit = getCalleeMemberNode(node)
158+
if (
159+
emit &&
160+
emit.name === 'emit' &&
161+
emit.member.object.type === 'Identifier' &&
162+
contextReferenceIds.has(emit.member.object)
163+
) {
164+
// verify setup(props,context) {context.emit()}
165+
reportWrappedIdentifiers(node)
166+
}
118167
}
119-
reportIfRefWrapped(node)
120-
},
121-
// refValue ? x : y
122-
/** @param {Identifier & {parent: ConditionalExpression}} node */
123-
'ConditionalExpression>Identifier'(node) {
124-
if (node.parent.test !== node) {
125-
return
168+
}
169+
}
170+
171+
return utils.compositingVisitors(
172+
{
173+
Program() {
174+
refReferences = extractRefObjectReferences(context)
175+
},
176+
// if (refValue)
177+
/** @param {Identifier} node */
178+
'IfStatement>Identifier'(node) {
179+
reportIfRefWrapped(node)
180+
},
181+
// switch (refValue)
182+
/** @param {Identifier} node */
183+
'SwitchStatement>Identifier'(node) {
184+
reportIfRefWrapped(node)
185+
},
186+
// -refValue, +refValue, !refValue, ~refValue, typeof refValue
187+
/** @param {Identifier} node */
188+
'UnaryExpression>Identifier'(node) {
189+
reportIfRefWrapped(node)
190+
},
191+
// refValue++, refValue--
192+
/** @param {Identifier} node */
193+
'UpdateExpression>Identifier'(node) {
194+
reportIfRefWrapped(node)
195+
},
196+
// refValue+1, refValue-1
197+
/** @param {Identifier} node */
198+
'BinaryExpression>Identifier'(node) {
199+
reportIfRefWrapped(node)
200+
},
201+
// refValue+=1, refValue-=1, foo+=refValue, foo-=refValue
202+
/** @param {Identifier & {parent: AssignmentExpression}} node */
203+
'AssignmentExpression>Identifier'(node) {
204+
if (node.parent.operator === '=' && node.parent.left !== node) {
205+
return
206+
}
207+
reportIfRefWrapped(node)
208+
},
209+
// refValue || other, refValue && other. ignore: other || refValue
210+
/** @param {Identifier & {parent: LogicalExpression}} node */
211+
'LogicalExpression>Identifier'(node) {
212+
if (node.parent.left !== node) {
213+
return
214+
}
215+
// Report only constants.
216+
const data = refReferences.get(node)
217+
if (
218+
!data ||
219+
!data.variableDeclaration ||
220+
data.variableDeclaration.kind !== 'const'
221+
) {
222+
return
223+
}
224+
reportIfRefWrapped(node)
225+
},
226+
// refValue ? x : y
227+
/** @param {Identifier & {parent: ConditionalExpression}} node */
228+
'ConditionalExpression>Identifier'(node) {
229+
if (node.parent.test !== node) {
230+
return
231+
}
232+
reportIfRefWrapped(node)
233+
},
234+
// `${refValue}`
235+
/** @param {Identifier} node */
236+
'TemplateLiteral>Identifier'(node) {
237+
reportIfRefWrapped(node)
238+
},
239+
// refValue.x
240+
/** @param {Identifier & {parent: MemberExpression}} node */
241+
'MemberExpression>Identifier'(node) {
242+
if (node.parent.object !== node) {
243+
return
244+
}
245+
const name = utils.getStaticPropertyName(node.parent)
246+
if (
247+
name === 'value' ||
248+
name == null ||
249+
// WritableComputedRef
250+
name === 'effect'
251+
) {
252+
return
253+
}
254+
reportIfRefWrapped(node)
126255
}
127-
reportIfRefWrapped(node)
128256
},
129-
// `${refValue}`
130-
/** @param {Identifier} node */
131-
'TemplateLiteral>Identifier'(node) {
132-
reportIfRefWrapped(node)
133-
},
134-
// refValue.x
135-
/** @param {Identifier & {parent: MemberExpression}} node */
136-
'MemberExpression>Identifier'(node) {
137-
if (node.parent.object !== node) {
138-
return
139-
}
140-
const name = utils.getStaticPropertyName(node.parent)
141-
if (
142-
name === 'value' ||
143-
name == null ||
144-
// WritableComputedRef
145-
name === 'effect'
146-
) {
147-
return
257+
utils.defineScriptSetupVisitor(context, {
258+
onDefineEmitsEnter(node) {
259+
if (
260+
!node.parent ||
261+
node.parent.type !== 'VariableDeclarator' ||
262+
node.parent.init !== node
263+
) {
264+
return
265+
}
266+
267+
const emitParam = node.parent.id
268+
if (emitParam.type !== 'Identifier') {
269+
return
270+
}
271+
272+
// const emit = defineEmits()
273+
const emitReferenceIds = new Set()
274+
collectReferenceIds(emitParam, emitReferenceIds)
275+
276+
setupContexts.set(programNode, {
277+
contextReferenceIds: new Set(),
278+
emitReferenceIds
279+
})
280+
},
281+
...callVisitor
282+
}),
283+
utils.defineVueVisitor(context, {
284+
onSetupFunctionEnter(node, { node: vueNode }) {
285+
const contextParam = utils.skipDefaultParamValue(node.params[1])
286+
if (!contextParam) {
287+
// no arguments
288+
return
289+
}
290+
if (
291+
contextParam.type === 'RestElement' ||
292+
contextParam.type === 'ArrayPattern'
293+
) {
294+
// cannot check
295+
return
296+
}
297+
298+
const contextReferenceIds = new Set()
299+
const emitReferenceIds = new Set()
300+
if (contextParam.type === 'ObjectPattern') {
301+
const emitProperty = utils.findAssignmentProperty(
302+
contextParam,
303+
'emit'
304+
)
305+
if (!emitProperty || emitProperty.value.type !== 'Identifier') {
306+
return
307+
}
308+
309+
// `setup(props, {emit})`
310+
collectReferenceIds(emitProperty.value, emitReferenceIds)
311+
} else {
312+
// `setup(props, context)`
313+
collectReferenceIds(contextParam, contextReferenceIds)
314+
}
315+
setupContexts.set(vueNode, {
316+
contextReferenceIds,
317+
emitReferenceIds
318+
})
319+
},
320+
...callVisitor,
321+
onVueObjectExit(node) {
322+
setupContexts.delete(node)
148323
}
149-
reportIfRefWrapped(node)
150-
}
151-
}
324+
})
325+
)
152326
}
153327
}

0 commit comments

Comments
 (0)