diff --git a/.changeset/cute-bears-sneeze.md b/.changeset/cute-bears-sneeze.md
new file mode 100644
index 000000000..7bc9cb0bc
--- /dev/null
+++ b/.changeset/cute-bears-sneeze.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-vue': minor
+---
+
+Added `ignoredObjectNames` option to `vue/no-async-in-computed-properties`
diff --git a/docs/rules/no-async-in-computed-properties.md b/docs/rules/no-async-in-computed-properties.md
index 3cea89465..ff4f18d78 100644
--- a/docs/rules/no-async-in-computed-properties.md
+++ b/docs/rules/no-async-in-computed-properties.md
@@ -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"]`
+
+
+
+```vue
+
+```
+
+
## :books: Further Reading
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
index e8ed02f0e..0cf8c639c 100644
--- a/lib/rules/no-async-in-computed-properties.js
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -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} 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)
+ if (rootObjectName && ignoredObjectNames.has(rootObjectName)) {
+ return false
+ }
+
+ return true
}
return false
}
@@ -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.',
@@ -95,6 +174,9 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
+ const options = context.options[0] || {}
+ const ignoredObjectNames = new Set(options.ignoredObjectNames || [])
+
/** @type {Map} */
const computedPropertiesMap = new Map()
/** @type {(FunctionExpression | ArrowFunctionExpression)[]} */
@@ -217,7 +299,7 @@ module.exports = {
if (!scopeStack) {
return
}
- if (isPromise(node)) {
+ if (isPromise(node, ignoredObjectNames)) {
verify(
node,
scopeStack.body,
diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js
index 3f08c81e5..2c2e29c72 100644
--- a/tests/lib/rules/no-async-in-computed-properties.js
+++ b/tests/lib/rules/no-async-in-computed-properties.js
@@ -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: `
+ `,
+ 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: `
+ `,
+ options: [{ ignoredObjectNames: ['z'] }],
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ }
}
],
@@ -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: `
+
+ `,
+ 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
+ }
+ ]
}
]
})