From 0a556c5e79d7f452ccb63face006fc46f7e73351 Mon Sep 17 00:00:00 2001 From: waynzh Date: Fri, 5 Sep 2025 14:56:50 +0800 Subject: [PATCH 1/7] feat(no-async-in-computed-properties): add `ignoredObjectNames` option --- docs/rules/no-async-in-computed-properties.md | 37 ++++- lib/rules/no-async-in-computed-properties.js | 78 ++++++++-- .../rules/no-async-in-computed-properties.js | 138 ++++++++++++++++++ 3 files changed, 240 insertions(+), 13 deletions(-) diff --git a/docs/rules/no-async-in-computed-properties.md b/docs/rules/no-async-in-computed-properties.md index 3cea89465..f6d7d043d 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. + +### `"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..007126871 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -38,22 +38,60 @@ 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) { + if (current.type === 'MemberExpression') { + current = utils.skipChainExpression(current.object) + } else if (current.type === 'CallExpression') { + const calleeExpr = utils.skipChainExpression(current.callee) + if (calleeExpr.type === 'MemberExpression') { + current = calleeExpr.object + } else if (calleeExpr.type === 'Identifier') { + return calleeExpr.name + } else { + break + } + } else if (current.type === 'Identifier') { + return current.name + } else { + break + } + } + + return null +} + /** * @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) return false + + const isPromiseMethod = + PROMISE_FUNCTIONS.has(name) || + (callee.object.type === 'Identifier' && + callee.object.name === 'Promise' && + PROMISE_METHODS.has(name)) + + if (!isPromiseMethod) return false + + const rootObjectName = getRootObjectName(callee) + if (rootObjectName && ignoredObjectNames.has(rootObjectName)) { + return false + } + + return true } return false } @@ -85,7 +123,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 +146,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 +271,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..c9d6ce87a 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -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: ` + `, + 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(() => {}) + } + } + } + `, + options: [{ ignoredObjectNames: ['z'] }], + languageOptions } ], @@ -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: ` + + `, + 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 + } + ] } ] }) From fdff3d9ac7cb446b5aad790cd89cf2248e6c5694 Mon Sep 17 00:00:00 2001 From: waynzh Date: Fri, 5 Sep 2025 14:58:09 +0800 Subject: [PATCH 2/7] chore: add changeset --- .changeset/cute-bears-sneeze.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cute-bears-sneeze.md diff --git a/.changeset/cute-bears-sneeze.md b/.changeset/cute-bears-sneeze.md new file mode 100644 index 000000000..4c1ad0a90 --- /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 From 0f4d3f3becfc737acffbee70d52ef949acf12e43 Mon Sep 17 00:00:00 2001 From: waynzh Date: Fri, 5 Sep 2025 16:30:21 +0800 Subject: [PATCH 3/7] update --- lib/rules/no-async-in-computed-properties.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 007126871..01300eb30 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -76,15 +76,17 @@ function isPromise(node, ignoredObjectNames) { const callee = utils.skipChainExpression(node.callee) if (callee.type === 'MemberExpression') { const name = utils.getStaticPropertyName(callee) - if (!name) return false - - const isPromiseMethod = - PROMISE_FUNCTIONS.has(name) || - (callee.object.type === 'Identifier' && - callee.object.name === 'Promise' && - PROMISE_METHODS.has(name)) - - if (!isPromiseMethod) return false + if ( + !name || + (!PROMISE_FUNCTIONS.has(name) && + !( + callee.object.type === 'Identifier' && + callee.object.name === 'Promise' && + PROMISE_METHODS.has(name) + )) + ) { + return false + } const rootObjectName = getRootObjectName(callee) if (rootObjectName && ignoredObjectNames.has(rootObjectName)) { From ce07db6948f0f560fb9e6ba70a47d6b9bd6707ac Mon Sep 17 00:00:00 2001 From: waynzh Date: Fri, 5 Sep 2025 16:35:35 +0800 Subject: [PATCH 4/7] fix lint --- lib/rules/no-async-in-computed-properties.js | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 01300eb30..4cb2448a7 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -47,21 +47,28 @@ function getRootObjectName(memberExpr) { let current = memberExpr.object while (current) { - if (current.type === 'MemberExpression') { - current = utils.skipChainExpression(current.object) - } else if (current.type === 'CallExpression') { - const calleeExpr = utils.skipChainExpression(current.callee) - if (calleeExpr.type === 'MemberExpression') { - current = calleeExpr.object - } else if (calleeExpr.type === 'Identifier') { - return calleeExpr.name - } else { + switch (current.type) { + case 'MemberExpression': { + current = utils.skipChainExpression(current.object) break } - } else if (current.type === 'Identifier') { - return current.name - } else { - 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 + } } } From 276be2754d1d0114b26a05e17a7712f677e74f40 Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Fri, 5 Sep 2025 17:26:43 +0800 Subject: [PATCH 5/7] Update .changeset/cute-bears-sneeze.md Co-authored-by: Flo Edelmann --- .changeset/cute-bears-sneeze.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cute-bears-sneeze.md b/.changeset/cute-bears-sneeze.md index 4c1ad0a90..7bc9cb0bc 100644 --- a/.changeset/cute-bears-sneeze.md +++ b/.changeset/cute-bears-sneeze.md @@ -2,4 +2,4 @@ 'eslint-plugin-vue': minor --- -Added `ignoredObjectNames` option to vue/no-async-in-computed-properties +Added `ignoredObjectNames` option to `vue/no-async-in-computed-properties` From 86eec3c2a87fcf191d0e23cf56315af24d2c6a6c Mon Sep 17 00:00:00 2001 From: Wayne Zhang Date: Fri, 5 Sep 2025 17:26:55 +0800 Subject: [PATCH 6/7] Update docs/rules/no-async-in-computed-properties.md Co-authored-by: Flo Edelmann --- docs/rules/no-async-in-computed-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-async-in-computed-properties.md b/docs/rules/no-async-in-computed-properties.md index f6d7d043d..ff4f18d78 100644 --- a/docs/rules/no-async-in-computed-properties.md +++ b/docs/rules/no-async-in-computed-properties.md @@ -116,7 +116,7 @@ export default { } ``` -- `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`: 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"]` From d5805cf3401ad49565bf367ff17360ac6e6328bf Mon Sep 17 00:00:00 2001 From: waynzh Date: Fri, 5 Sep 2025 19:39:51 +0800 Subject: [PATCH 7/7] feat: enhance member expression handling and add TypeScript support --- lib/rules/no-async-in-computed-properties.js | 45 +++++++++++++------ .../rules/no-async-in-computed-properties.js | 25 ++++++++++- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 4cb2448a7..0cf8c639c 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -38,24 +38,35 @@ 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 = memberExpr.object + let current = skipWrapper(memberExpr.object) while (current) { switch (current.type) { case 'MemberExpression': { - current = utils.skipChainExpression(current.object) + current = skipWrapper(current.object) break } case 'CallExpression': { - const calleeExpr = utils.skipChainExpression(current.callee) + const calleeExpr = skipWrapper(current.callee) if (calleeExpr.type === 'MemberExpression') { - current = calleeExpr.object + current = skipWrapper(calleeExpr.object) } else if (calleeExpr.type === 'Identifier') { return calleeExpr.name } else { @@ -75,6 +86,22 @@ function getRootObjectName(memberExpr) { 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 @@ -83,15 +110,7 @@ function isPromise(node, ignoredObjectNames) { const callee = utils.skipChainExpression(node.callee) if (callee.type === 'MemberExpression') { const name = utils.getStaticPropertyName(callee) - if ( - !name || - (!PROMISE_FUNCTIONS.has(name) && - !( - callee.object.type === 'Identifier' && - callee.object.name === 'Promise' && - PROMISE_METHODS.has(name) - )) - ) { + if (!name || !isPromiseMethod(name, callee)) { return false } diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js index c9d6ce87a..2c2e29c72 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -363,13 +363,34 @@ ruleTester.run('no-async-in-computed-properties', rule, { export default { computed: { foo: function () { - return z.a.b.c.d.e.f.method().catch(err => err).finally(() => {}) + return z.a?.['b'].[c].d.method().catch(err => err).finally(() => {}) } } } `, options: [{ ignoredObjectNames: ['z'] }], - languageOptions + 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') + } + } } ],

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