Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit af3700f

Browse files
authoredFeb 20, 2025··
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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 248 additions & 74 deletions
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
}

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

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,106 @@ tester.run('no-ref-as-operand', rule, {
191191
model.value = value;
192192
}
193193
</script>
194+
`,
195+
`
196+
<script setup>
197+
const emit = defineEmits(['test'])
198+
const [model, mod] = defineModel();
199+
200+
function update() {
201+
emit('test', model.value)
202+
}
203+
</script>
204+
`,
205+
`
206+
<script>
207+
import { ref, defineComponent } from 'vue'
208+
209+
export default defineComponent({
210+
emits: ['incremented'],
211+
setup(_, ctx) {
212+
const counter = ref(0)
213+
214+
ctx.emit('incremented', counter.value)
215+
216+
return {
217+
counter
218+
}
219+
}
220+
})
221+
</script>
222+
`,
223+
`
224+
<script>
225+
import { ref, defineComponent } from 'vue'
226+
227+
export default defineComponent({
228+
emits: ['incremented'],
229+
setup(_, { emit }) {
230+
const counter = ref(0)
231+
232+
emit('incremented', counter.value)
233+
234+
return {
235+
counter
236+
}
237+
}
238+
})
239+
</script>
240+
`,
241+
`
242+
<script>
243+
import { ref, defineComponent } from 'vue'
244+
245+
export default defineComponent({
246+
emits: ['incremented'],
247+
setup(_, { emit }) {
248+
const counter = ref(0)
249+
250+
emit('incremented', counter.value, 'xxx')
251+
252+
return {
253+
counter
254+
}
255+
}
256+
})
257+
</script>
258+
`,
259+
`
260+
<script>
261+
import { ref, defineComponent } from 'vue'
262+
263+
export default defineComponent({
264+
emits: ['incremented'],
265+
setup(_, { emit }) {
266+
const counter = ref(0)
267+
268+
emit('incremented', 'xxx')
269+
270+
return {
271+
counter
272+
}
273+
}
274+
})
275+
</script>
276+
`,
277+
`
278+
<script>
279+
import { ref, defineComponent } from 'vue'
280+
281+
export default defineComponent({
282+
emits: ['incremented'],
283+
setup(_, { emit }) {
284+
const counter = ref(0)
285+
286+
emit('incremented')
287+
288+
return {
289+
counter
290+
}
291+
}
292+
})
293+
</script>
194294
`
195295
],
196296
invalid: [
@@ -823,6 +923,176 @@ tester.run('no-ref-as-operand', rule, {
823923
}
824924
]
825925
},
926+
{
927+
code: `
928+
<script setup>
929+
import { ref } from 'vue'
930+
const emits = defineEmits(['test'])
931+
const count = ref(0)
932+
933+
function update() {
934+
emits('test', count)
935+
}
936+
</script>
937+
`,
938+
output: `
939+
<script setup>
940+
import { ref } from 'vue'
941+
const emits = defineEmits(['test'])
942+
const count = ref(0)
943+
944+
function update() {
945+
emits('test', count.value)
946+
}
947+
</script>
948+
`,
949+
errors: [
950+
{
951+
message:
952+
'Must use `.value` to read or write the value wrapped by `ref()`.',
953+
line: 8,
954+
endLine: 8
955+
}
956+
]
957+
},
958+
{
959+
code: `
960+
<script>
961+
import { ref, defineComponent } from 'vue'
962+
963+
export default defineComponent({
964+
emits: ['incremented'],
965+
setup(_, ctx) {
966+
const counter = ref(0)
967+
968+
ctx.emit('incremented', counter)
969+
970+
return {
971+
counter
972+
}
973+
}
974+
})
975+
</script>
976+
`,
977+
output: `
978+
<script>
979+
import { ref, defineComponent } from 'vue'
980+
981+
export default defineComponent({
982+
emits: ['incremented'],
983+
setup(_, ctx) {
984+
const counter = ref(0)
985+
986+
ctx.emit('incremented', counter.value)
987+
988+
return {
989+
counter
990+
}
991+
}
992+
})
993+
</script>
994+
`,
995+
errors: [
996+
{
997+
message:
998+
'Must use `.value` to read or write the value wrapped by `ref()`.',
999+
line: 10,
1000+
endLine: 10
1001+
}
1002+
]
1003+
},
1004+
{
1005+
code: `
1006+
<script>
1007+
import { ref, defineComponent } from 'vue'
1008+
1009+
export default defineComponent({
1010+
emits: ['incremented'],
1011+
setup(_, { emit }) {
1012+
const counter = ref(0)
1013+
1014+
emit('incremented', counter)
1015+
1016+
return {
1017+
counter
1018+
}
1019+
}
1020+
})
1021+
</script>
1022+
`,
1023+
output: `
1024+
<script>
1025+
import { ref, defineComponent } from 'vue'
1026+
1027+
export default defineComponent({
1028+
emits: ['incremented'],
1029+
setup(_, { emit }) {
1030+
const counter = ref(0)
1031+
1032+
emit('incremented', counter.value)
1033+
1034+
return {
1035+
counter
1036+
}
1037+
}
1038+
})
1039+
</script>
1040+
`,
1041+
errors: [
1042+
{
1043+
message:
1044+
'Must use `.value` to read or write the value wrapped by `ref()`.',
1045+
line: 10,
1046+
endLine: 10
1047+
}
1048+
]
1049+
},
1050+
{
1051+
code: `
1052+
<script>
1053+
import { ref, defineComponent } from 'vue'
1054+
1055+
export default defineComponent({
1056+
emits: ['incremented'],
1057+
setup(_, { emit }) {
1058+
const counter = ref(0)
1059+
1060+
emit('incremented', 'xxx', counter)
1061+
1062+
return {
1063+
counter
1064+
}
1065+
}
1066+
})
1067+
</script>
1068+
`,
1069+
output: `
1070+
<script>
1071+
import { ref, defineComponent } from 'vue'
1072+
1073+
export default defineComponent({
1074+
emits: ['incremented'],
1075+
setup(_, { emit }) {
1076+
const counter = ref(0)
1077+
1078+
emit('incremented', 'xxx', counter.value)
1079+
1080+
return {
1081+
counter
1082+
}
1083+
}
1084+
})
1085+
</script>
1086+
`,
1087+
errors: [
1088+
{
1089+
message:
1090+
'Must use `.value` to read or write the value wrapped by `ref()`.',
1091+
line: 10,
1092+
endLine: 10
1093+
}
1094+
]
1095+
},
8261096
// Auto-import
8271097
{
8281098
code: `

0 commit comments

Comments
 (0)
Please sign in to comment.