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

Added ignoredObjectNames option to vue/no-async-in-computed-properties #2927

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

Open
waynzh wants to merge 7 commits into master
base: master
Choose a base branch
Loading
from ignore-object-names
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
View file Open in desktop
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
View file Open in desktop
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
View file Open in desktop
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
View file Open in desktop
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
}
]
}
]
})

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