diff --git a/docs/rules/no-async-in-computed-properties.md b/docs/rules/no-async-in-computed-properties.md index 93e0ef045..549dc2013 100644 --- a/docs/rules/no-async-in-computed-properties.md +++ b/docs/rules/no-async-in-computed-properties.md @@ -2,21 +2,21 @@ pageClass: rule-details sidebarDepth: 0 title: vue/no-async-in-computed-properties -description: disallow asynchronous actions in computed properties +description: disallow asynchronous actions in computed properties and functions since: v3.8.0 --- # vue/no-async-in-computed-properties -> disallow asynchronous actions in computed properties +> disallow asynchronous actions in computed properties and functions - :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`. -Computed properties should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them. +Computed properties and functions should be synchronous. Asynchronous actions inside them may not work as expected and can lead to an unexpected behaviour, that's why you should avoid them. If you need async computed properties you might want to consider using additional plugin [vue-async-computed] ## :book: Rule Details -This rule is aimed at preventing asynchronous methods from being called in computed properties. +This rule is aimed at preventing asynchronous methods from being called in computed properties and functions. @@ -62,6 +62,47 @@ export default { + + +```vue + +``` + + + ## :wrench: Options Nothing. diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 9812d61c0..6d853f52a 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -3,7 +3,7 @@ * @author Armano */ 'use strict' - +const { ReferenceTracker } = require('eslint-utils') const utils = require('../utils') /** @@ -77,13 +77,16 @@ module.exports = { }, /** @param {RuleContext} context */ create(context) { + /** @type {Map} */ + const computedPropertiesMap = new Map() + /** @type {Array} */ + const computedFunctionNodes = [] + /** * @typedef {object} ScopeStack * @property {ScopeStack | null} upper * @property {BlockStatement | Expression} body */ - /** @type {Map} */ - const computedPropertiesMap = new Map() /** @type {ScopeStack | null} */ let scopeStack = null @@ -139,63 +142,103 @@ module.exports = { }) } }) - } - return utils.defineVueVisitor(context, { - onVueObjectEnter(node) { - computedPropertiesMap.set(node, utils.getComputedProperties(node)) - }, - ':function': onFunctionEnter, - ':function:exit': onFunctionExit, - NewExpression(node, { node: vueNode }) { - if (!scopeStack) { - return - } + computedFunctionNodes.forEach((c) => { if ( - node.callee.type === 'Identifier' && - node.callee.name === 'Promise' + node.loc.start.line >= c.loc.start.line && + node.loc.end.line <= c.loc.end.line && + targetBody === c.body ) { - verify( + context.report({ node, - scopeStack.body, - 'new', - computedPropertiesMap.get(vueNode) - ) + message: 'Unexpected {{expressionName}} in computed function.', + data: { + expressionName: expressionTypes[type] + } + }) } - }, + }) + } + return Object.assign( + { + Program() { + const tracker = new ReferenceTracker(context.getScope()) + const traceMap = utils.createCompositionApiTraceMap({ + [ReferenceTracker.ESM]: true, + computed: { + [ReferenceTracker.CALL]: true + } + }) + + for (const { node } of tracker.iterateEsmReferences(traceMap)) { + if (node.type !== 'CallExpression') { + continue + } - CallExpression(node, { node: vueNode }) { - if (!scopeStack) { - return + const getter = utils.getGetterBodyFromComputedFunction(node) + if (getter) { + computedFunctionNodes.push(getter) + } + } } - if (isPromise(node)) { - verify( - node, - scopeStack.body, - 'promise', - computedPropertiesMap.get(vueNode) - ) - } else if (isTimedFunction(node)) { + }, + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + computedPropertiesMap.set(node, utils.getComputedProperties(node)) + }, + ':function': onFunctionEnter, + ':function:exit': onFunctionExit, + + NewExpression(node, { node: vueNode }) { + if (!scopeStack) { + return + } + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'Promise' + ) { + verify( + node, + scopeStack.body, + 'new', + computedPropertiesMap.get(vueNode) + ) + } + }, + + CallExpression(node, { node: vueNode }) { + if (!scopeStack) { + return + } + if (isPromise(node)) { + verify( + node, + scopeStack.body, + 'promise', + computedPropertiesMap.get(vueNode) + ) + } else if (isTimedFunction(node)) { + verify( + node, + scopeStack.body, + 'timed', + computedPropertiesMap.get(vueNode) + ) + } + }, + + AwaitExpression(node, { node: vueNode }) { + if (!scopeStack) { + return + } verify( node, scopeStack.body, - 'timed', + 'await', computedPropertiesMap.get(vueNode) ) } - }, - - AwaitExpression(node, { node: vueNode }) { - if (!scopeStack) { - return - } - verify( - node, - scopeStack.body, - 'await', - computedPropertiesMap.get(vueNode) - ) - } - }) + }) + ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index b6ed05900..6d38bb97a 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -860,6 +860,44 @@ module.exports = { }) }, + /** + * Get getter body from computed function + * @param {CallExpression} callExpression call of computed function + * @return {FunctionExpression | ArrowFunctionExpression | null} getter function + */ + getGetterBodyFromComputedFunction(callExpression) { + if (callExpression.arguments.length <= 0) { + return null + } + + const arg = callExpression.arguments[0] + + if ( + arg.type === 'FunctionExpression' || + arg.type === 'ArrowFunctionExpression' + ) { + return arg + } + + if (arg.type === 'ObjectExpression') { + const getProperty = arg.properties.find( + /** + * @param {ESNode} p + * @returns { p is (Property & { value: FunctionExpression | ArrowFunctionExpression }) } + */ + (p) => + p.type === 'Property' && + p.key.type === 'Identifier' && + p.key.name === 'get' && + (p.value.type === 'FunctionExpression' || + p.value.type === 'ArrowFunctionExpression') + ) + return getProperty ? getProperty.value : null + } + + return null + }, + isVueFile, /** diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js index 8ea81b5cb..2a15712cf 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -231,6 +231,81 @@ ruleTester.run('no-async-in-computed-properties', rule, { data2: Promise.resolve(), })`, parserOptions + }, + { + code: ` + import {computed} from 'vue' + export default { + setup() { + const test1 = computed(() => {}) + const test2 = computed(function () { + var bar = 0 + try { + bar = bar / 0 + } catch (e) { + return e + } finally { + return bar + } + }) + const test3 = computed({ + set() { + new Promise((resolve, reject) => {}) + } + }) + const test4 = computed(() => { + return { + async bar() { + const data = await baz(this.a) + return data + } + } + }) + const test5 = computed(() => { + const a = 'test' + return [ + async () => { + const baz = await bar(a) + return baz + }, + 'b', + {} + ] + }) + const test6 = computed(() => function () { + return async () => await bar() + }) + const test7 = computed(() => new Promise.resolve()) + const test8 = computed(() => { + return new Bar(async () => await baz()) + }) + const test9 = computed(() => { + return someFunc.doSomething({ + async bar() { + return await baz() + } + }) + }) + const test10 = computed(() => { + return this.bar + ? { + baz:() => Promise.resolve(1) + } + : {} + }) + const test11 = computed(() => { + return this.bar ? () => Promise.resolve(1) : null + }) + const test12 = computed(() => { + return this.bar ? async () => 1 : null + }) + const test13 = computed(() => { + bar() + }) + } + } + `, + parserOptions } ], @@ -639,6 +714,212 @@ ruleTester.run('no-async-in-computed-properties', rule, { 'Unexpected timed function in "foo" computed property.', 'Unexpected timed function in "foo" computed property.' ] + }, + { + filename: 'test.vue', + code: ` + import {computed} from 'vue' + export default { + setup() { + const test1 = computed(async () => { + return await someFunc() + }) + const test2 = computed(async () => await someFunc()) + const test3 = computed(async function () { + return await someFunc() + }) + } + } + `, + parserOptions, + errors: [ + { + message: + 'Unexpected async function declaration in computed function.', + line: 5 + }, + { + message: 'Unexpected await operator in computed function.', + line: 6 + }, + { + message: + 'Unexpected async function declaration in computed function.', + line: 8 + }, + { + message: 'Unexpected await operator in computed function.', + line: 8 + }, + { + message: + 'Unexpected async function declaration in computed function.', + line: 9 + }, + { + message: 'Unexpected await operator in computed function.', + line: 10 + } + ] + }, + { + filename: 'test.vue', + code: ` + import {computed} from 'vue' + export default { + setup() { + const test = computed(async () => { + return new Promise((resolve, reject) => {}) + }) + } + } + `, + parserOptions, + errors: [ + { + message: + 'Unexpected async function declaration in computed function.', + line: 5 + }, + { + message: 'Unexpected Promise object in computed function.', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + import {computed} from 'vue' + export default { + setup() { + const test1 = computed(() => { + return bar.then(response => {}) + }) + const test2 = computed(() => { + return Promise.all([]) + }) + } + } + `, + parserOptions, + errors: [ + { + message: 'Unexpected asynchronous action in computed function.', + line: 6 + }, + { + message: 'Unexpected asynchronous action in computed function.', + line: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + import {computed} from 'vue' + export default { + setup() { + const test1 = computed({ + get: () => { + return Promise.resolve([]) + } + }) + const test2 = computed({ + get() { + return Promise.resolve([]) + } + }) + } + } + `, + parserOptions, + errors: [ + { + message: 'Unexpected asynchronous action in computed function.', + line: 7 + }, + { + message: 'Unexpected asynchronous action in computed function.', + line: 12 + } + ] + }, + { + filename: 'test.vue', + code: ` + import {computed} from 'vue' + export default { + setup() { + const test = computed(() => { + setTimeout(() => { }, 0) + window.setTimeout(() => { }, 0) + setInterval(() => { }, 0) + window.setInterval(() => { }, 0) + setImmediate(() => { }) + window.setImmediate(() => { }) + requestAnimationFrame(() => {}) + window.requestAnimationFrame(() => {}) + }) + } + } + `, + parserOptions, + errors: [ + { + message: 'Unexpected timed function in computed function.', + line: 6 + }, + { + message: 'Unexpected timed function in computed function.', + line: 7 + }, + { + message: 'Unexpected timed function in computed function.', + line: 8 + }, + { + message: 'Unexpected timed function in computed function.', + line: 9 + }, + { + message: 'Unexpected timed function in computed function.', + line: 10 + }, + { + message: 'Unexpected timed function in computed function.', + line: 11 + }, + { + message: 'Unexpected timed function in computed function.', + line: 12 + }, + { + message: 'Unexpected timed function in computed function.', + line: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + import {computed} from 'vue' + export default { + setup() { + const test = computed(async () => { + bar() + }) + } + } + `, + parserOptions, + errors: [ + { + message: + 'Unexpected async function declaration in computed function.', + line: 5 + } + ] } ] })