UNPKG

eslint-plugin-unicorn

Version:
116 lines (99 loc) 3.38 kB
import {findVariable, getPropertyName} from '@eslint-community/eslint-utils'; import {isFunction} from './ast/index.js'; import {getConstVariableInitializer, isPromiseType} from './utils/index.js'; import {isIteratorExpression, unwrapExpression} from './shared/iterator-helpers.js'; const MESSAGE_ID = 'no-async-iterator-callback'; const messages = { [MESSAGE_ID]: 'Do not pass an asynchronous callback to `Iterator#{{method}}()`; returned promises are not awaited.', }; const methods = new Set(['filter', 'forEach', 'some', 'every', 'find', 'flatMap']); function getDirectConstInitializer(node, context) { const initializer = getConstVariableInitializer(node, context); return initializer?.parent.id.type === 'Identifier' ? unwrapExpression(initializer) : undefined; } const isAsyncNonGeneratorFunction = node => isFunction(node) && node.async && !node.generator; function isAsyncCallback(node, context) { node = unwrapExpression(node); if (isAsyncNonGeneratorFunction(node)) { return true; } const initializer = getDirectConstInitializer(node, context); if (initializer && isAsyncNonGeneratorFunction(initializer)) { return true; } if (node.type !== 'Identifier') { return false; } const variable = findVariable(context.sourceCode.getScope(node), node); if (!variable || variable.references.some(reference => reference.isWrite())) { return false; } const functionImplementations = variable.defs.filter(definition => definition.type === 'FunctionName' && definition.node.body, ); return functionImplementations.length === 1 && isAsyncNonGeneratorFunction(functionImplementations[0].node); } function hasPromiseReturnType(node, context) { const {parserServices} = context.sourceCode; if (!parserServices?.program) { return false; } try { const checker = parserServices.program.getTypeChecker(); const type = parserServices.getTypeAtLocation(node); return type.getCallSignatures().some(signature => { const signatureReturnType = checker.getReturnTypeOfSignature(signature); const returnType = checker.getBaseConstraintOfType(signatureReturnType) ?? signatureReturnType; const types = returnType.isUnion() ? returnType.types : [returnType]; return types.some(type => isPromiseType(type, checker) === true); }); } catch { // Type information can be unavailable in incomplete projects; retain syntax-based detection. return false; } } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { context.on('CallExpression', node => { const {callee} = node; const [callback] = node.arguments; if (callee.type !== 'MemberExpression' || !callback || callback.type === 'SpreadElement') { return; } const method = getPropertyName(callee, context.sourceCode.getScope(node)); if ( !methods.has(method) || !isIteratorExpression(callee.object, context) || !(isAsyncCallback(callback, context) || hasPromiseReturnType(callback, context)) ) { return; } return { node: callback, messageId: MESSAGE_ID, data: {method}, }; }); }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'problem', docs: { description: 'Disallow asynchronous callbacks in synchronous iterator helpers.', recommended: 'unopinionated', }, schema: [], messages, languages: [ 'js/js', ], }, }; export default config;