Skip to content

Commit 2507cc9

Browse files
committed
feat(no-ref-as-operand): Add composable function ref detection support
Implement comprehensive support for detecting ref objects returned from composable functions in the no-ref-as-operand rule. This enhancement includes: Type Information Foundation: - Add utility functions to detect functions that return Ref objects - checkFunctionReturnsRef(): Analyzes function bodies for ref returns - isRefCall(): Recursively checks for ref() calls in various patterns - Support for multiple return patterns: * Direct ref() calls: return ref(0) * Object properties: return { data: ref(0) } * Array elements: return [ref(0)] Composable Function Detection: - Implement processComposableRefCall() method for handling composable function calls - Build map of ref-returning functions via body analysis (not JSDoc) - Detect variable assignments from composable function calls - Properly handle all scope contexts, not just global scope - Generate appropriate error messages showing composable function names Testing: - Add 6 new test cases covering composable function ref detection - Test valid usage with proper .value access - Test invalid usage without .value access - All 55 tests pass (49 existing + 6 new) This approach is more robust than JSDoc detection as it analyzes actual code structure and works reliably in test environments.
1 parent b47d479 commit 2507cc9

File tree

3 files changed

+285
-0
lines changed

3 files changed

+285
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-vue': minor
3+
---
4+
5+
Enhanced `vue/no-ref-as-operand` rule to detect ref objects returned from composable functions

lib/utils/ref-object-references.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,72 @@ function* iterateIdentifierReferences(id, globalScope) {
275275
}
276276
}
277277

278+
/**
279+
* Check if a function returns a ref() call
280+
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
281+
* @returns {boolean}
282+
*/
283+
function checkFunctionReturnsRef(node) {
284+
const body = node.body
285+
if (!body) {
286+
return false
287+
}
288+
289+
// For arrow functions with expression body
290+
if (
291+
node.type === 'ArrowFunctionExpression' &&
292+
body.type !== 'BlockStatement'
293+
) {
294+
return isRefCall(body)
295+
}
296+
297+
// For function declarations and arrow functions with block body
298+
if (body.type === 'BlockStatement') {
299+
for (const stmt of body.body) {
300+
if (stmt.type === 'ReturnStatement' && stmt.argument) {
301+
return isRefCall(stmt.argument)
302+
}
303+
}
304+
}
305+
306+
return false
307+
}
308+
309+
/**
310+
* Check if an expression is a ref() call or returns a ref object
311+
* @param {Expression} expr
312+
* @returns {boolean}
313+
*/
314+
function isRefCall(expr) {
315+
// Direct ref() call
316+
if (expr.type === 'CallExpression') {
317+
const callee = expr.callee
318+
if (callee.type === 'Identifier' && callee.name === 'ref') {
319+
return true
320+
}
321+
}
322+
323+
// Object with ref properties: { data: ref(...) }
324+
if (expr.type === 'ObjectExpression') {
325+
for (const prop of expr.properties) {
326+
if (prop.type === 'Property' && prop.value && isRefCall(prop.value)) {
327+
return true
328+
}
329+
}
330+
}
331+
332+
// Array with ref items: [ref(...)]
333+
if (expr.type === 'ArrayExpression') {
334+
for (const element of expr.elements) {
335+
if (element && element.type !== 'SpreadElement' && isRefCall(element)) {
336+
return true
337+
}
338+
}
339+
}
340+
341+
return false
342+
}
343+
278344
/**
279345
* @param {RuleContext} context The rule context.
280346
*/
@@ -415,6 +481,35 @@ class RefObjectReferenceExtractor {
415481
this.processPattern(pattern, ctx)
416482
}
417483

