Thanks to visit codestin.com
Credit goes to github.com

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/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 (e.g. [`z.catch()`](https://zod.dev/api#catch)).

### `"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
106 changes: 94 additions & 12 deletions lib/rules/no-async-in-computed-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,88 @@ function isTimedFunction(node) {
)
}

/**
* @param {*} node
* @returns {*}
*/
function skipWrapper(node) {
while (node && node.expression) {
node = node.expression
}
return node
}

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

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

return null
}

/**
* @param {string} name
* @param {*} callee
* @returns {boolean}
*/
function isPromiseMethod(name, callee) {
return (
// hello.PROMISE_FUNCTION()
PROMISE_FUNCTIONS.has(name) ||
// Promise.PROMISE_METHOD()
(callee.object.type === 'Identifier' &&
callee.object.name === 'Promise' &&
PROMISE_METHODS.has(name))
)
}

/**
* @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' &&
callee.object.name === 'Promise' &&
PROMISE_METHODS.has(name)))
)
if (!name || !isPromiseMethod(name, callee)) {
return false
}

const rootObjectName = getRootObjectName(callee)
Copy link
Preview

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 +151,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 +174,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 +299,7 @@ module.exports = {
if (!scopeStack) {
return
}
if (isPromise(node)) {
if (isPromise(node, ignoredObjectNames)) {
verify(
node,
scopeStack.body,
Expand Down
159 changes: 159 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,73 @@ 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.method().catch(err => err).finally(() => {})
}
}
}
`,
options: [{ ignoredObjectNames: ['z'] }],
languageOptions: {
parser,
sourceType: 'module',
ecmaVersion: 2020
}
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
import { computed } from 'vue'
import { z } from 'zod'

const foo = computed(() => z.a?.['b'].c!.d.method().catch(err => err).finally(() => {}))
</script>`,
options: [{ ignoredObjectNames: ['z'] }],
languageOptions: {
parser: require('vue-eslint-parser'),
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
}
}
}
],

Expand Down Expand Up @@ -1542,6 +1609,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
}
]
}
]
})