Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/support-composable-ref-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-vue': minor
---

Enhanced `vue/no-ref-as-operand` rule to detect ref objects returned from composable functions
182 changes: 182 additions & 0 deletions lib/utils/ref-object-references.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,72 @@ function* iterateIdentifierReferences(id, globalScope) {
}
}

/**
* Check if a function returns a ref() call
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @returns {boolean}
*/
function checkFunctionReturnsRef(node) {
const body = node.body
if (!body) {
return false
}

// For arrow functions with expression body
if (
node.type === 'ArrowFunctionExpression' &&
body.type !== 'BlockStatement'
) {
return isRefCall(body)
}

// For function declarations and arrow functions with block body
if (body.type === 'BlockStatement') {
for (const stmt of body.body) {
if (stmt.type === 'ReturnStatement' && stmt.argument) {
return isRefCall(stmt.argument)
}
}
}

return false
}

/**
* Check if an expression is a ref() call or returns a ref object
* @param {Expression} expr
* @returns {boolean}
*/
function isRefCall(expr) {
// Direct ref() call
if (expr.type === 'CallExpression') {
const callee = expr.callee
if (callee.type === 'Identifier' && callee.name === 'ref') {
return true
}
}

// Object with ref properties: { data: ref(...) }
if (expr.type === 'ObjectExpression') {
for (const prop of expr.properties) {
if (prop.type === 'Property' && prop.value && isRefCall(prop.value)) {
return true
}
}
}

// Array with ref items: [ref(...)]
if (expr.type === 'ArrayExpression') {
for (const element of expr.elements) {
if (element && element.type !== 'SpreadElement' && isRefCall(element)) {
return true
}
}
}

return false
}

/**
* @param {RuleContext} context The rule context.
*/
Expand Down Expand Up @@ -415,6 +481,35 @@ class RefObjectReferenceExtractor {
this.processPattern(pattern, ctx)
}

/**
* Process composable function calls that return Ref
* @param {CallExpression} node
* @param {string} composableName
*/
processComposableRefCall(node, composableName) {
const parent = node.parent
/** @type {Pattern | null} */
let pattern = null
if (parent.type === 'VariableDeclarator') {
pattern = parent.id
} else if (
parent.type === 'AssignmentExpression' &&
parent.operator === '='
) {
pattern = parent.left
} else {
return
}

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

this.processPattern(pattern, ctx)
}

/**
* @param {MemberExpression | Identifier} node
* @param {RefObjectReferenceContext} ctx
Expand Down Expand Up @@ -547,6 +642,93 @@ function extractRefObjectReferences(context) {
references.processDefineModel(node)
}

// Process composable functions that return Ref by analyzing all function definitions
// Build a map of functions that return Ref by checking all scopes
const refReturningFunctions = new Map()

/**
* @param {import('eslint').Scope.Scope} scope
*/
function findRefReturningFunctions(scope) {
for (const variable of scope.variables) {
if (variable.defs.length === 1) {
const def = variable.defs[0]
// Function declaration
if (def.type === 'FunctionName') {
const node = def.node
if (checkFunctionReturnsRef(node)) {
refReturningFunctions.set(variable.name, node)
}
}
// Variable with function expression
else if (def.type === 'Variable' && def.node.init) {
const init = def.node.init
if (
(init.type === 'FunctionExpression' ||
init.type === 'ArrowFunctionExpression') &&
checkFunctionReturnsRef(init)
) {
refReturningFunctions.set(variable.name, init)
}
}
}
}
}

// Search all scopes for function definitions
const allScopes = sourceCode.scopeManager
? sourceCode.scopeManager.scopes
: []
for (const scope of allScopes) {
findRefReturningFunctions(scope)
}
if (!sourceCode.scopeManager) {
findRefReturningFunctions(globalScope)
}

// Now find all calls to these functions and process them
// We need to search through all variables, not just globalScope.variables
const searchedVariables = new Set()

for (const scope of allScopes) {
for (const variable of scope.variables) {
if (!searchedVariables.has(variable.name)) {
searchedVariables.add(variable.name)
if (refReturningFunctions.has(variable.name)) {
for (const ref of variable.references) {
const parent = ref.identifier.parent
// Check if this is a call expression to a composable function that returns Ref
if (
parent &&
parent.type === 'CallExpression' &&
parent.callee === ref.identifier
) {
references.processComposableRefCall(parent, variable.name)
}
}
}
}
}
}

if (!sourceCode.scopeManager) {
for (const variable of globalScope.variables) {
if (refReturningFunctions.has(variable.name)) {
for (const ref of variable.references) {
const parent = ref.identifier.parent
// Check if this is a call expression to a composable function that returns Ref
if (
parent &&
parent.type === 'CallExpression' &&
parent.callee === ref.identifier
) {
references.processComposableRefCall(parent, variable.name)
}
}
}
}
}

cacheForRefObjectReferences.set(sourceCode.ast, references)

return references
Expand Down
98 changes: 98 additions & 0 deletions tests/lib/rules/no-ref-as-operand.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,44 @@ tester.run('no-ref-as-operand', rule, {
}
})
</script>
`,
`
import { ref } from 'vue'

function useCount() {
return ref(0)
}

const count = useCount()
console.log(count.value)
`,
`
import { ref } from 'vue'

const useList = () => ref([])

const list = useList()
console.log(list.value)
`,
`
import { ref } from 'vue'

function useMultiple() {
return [ref(0), ref(1)]
}

const [a, b] = useMultiple()
console.log(a.value, b.value)
`,
`
import { ref } from 'vue'

function useRef() {
return ref(0)
}

const count = useRef()
count.value++
`
],
invalid: [
Expand Down Expand Up @@ -1249,6 +1287,66 @@ tester.run('no-ref-as-operand', rule, {
endColumn: 28
}
]
},
{
code: `
import { ref } from 'vue'

function useCount() {
return ref(0)
}

const count = useCount()
count++ // error
`,
output: `
import { ref } from 'vue'

function useCount() {
return ref(0)
}

const count = useCount()
count.value++ // error
`,
errors: [
{
message:
'Must use `.value` to read or write the value wrapped by `useCount()`.',
line: 9,
column: 7,
endLine: 9,
endColumn: 12
}
]
},
{
code: `
import { ref } from 'vue'

const useList = () => ref([])

const list = useList()
list + 1 // error
`,
output: `
import { ref } from 'vue'

const useList = () => ref([])

const list = useList()
list.value + 1 // error
`,
errors: [
{
message:
'Must use `.value` to read or write the value wrapped by `useList()`.',
line: 7,
column: 7,
endLine: 7,
endColumn: 11
}
]
}
]
})