From 8d1b11c9d8e75e03a39ce759d3871a6e0196ca17 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Wed, 30 Dec 2020 16:33:46 +0900 Subject: [PATCH 1/4] Add composition api's computed function support to vue/no-async-in-computed-properties refs #1393 --- docs/rules/no-async-in-computed-properties.md | 49 +++- lib/rules/no-async-in-computed-properties.js | 139 ++++++---- lib/utils/index.js | 38 +++ .../rules/no-async-in-computed-properties.js | 257 ++++++++++++++++++ 4 files changed, 431 insertions(+), 52 deletions(-) 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..d66f03dd5 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 {Set} */ + const computedFunctionNodes = new Set() + /** * @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.getComputedGetterBody(node) + if (getter) { + computedFunctionNodes.add(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..220cdefe2 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 + */ + getComputedGetterBody(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..a100157c8 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -231,6 +231,78 @@ 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 + }) + } + } + `, + parserOptions } ], @@ -639,6 +711,191 @@ 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 + } + ] } ] }) From a5587ab860f569a3b4b07d8562e4f9a89a0d3484 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sat, 2 Jan 2021 20:12:09 +0900 Subject: [PATCH 2/4] rename getComputedGetterBody to getGetterBodyFromComputedFunction --- lib/rules/no-async-in-computed-properties.js | 2 +- lib/utils/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index d66f03dd5..08c97c21e 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -175,7 +175,7 @@ module.exports = { continue } - const getter = utils.getComputedGetterBody(node) + const getter = utils.getGetterBodyFromComputedFunction(node) if (getter) { computedFunctionNodes.add(getter) } diff --git a/lib/utils/index.js b/lib/utils/index.js index 220cdefe2..6d38bb97a 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -865,7 +865,7 @@ module.exports = { * @param {CallExpression} callExpression call of computed function * @return {FunctionExpression | ArrowFunctionExpression | null} getter function */ - getComputedGetterBody(callExpression) { + getGetterBodyFromComputedFunction(callExpression) { if (callExpression.arguments.length <= 0) { return null } From 5c0ae31a18ca7980a38a8420d4cbf73401c427b8 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sat, 2 Jan 2021 20:15:43 +0900 Subject: [PATCH 3/4] add testcase without return --- .../rules/no-async-in-computed-properties.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js index a100157c8..2a15712cf 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -299,6 +299,9 @@ ruleTester.run('no-async-in-computed-properties', rule, { const test12 = computed(() => { return this.bar ? async () => 1 : null }) + const test13 = computed(() => { + bar() + }) } } `, @@ -896,6 +899,27 @@ ruleTester.run('no-async-in-computed-properties', rule, { 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 + } + ] } ] }) From fd88047ef3ffb5e308f42aa937cd5796e7dac323 Mon Sep 17 00:00:00 2001 From: sapphi-red Date: Sat, 2 Jan 2021 20:17:56 +0900 Subject: [PATCH 4/4] use array instead of set --- lib/rules/no-async-in-computed-properties.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js index 08c97c21e..6d853f52a 100644 --- a/lib/rules/no-async-in-computed-properties.js +++ b/lib/rules/no-async-in-computed-properties.js @@ -79,8 +79,8 @@ module.exports = { create(context) { /** @type {Map} */ const computedPropertiesMap = new Map() - /** @type {Set} */ - const computedFunctionNodes = new Set() + /** @type {Array} */ + const computedFunctionNodes = [] /** * @typedef {object} ScopeStack @@ -177,7 +177,7 @@ module.exports = { const getter = utils.getGetterBodyFromComputedFunction(node) if (getter) { - computedFunctionNodes.add(getter) + computedFunctionNodes.push(getter) } } }