484+
/**
485+
* Process composable function calls that return Ref
486+
* @param {CallExpression} node
487+
* @param {string} composableName
488+
*/
489+
processComposableRefCall(node, composableName) {
490+
const parent = node.parent
491+
/** @type {Pattern | null} */
492+
let pattern = null
493+
if (parent.type === 'VariableDeclarator') {
494+
pattern = parent.id
495+
} else if (
496+
parent.type === 'AssignmentExpression' &&
497+
parent.operator === '='
498+
) {
499+
pattern = parent.left
500+
} else {
501+
return
502+
}
503+
504+
const ctx = {
505+
method: composableName,
506+
define: node,
507+
defineChain: [node]
508+
}
509+
510+
this.processPattern(pattern, ctx)
511+
}
512+
418513
/**
419514
* @param {MemberExpression | Identifier} node
420515
* @param {RefObjectReferenceContext} ctx
@@ -547,6 +642,93 @@ function extractRefObjectReferences(context) {
547642
references.processDefineModel(node)
548643
}
549644

645+
// Process composable functions that return Ref by analyzing all function definitions
646+
// Build a map of functions that return Ref by checking all scopes
647+
const refReturningFunctions = new Map()
648+
649+
/**
650+
* @param {import('eslint').Scope.Scope} scope
651+
*/
652+
function findRefReturningFunctions(scope) {
653+
for (const variable of scope.variables) {
654+
if (variable.defs.length === 1) {
655+
const def = variable.defs[0]
656+
// Function declaration
657+
if (def.type === 'FunctionName') {
658+
const node = def.node
659+
if (checkFunctionReturnsRef(node)) {
660+
refReturningFunctions.set(variable.name, node)
661+
}
662+
}
663+
// Variable with function expression
664+
else if (def.type === 'Variable' && def.node.init) {
665+
const init = def.node.init
666+
if (
667+
(init.type === 'FunctionExpression' ||
668+
init.type === 'ArrowFunctionExpression') &&
669+
checkFunctionReturnsRef(init)
670+
) {
671+
refReturningFunctions.set(variable.name, init)
672+
}
673+
}
674+
}
675+
}
676+
}
677+
678+
// Search all scopes for function definitions
679+
const allScopes = sourceCode.scopeManager
680+
? sourceCode.scopeManager.scopes
681+
: []
682+
for (const scope of allScopes) {
683+
findRefReturningFunctions(scope)
684+
}
685+
if (!sourceCode.scopeManager) {
686+
findRefReturningFunctions(globalScope)
687+
}
688+
689+
// Now find all calls to these functions and process them
690+
// We need to search through all variables, not just globalScope.variables
691+
const searchedVariables = new Set()
692+
693+
for (const scope of allScopes) {
694+
for (const variable of scope.variables) {
695+
if (!searchedVariables.has(variable.name)) {
696+
searchedVariables.add(variable.name)
697+
if (refReturningFunctions.has(variable.name)) {
698+
for (const ref of variable.references) {
699+
const parent = ref.identifier.parent
700+
// Check if this is a call expression to a composable function that returns Ref
701+
if (
702+
parent &&
703+
parent.type === 'CallExpression' &&
704+
parent.callee === ref.identifier
705+
) {
706+
references.processComposableRefCall(parent, variable.name)
707+
}
708+
}
709+
}
710+
}
711+
}
712+
}
713+
714+
if (!sourceCode.scopeManager) {
715+
for (const variable of globalScope.variables) {
716+
if (refReturningFunctions.has(variable.name)) {
717+
for (const ref of variable.references) {
718+
const parent = ref.identifier.parent
719+
// Check if this is a call expression to a composable function that returns Ref
720+
if (
721+
parent &&
722+
parent.type === 'CallExpression' &&
723+
parent.callee === ref.identifier
724+
) {
725+
references.processComposableRefCall(parent, variable.name)
726+
}
727+
}
728+
}
729+
}
730+
}
731+
550732
cacheForRefObjectReferences.set(sourceCode.ast, references)
551733

552734
return references

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,44 @@ tester.run('no-ref-as-operand', rule, {
307307
}
308308
})
309309
</script>
310+
`,
311+
`
312+
import { ref } from 'vue'
313+
314+
function useCount() {
315+
return ref(0)
316+
}
317+
318+
const count = useCount()
319+
console.log(count.value)
320+
`,
321+
`
322+
import { ref } from 'vue'
323+
324+
const useList = () => ref([])
325+
326+
const list = useList()
327+
console.log(list.value)
328+
`,
329+
`
330+
import { ref } from 'vue'
331+
332+
function useMultiple() {
333+
return [ref(0), ref(1)]
334+
}
335+
336+
const [a, b] = useMultiple()
337+
console.log(a.value, b.value)
338+
`,
339+
`
340+
import { ref } from 'vue'
341+
342+
function useRef() {
343+
return ref(0)
344+
}
345+
346+
const count = useRef()
347+
count.value++
310348
`
311349
],
312350
invalid: [
@@ -1249,6 +1287,66 @@ tester.run('no-ref-as-operand', rule, {
12491287
endColumn: 28
12501288
}
12511289
]
1290+
},
1291+
{
1292+
code: `
1293+
import { ref } from 'vue'
1294+
1295+
function useCount() {
1296+
return ref(0)
1297+
}
1298+
1299+
const count = useCount()
1300+
count++ // error
1301+
`,
1302+
output: `
1303+
import { ref } from 'vue'
1304+
1305+
function useCount() {
1306+
return ref(0)
1307+
}
1308+
1309+
const count = useCount()
1310+
count.value++ // error
1311+
`,
1312+
errors: [
1313+
{
1314+
message:
1315+
'Must use `.value` to read or write the value wrapped by `useCount()`.',
1316+
line: 9,
1317+
column: 7,
1318+
endLine: 9,
1319+
endColumn: 12
1320+
}
1321+
]
1322+
},
1323+
{
1324+
code: `
1325+
import { ref } from 'vue'
1326+
1327+
const useList = () => ref([])
1328+
1329+
const list = useList()
1330+
list + 1 // error
1331+
`,
1332+
output: `
1333+
import { ref } from 'vue'
1334+
1335+
const useList = () => ref([])
1336+
1337+
const list = useList()
1338+
list.value + 1 // error
1339+
`,
1340+
errors: [
1341+
{
1342+
message:
1343+
'Must use `.value` to read or write the value wrapped by `useList()`.',
1344+
line: 7,
1345+
column: 7,
1346+
endLine: 7,
1347+
endColumn: 11
1348+
}
1349+
]
12521350
}
12531351
]
12541352
})

0 commit comments

Comments
 (0)