Skip to content

Commit 228b49f

Browse files
authored
Fix false positives for uninitialized vars in vue/no-ref-as-operand rule (#1988)
* Fix false positives for vars that is not initialized in `vue/no-ref-as-operand` rule * fix test
1 parent 2e54472 commit 228b49f

File tree

3 files changed

+120
-31
lines changed

3 files changed

+120
-31
lines changed

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,21 @@ const utils = require('../utils')
99

1010
/**
1111
* @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
12+
* @typedef {import('../utils/ref-object-references').RefObjectReferenceForIdentifier} RefObjectReferenceForIdentifier
1213
*/
1314

15+
/**
16+
* Checks whether the given identifier reference has been initialized with a ref object.
17+
* @param {RefObjectReferenceForIdentifier | null} data
18+
* @returns {data is RefObjectReferenceForIdentifier}
19+
*/
20+
function isRefInit(data) {
21+
const init = data && data.variableDeclarator && data.variableDeclarator.init
22+
if (!init) {
23+
return false
24+
}
25+
return data.defineChain.includes(/** @type {any} */ (init))
26+
}
1427
module.exports = {
1528
meta: {
1629
type: 'suggestion',
@@ -37,7 +50,7 @@ module.exports = {
3750
*/
3851
function reportIfRefWrapped(node) {
3952
const data = refReferences.get(node)
40-
if (!data) {
53+
if (!isRefInit(data)) {
4154
return
4255
}
4356
context.report({

lib/utils/ref-object-references.js

+48-30
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,23 @@ const { ReferenceTracker } = eslintUtils
2020
* @property {MemberExpression | CallExpression} node
2121
* @property {string} method
2222
* @property {CallExpression} define
23+
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
2324
*
2425
* @typedef {object} RefObjectReferenceForPattern
2526
* @property {'pattern'} type
2627
* @property {ObjectPattern} node
2728
* @property {string} method
2829
* @property {CallExpression} define
30+
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
2931
*
3032
* @typedef {object} RefObjectReferenceForIdentifier
3133
* @property {'expression' | 'pattern'} type
3234
* @property {Identifier} node
35+
* @property {VariableDeclarator | null} variableDeclarator
3336
* @property {VariableDeclaration | null} variableDeclaration
3437
* @property {string} method
3538
* @property {CallExpression} define
39+
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
3640
*
3741
* @typedef {RefObjectReferenceForIdentifier | RefObjectReferenceForExpression | RefObjectReferenceForPattern} RefObjectReference
3842
*/
@@ -258,6 +262,13 @@ module.exports = {
258262
extractReactiveVariableReferences
259263
}
260264

265+
/**
266+
* @typedef {object} RefObjectReferenceContext
267+
* @property {string} method
268+
* @property {CallExpression} define
269+
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
270+
*/
271+
261272
/**
262273
* @implements {RefObjectReferences}
263274
*/
@@ -312,12 +323,19 @@ class RefObjectReferenceExtractor {
312323
type: 'expression',
313324
node,
314325
method,
315-
define: node
326+
define: node,
327+
defineChain: [node]
316328
})
317329
}
318330
return
319331
}
320332

333+
const ctx = {
334+
method,
335+
define: node,
336+
defineChain: [node]
337+
}
338+
321339
if (method === 'toRefs') {
322340
const propertyReferenceExtractor = definePropertyReferenceExtractor(
323341
this.context
@@ -327,63 +345,65 @@ class RefObjectReferenceExtractor {
327345
for (const name of propertyReferences.allProperties().keys()) {
328346
for (const nest of propertyReferences.getNestNodes(name)) {
329347
if (nest.type === 'expression') {
330-
this.processMemberExpression(nest.node, method, node)
348+
this.processMemberExpression(nest.node, ctx)
331349
} else if (nest.type === 'pattern') {
332-
this.processPattern(nest.node, method, node)
350+
this.processPattern(nest.node, ctx)
333351
}
334352
}
335353
}
336354
} else {
337-
this.processPattern(pattern, method, node)
355+
this.processPattern(pattern, ctx)
338356
}
339357
}
340358

341359
/**
342-
* @param {Expression} node
343-
* @param {string} method
344-
* @param {CallExpression} define
360+
* @param {MemberExpression | Identifier} node
361+
* @param {RefObjectReferenceContext} ctx
345362
*/
346-
processExpression(node, method, define) {
363+
processExpression(node, ctx) {
347364
const parent = node.parent
348365
if (parent.type === 'AssignmentExpression') {
349366
if (parent.operator === '=' && parent.right === node) {
350367
// `(foo = obj.mem)`
351-
this.processPattern(parent.left, method, define)
368+
this.processPattern(parent.left, {
369+
...ctx,
370+
defineChain: [node, ...ctx.defineChain]
371+
})
352372
return true
353373
}
354374
} else if (parent.type === 'VariableDeclarator' && parent.init === node) {
355375
// `const foo = obj.mem`
356-
this.processPattern(parent.id, method, define)
376+
this.processPattern(parent.id, {
377+
...ctx,
378+
defineChain: [node, ...ctx.defineChain]
379+
})
357380
return true
358381
}
359382
return false
360383
}
361384
/**
362385
* @param {MemberExpression} node
363-
* @param {string} method
364-
* @param {CallExpression} define
386+
* @param {RefObjectReferenceContext} ctx
365387
*/
366-
processMemberExpression(node, method, define) {
367-
if (this.processExpression(node, method, define)) {
388+
processMemberExpression(node, ctx) {
389+
if (this.processExpression(node, ctx)) {
368390
return
369391
}
370392
this.references.set(node, {
371393
type: 'expression',
372394
node,
373-
method,
374-
define
395+
...ctx
375396
})
376397
}
377398

378399
/**
379400
* @param {Pattern} node
380-
* @param {string} method
381-
* @param {CallExpression} define
401+
* @param {RefObjectReferenceContext} ctx
382402
*/
383-
processPattern(node, method, define) {
403+
processPattern(node, ctx) {
384404
switch (node.type) {
385405
case 'Identifier': {
386-
this.processIdentifierPattern(node, method, define)
406+
this.processIdentifierPattern(node, ctx)
387407
break
388408
}
389409
case 'ArrayPattern':
@@ -395,13 +415,12 @@ class RefObjectReferenceExtractor {
395415
this.references.set(node, {
396416
type: 'pattern',
397417
node,
398-
method,
399-
define
418+
...ctx
400419
})
401420
return
402421
}
403422
case 'AssignmentPattern': {
404-
this.processPattern(node.left, method, define)
423+
this.processPattern(node.left, ctx)
405424
return
406425
}
407426
// No default
@@ -410,10 +429,9 @@ class RefObjectReferenceExtractor {
410429

411430
/**
412431
* @param {Identifier} node
413-
* @param {string} method
414-
* @param {CallExpression} define
432+
* @param {RefObjectReferenceContext} ctx
415433
*/
416-
processIdentifierPattern(node, method, define) {
434+
processIdentifierPattern(node, ctx) {
417435
if (this._processedIds.has(node)) {
418436
return
419437
}
@@ -434,16 +452,16 @@ class RefObjectReferenceExtractor {
434452
}
435453
if (
436454
reference.isRead() &&
437-
this.processExpression(reference.identifier, method, define)
455+
this.processExpression(reference.identifier, ctx)
438456
) {
439457
continue
440458
}
441459
this.references.set(reference.identifier, {
442460
type: reference.isWrite() ? 'pattern' : 'expression',
443461
node: reference.identifier,
444-
method,
445-
define,
446-
variableDeclaration: def ? def.parent : null
462+
variableDeclarator: def ? def.node : null,
463+
variableDeclaration: def ? def.parent : null,
464+
...ctx
447465
})
448466
}
449467
}

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

+58
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,30 @@ tester.run('no-ref-as-operand', rule, {
148148
const isComp = foo.effect
149149
</script>
150150
`
151+
},
152+
{
153+
code: `
154+
<script>
155+
import { ref } from 'vue'
156+
let foo;
157+
158+
if (!foo) {
159+
foo = ref(5);
160+
}
161+
</script>
162+
`
163+
},
164+
{
165+
code: `
166+
<script>
167+
import { ref } from 'vue'
168+
let foo = undefined;
169+
170+
if (!foo) {
171+
foo = ref(5);
172+
}
173+
</script>
174+
`
151175
}
152176
],
153177
invalid: [
@@ -669,6 +693,40 @@ tester.run('no-ref-as-operand', rule, {
669693
messageId: 'requireDotValue'
670694
}
671695
]
696+
},
697+
{
698+
code: `
699+
<script>
700+
import { ref } from 'vue'
701+
let foo = undefined;
702+
703+
if (!foo) {
704+
foo = ref(5);
705+
}
706+
let bar = foo;
707+
bar = 4;
708+
</script>
709+
`,
710+
output: `
711+
<script>
712+
import { ref } from 'vue'
713+
let foo = undefined;
714+
715+
if (!foo) {
716+
foo = ref(5);
717+
}
718+
let bar = foo;
719+
bar.value = 4;
720+
</script>
721+
`,
722+
errors: [
723+
{
724+
message:
725+
'Must use `.value` to read or write the value wrapped by `ref()`.',
726+
line: 10,
727+
column: 7
728+
}
729+
]
672730
}
673731
]
674732
})

0 commit comments

Comments
 (0)