Skip to content

Commit

Permalink
Fix false positives for uninitialized vars in vue/no-ref-as-operand
Browse files Browse the repository at this point in the history
… rule (#1988)

* Fix false positives for vars that is not initialized in `vue/no-ref-as-operand` rule

* fix test
  • Loading branch information
ota-meshi committed Oct 4, 2022
1 parent 2e54472 commit 228b49f
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 31 deletions.
15 changes: 14 additions & 1 deletion lib/rules/no-ref-as-operand.js
Expand Up @@ -9,8 +9,21 @@ const utils = require('../utils')

/**
* @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences
* @typedef {import('../utils/ref-object-references').RefObjectReferenceForIdentifier} RefObjectReferenceForIdentifier
*/

/**
* Checks whether the given identifier reference has been initialized with a ref object.
* @param {RefObjectReferenceForIdentifier | null} data
* @returns {data is RefObjectReferenceForIdentifier}
*/
function isRefInit(data) {
const init = data && data.variableDeclarator && data.variableDeclarator.init
if (!init) {
return false
}
return data.defineChain.includes(/** @type {any} */ (init))
}
module.exports = {
meta: {
type: 'suggestion',
Expand All @@ -37,7 +50,7 @@ module.exports = {
*/
function reportIfRefWrapped(node) {
const data = refReferences.get(node)
if (!data) {
if (!isRefInit(data)) {
return
}
context.report({
Expand Down
78 changes: 48 additions & 30 deletions lib/utils/ref-object-references.js
Expand Up @@ -20,19 +20,23 @@ const { ReferenceTracker } = eslintUtils
* @property {MemberExpression | CallExpression} node
* @property {string} method
* @property {CallExpression} define
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
*
* @typedef {object} RefObjectReferenceForPattern
* @property {'pattern'} type
* @property {ObjectPattern} node
* @property {string} method
* @property {CallExpression} define
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
*
* @typedef {object} RefObjectReferenceForIdentifier
* @property {'expression' | 'pattern'} type
* @property {Identifier} node
* @property {VariableDeclarator | null} variableDeclarator
* @property {VariableDeclaration | null} variableDeclaration
* @property {string} method
* @property {CallExpression} define
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
*
* @typedef {RefObjectReferenceForIdentifier | RefObjectReferenceForExpression | RefObjectReferenceForPattern} RefObjectReference
*/
Expand Down Expand Up @@ -258,6 +262,13 @@ module.exports = {
extractReactiveVariableReferences
}

/**
* @typedef {object} RefObjectReferenceContext
* @property {string} method
* @property {CallExpression} define
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
*/

/**
* @implements {RefObjectReferences}
*/
Expand Down Expand Up @@ -312,12 +323,19 @@ class RefObjectReferenceExtractor {
type: 'expression',
node,
method,
define: node
define: node,
defineChain: [node]
})
}
return
}

const ctx = {
method,
define: node,
defineChain: [node]
}

if (method === 'toRefs') {
const propertyReferenceExtractor = definePropertyReferenceExtractor(
this.context
Expand All @@ -327,63 +345,65 @@ class RefObjectReferenceExtractor {
for (const name of propertyReferences.allProperties().keys()) {
for (const nest of propertyReferences.getNestNodes(name)) {
if (nest.type === 'expression') {
this.processMemberExpression(nest.node, method, node)
this.processMemberExpression(nest.node, ctx)
} else if (nest.type === 'pattern') {
this.processPattern(nest.node, method, node)
this.processPattern(nest.node, ctx)
}
}
}
} else {
this.processPattern(pattern, method, node)
this.processPattern(pattern, ctx)
}
}

/**
* @param {Expression} node
* @param {string} method
* @param {CallExpression} define
* @param {MemberExpression | Identifier} node
* @param {RefObjectReferenceContext} ctx
*/
processExpression(node, method, define) {
processExpression(node, ctx) {
const parent = node.parent
if (parent.type === 'AssignmentExpression') {
if (parent.operator === '=' && parent.right === node) {
// `(foo = obj.mem)`
this.processPattern(parent.left, method, define)
this.processPattern(parent.left, {
...ctx,
defineChain: [node, ...ctx.defineChain]
})
return true
}
} else if (parent.type === 'VariableDeclarator' && parent.init === node) {
// `const foo = obj.mem`
this.processPattern(parent.id, method, define)
this.processPattern(parent.id, {
...ctx,
defineChain: [node, ...ctx.defineChain]
})
return true
}
return false
}
/**
* @param {MemberExpression} node
* @param {string} method
* @param {CallExpression} define
* @param {RefObjectReferenceContext} ctx
*/
processMemberExpression(node, method, define) {
if (this.processExpression(node, method, define)) {
processMemberExpression(node, ctx) {
if (this.processExpression(node, ctx)) {
return
}
this.references.set(node, {
type: 'expression',
node,
method,
define
...ctx
})
}

/**
* @param {Pattern} node
* @param {string} method
* @param {CallExpression} define
* @param {RefObjectReferenceContext} ctx
*/
processPattern(node, method, define) {
processPattern(node, ctx) {
switch (node.type) {
case 'Identifier': {
this.processIdentifierPattern(node, method, define)
this.processIdentifierPattern(node, ctx)
break
}
case 'ArrayPattern':
Expand All @@ -395,13 +415,12 @@ class RefObjectReferenceExtractor {
this.references.set(node, {
type: 'pattern',
node,
method,
define
...ctx
})
return
}
case 'AssignmentPattern': {
this.processPattern(node.left, method, define)
this.processPattern(node.left, ctx)
return
}
// No default
Expand All @@ -410,10 +429,9 @@ class RefObjectReferenceExtractor {

/**
* @param {Identifier} node
* @param {string} method
* @param {CallExpression} define
* @param {RefObjectReferenceContext} ctx
*/
processIdentifierPattern(node, method, define) {
processIdentifierPattern(node, ctx) {
if (this._processedIds.has(node)) {
return
}
Expand All @@ -434,16 +452,16 @@ class RefObjectReferenceExtractor {
}
if (
reference.isRead() &&
this.processExpression(reference.identifier, method, define)
this.processExpression(reference.identifier, ctx)
) {
continue
}
this.references.set(reference.identifier, {
type: reference.isWrite() ? 'pattern' : 'expression',
node: reference.identifier,
method,
define,
variableDeclaration: def ? def.parent : null
variableDeclarator: def ? def.node : null,
variableDeclaration: def ? def.parent : null,
...ctx
})
}
}
Expand Down
58 changes: 58 additions & 0 deletions tests/lib/rules/no-ref-as-operand.js
Expand Up @@ -148,6 +148,30 @@ tester.run('no-ref-as-operand', rule, {
const isComp = foo.effect
</script>
`
},
{
code: `
<script>
import { ref } from 'vue'
let foo;
if (!foo) {
foo = ref(5);
}
</script>
`
},
{
code: `
<script>
import { ref } from 'vue'
let foo = undefined;
if (!foo) {
foo = ref(5);
}
</script>
`
}
],
invalid: [
Expand Down Expand Up @@ -669,6 +693,40 @@ tester.run('no-ref-as-operand', rule, {
messageId: 'requireDotValue'
}
]
},
{
code: `
<script>
import { ref } from 'vue'
let foo = undefined;
if (!foo) {
foo = ref(5);
}
let bar = foo;
bar = 4;
</script>
`,
output: `
<script>
import { ref } from 'vue'
let foo = undefined;
if (!foo) {
foo = ref(5);
}
let bar = foo;
bar.value = 4;
</script>
`,
errors: [
{
message:
'Must use `.value` to read or write the value wrapped by `ref()`.',
line: 10,
column: 7
}
]
}
]
})

0 comments on commit 228b49f

Please sign in to comment.