Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(no-ref-as-operand): support ref detection from composable functions #2954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
kzhrk wants to merge 1 commit into vuejs:master from kzhrk:issue-2519
Closed
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
View file Open in desktop
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
View file Open in desktop
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
View file Open in desktop
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
}
]
}
]
})

AltStyle によって変換されたページ (->オリジナル) /