Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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/cute-bears-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-vue': minor
---

Added `ignoredObjectNames` option to vue/no-async-in-computed-properties
37 changes: 36 additions & 1 deletion docs/rules/no-async-in-computed-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,42 @@ export default {

## :wrench: Options

Nothing.
```js
{
"vue/no-async-in-computed-properties": ["error", {
"ignoredObjectNames": []
}]
}
```

- `ignoredObjectNames`: An array of object names that should be ignored when used with promise-like methods (`.then()`, `.catch()`, `.finally()`). This is useful for validation libraries like Zod that use these method names for non-promise purposes.

### `"ignoredObjectNames": ["z"]`

<eslint-code-block :rules="{'vue/no-async-in-computed-properties': ['error', {ignoredObjectNames: ['z']}]}">

```vue
<script setup>
import { computed } from 'vue'
import { z } from 'zod'

/* ✓ GOOD */
const schema1 = computed(() => {
return z.string().catch('default')
})

const schema2 = computed(() => {
return z.catch(z.string().min(2), 'fallback')
})

/* ✗ BAD */
const fetchData = computed(() => {
return myFunc().then(res => res.json())
})
</script>
```

</eslint-code-block>

## :books: Further Reading

Expand Down
85 changes: 74 additions & 11 deletions lib/rules/no-async-in-computed-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,69 @@ function isTimedFunction(node) {
)
}

/**
* Get the root object name from a member expression chain
* @param {MemberExpression} memberExpr
* @returns {string|null}
*/
function getRootObjectName(memberExpr) {
let current = memberExpr.object

while (current) {
switch (current.type) {
case 'MemberExpression': {
current = utils.skipChainExpression(current.object)
break
}
case 'CallExpression': {
const calleeExpr = utils.skipChainExpression(current.callee)
if (calleeExpr.type === 'MemberExpression') {
current = calleeExpr.object
} else if (calleeExpr.type === 'Identifier') {
return calleeExpr.name
} else {
return null
}
break
}
case 'Identifier': {
return current.name
}
default: {
return null
}
}
}

return null
}
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function parameter memberExpr suggests it expects a MemberExpression, but the function accesses memberExpr.object without validation. If a non-MemberExpression node is passed, this will cause a runtime error when trying to access the object property.

Copilot uses AI. Check for mistakes.


/**
* @param {CallExpression} node
* @param {Set<string>} ignoredObjectNames
*/
function isPromise(node) {
function isPromise(node, ignoredObjectNames) {
const callee = utils.skipChainExpression(node.callee)
if (callee.type === 'MemberExpression') {
const name = utils.getStaticPropertyName(callee)
return (
name &&
// hello.PROMISE_FUNCTION()
(PROMISE_FUNCTIONS.has(name) ||
// Promise.PROMISE_METHOD()
(callee.object.type === 'Identifier' &&
if (
!name ||
(!PROMISE_FUNCTIONS.has(name) &&
!(
callee.object.type === 'Identifier' &&
callee.object.name === 'Promise' &&
PROMISE_METHODS.has(name)))
)
PROMISE_METHODS.has(name)
))
) {
return false
}

const rootObjectName = getRootObjectName(callee)
Copy link

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getRootObjectName function expects a MemberExpression but callee is already confirmed to be a MemberExpression at line 84. However, the function should validate its input parameter or the call should be made more explicit about the type being passed.

Copilot uses AI. Check for mistakes.

if (rootObjectName && ignoredObjectNames.has(rootObjectName)) {
return false
}

return true
}
return false
}
Expand Down Expand Up @@ -85,7 +132,20 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html'
},
fixable: null,
schema: [],
schema: [
{
type: 'object',
properties: {
ignoredObjectNames: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
unexpectedInFunction:
'Unexpected {{expressionName}} in computed function.',
Expand All @@ -95,6 +155,9 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
const options = context.options[0] || {}
const ignoredObjectNames = new Set(options.ignoredObjectNames || [])

/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
const computedPropertiesMap = new Map()
/** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
Expand Down Expand Up @@ -217,7 +280,7 @@ module.exports = {
if (!scopeStack) {
return
}
if (isPromise(node)) {
if (isPromise(node, ignoredObjectNames)) {
verify(
node,
scopeStack.body,
Expand Down
138 changes: 138 additions & 0 deletions tests/lib/rules/no-async-in-computed-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,52 @@ ruleTester.run('no-async-in-computed-properties', rule, {
sourceType: 'module',
ecmaVersion: 2020
}
},
{
filename: 'test.vue',
code: `
export default {
computed: {
foo: function () {
return z.catch(
z.string().check(z.minLength(2)),
'default'
).then(val => val).finally(() => {})
}
}
}
`,
options: [{ ignoredObjectNames: ['z'] }],
languageOptions
},
{
filename: 'test.vue',
code: `
<script setup>
import { computed } from 'vue'

const numberWithCatch = computed(() => z.number().catch(42))
</script>`,
options: [{ ignoredObjectNames: ['z'] }],
languageOptions: {
parser,
sourceType: 'module',
ecmaVersion: 2020
}
},
{
filename: 'test.vue',
code: `
export default {
computed: {
foo: function () {
return z.a.b.c.d.e.f.method().catch(err => err).finally(() => {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some more elaborate tests 😄

Suggested change
return z.a.b.c.d.e.f.method().catch(err => err).finally(() => {})
return z.a?.b!.['c'][d].e.f.method().catch(err => err).finally(() => {})

(The non-null assertion ! needs the typescript-eslint parser, so maybe this should be a separate test)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pushed a commit that adds a skipWrapper function to handle complex member expression chains with optional chaining support.

Decided not to use TypeScript AST node types directly since that could lead to a bunch of type conflicts...

}
}
}
`,
options: [{ ignoredObjectNames: ['z'] }],
languageOptions
}
],

Expand Down Expand Up @@ -1542,6 +1588,98 @@ ruleTester.run('no-async-in-computed-properties', rule, {
endColumn: 8
}
]
},
{
filename: 'test.vue',
code: `
export default {
computed: {
foo: function () {
return myFunc().catch('default')
}
}
}
`,
languageOptions,
errors: [
{
message: 'Unexpected asynchronous action in "foo" computed property.',
line: 5,
column: 22,
endLine: 5,
endColumn: 47
}
]
},
{
filename: 'test.vue',
code: `
export default {
computed: {
foo: function () {
return z.number().catch(42)
}
}
}
`,
languageOptions,
errors: [
{
message: 'Unexpected asynchronous action in "foo" computed property.',
line: 5,
column: 22,
endLine: 5,
endColumn: 42
}
]
},
{
filename: 'test.vue',
code: `
export default {
computed: {
foo: function () {
return someLib.string().catch(42)
}
}
}
`,
options: [{ ignoredObjectNames: ['z'] }],
languageOptions,
errors: [
{
message: 'Unexpected asynchronous action in "foo" computed property.',
line: 5,
column: 22,
endLine: 5,
endColumn: 48
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
import {computed} from 'vue'

const deepCall = computed(() => z.a.b.c.d().e().f().catch())
</script>
`,
options: [{ ignoredObjectNames: ['a'] }],
languageOptions: {
parser,
sourceType: 'module',
ecmaVersion: 2020
},
errors: [
{
message: 'Unexpected asynchronous action in computed function.',
line: 5,
column: 41,
endLine: 5,
endColumn: 68
}
]
}
]
})