/** * @fileoverview Check if there are no asynchronous actions inside computed properties. * @author Armano */ 'use strict' const { ReferenceTracker } = require('@eslint-community/eslint-utils') const utils = require('../utils') /** * @typedef {import('../utils').VueObjectData} VueObjectData * @typedef {import('../utils').VueVisitor} VueVisitor * @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty */ const PROMISE_FUNCTIONS = new Set(['then', 'catch', 'finally']) const PROMISE_METHODS = new Set(['all', 'race', 'reject', 'resolve']) const TIMED_FUNCTIONS = new Set([ 'setTimeout', 'setInterval', 'setImmediate', 'requestAnimationFrame' ]) /** * @param {CallExpression} node */ function isTimedFunction(node) { const callee = utils.skipChainExpression(node.callee) return ( ((callee.type === 'Identifier' && TIMED_FUNCTIONS.has(callee.name)) || (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'window' && TIMED_FUNCTIONS.has(utils.getStaticPropertyName(callee) || ''))) && node.arguments.length > 0 ) } /** * @param {CallExpression} node */ function isPromise(node) { 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))) ) } return false } /** * @param {CallExpression} node * @param {RuleContext} context */ function isNextTick(node, context) { const callee = utils.skipChainExpression(node.callee) if (callee.type === 'MemberExpression') { const name = utils.getStaticPropertyName(callee) return ( (utils.isThis(callee.object, context) && name === '$nextTick') || (callee.object.type === 'Identifier' && callee.object.name === 'Vue' && name === 'nextTick') ) } return false } module.exports = { meta: { type: 'problem', docs: { description: 'disallow asynchronous actions in computed properties', categories: ['vue3-essential', 'vue2-essential'], url: 'https://eslint.vuejs.org/rules/no-async-in-computed-properties.html' }, fixable: null, schema: [], messages: { unexpectedInFunction: 'Unexpected {{expressionName}} in computed function.', unexpectedInProperty: 'Unexpected {{expressionName}} in "{{propertyName}}" computed property.' } }, /** @param {RuleContext} context */ create(context) { /** @type {Map} */ const computedPropertiesMap = new Map() /** @type {(FunctionExpression | ArrowFunctionExpression)[]} */ const computedFunctionNodes = [] /** * @typedef {object} ScopeStack * @property {ScopeStack | null} upper * @property {BlockStatement | Expression} body */ /** @type {ScopeStack | null} */ let scopeStack = null const expressionTypes = { promise: 'asynchronous action', nextTick: 'asynchronous action', await: 'await operator', async: 'async function declaration', new: 'Promise object', timed: 'timed function' } /** * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node * @param {VueObjectData|undefined} [info] */ function onFunctionEnter(node, info) { if (node.async) { verify( node, node.body, 'async', info ? computedPropertiesMap.get(info.node) : null ) } scopeStack = { upper: scopeStack, body: node.body } } function onFunctionExit() { scopeStack = scopeStack && scopeStack.upper } /** * @param {ESNode} node * @param {BlockStatement | Expression} targetBody * @param {keyof expressionTypes} type * @param {ComponentComputedProperty[]|undefined|null} computedProperties */ function verify(node, targetBody, type, computedProperties) { for (const cp of computedProperties || []) { if ( cp.value && node.loc.start.line >= cp.value.loc.start.line && node.loc.end.line <= cp.value.loc.end.line && targetBody === cp.value ) { context.report({ node, messageId: 'unexpectedInProperty', data: { expressionName: expressionTypes[type], propertyName: cp.key || 'unknown' } }) return } } for (const cf of computedFunctionNodes) { if ( node.loc.start.line >= cf.body.loc.start.line && node.loc.end.line <= cf.body.loc.end.line && targetBody === cf.body ) { context.report({ node, messageId: 'unexpectedInFunction', data: { expressionName: expressionTypes[type] } }) return } } } const nodeVisitor = { ':function': onFunctionEnter, ':function:exit': onFunctionExit, /** * @param {NewExpression} node * @param {VueObjectData|undefined} [info] */ NewExpression(node, info) { if (!scopeStack) { return } if ( node.callee.type === 'Identifier' && node.callee.name === 'Promise' ) { verify( node, scopeStack.body, 'new', info ? computedPropertiesMap.get(info.node) : null ) } }, /** * @param {CallExpression} node * @param {VueObjectData|undefined} [info] */ CallExpression(node, info) { if (!scopeStack) { return } if (isPromise(node)) { verify( node, scopeStack.body, 'promise', info ? computedPropertiesMap.get(info.node) : null ) } else if (isTimedFunction(node)) { verify( node, scopeStack.body, 'timed', info ? computedPropertiesMap.get(info.node) : null ) } else if (isNextTick(node, context)) { verify( node, scopeStack.body, 'nextTick', info ? computedPropertiesMap.get(info.node) : null ) } }, /** * @param {AwaitExpression} node * @param {VueObjectData|undefined} [info] */ AwaitExpression(node, info) { if (!scopeStack) { return } verify( node, scopeStack.body, 'await', info ? computedPropertiesMap.get(info.node) : null ) } } return utils.compositingVisitors( { /** @param {Program} program */ Program(program) { const tracker = new ReferenceTracker(utils.getScope(context, program)) for (const { node } of utils.iterateReferencesTraceMap(tracker, { computed: { [ReferenceTracker.CALL]: true } })) { if (node.type !== 'CallExpression') { continue } const getter = utils.getGetterBodyFromComputedFunction(node) if (getter) { computedFunctionNodes.push(getter) } } } }, utils.isScriptSetup(context) ? utils.defineScriptSetupVisitor(context, nodeVisitor) : utils.defineVueVisitor(context, { onVueObjectEnter(node) { computedPropertiesMap.set(node, utils.getComputedProperties(node)) }, ...nodeVisitor }) ) } }