From 228b49facb1e7ac6cd1d4d529b1d349d772240a5 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 4 Oct 2022 13:39:45 +0900 Subject: [PATCH] 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 --- lib/rules/no-ref-as-operand.js | 15 +++++- lib/utils/ref-object-references.js | 78 +++++++++++++++++----------- tests/lib/rules/no-ref-as-operand.js | 58 +++++++++++++++++++++ 3 files changed, 120 insertions(+), 31 deletions(-) diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js index 067be4ec3..9661b2b60 100644 --- a/lib/rules/no-ref-as-operand.js +++ b/lib/rules/no-ref-as-operand.js @@ -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', @@ -37,7 +50,7 @@ module.exports = { */ function reportIfRefWrapped(node) { const data = refReferences.get(node) - if (!data) { + if (!isRefInit(data)) { return } context.report({ diff --git a/lib/utils/ref-object-references.js b/lib/utils/ref-object-references.js index 1b3781bbb..0dd9abcd4 100644 --- a/lib/utils/ref-object-references.js +++ b/lib/utils/ref-object-references.js @@ -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 */ @@ -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} */ @@ -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 @@ -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': @@ -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 @@ -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 } @@ -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 }) } } diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index fb564ce17..1cdbb1041 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -148,6 +148,30 @@ tester.run('no-ref-as-operand', rule, { const isComp = foo.effect ` + }, + { + code: ` + + ` + }, + { + code: ` + + ` } ], invalid: [ @@ -669,6 +693,40 @@ tester.run('no-ref-as-operand', rule, { messageId: 'requireDotValue' } ] + }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 10, + column: 7 + } + ] } ] })