eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
100 lines (85 loc) • 3.01 kB
JavaScript
import {findVariable, getPropertyName} from '@eslint-community/eslint-utils';
import {isFunction} from './ast/index.js';
import {getConstVariableInitializer, isPromiseType, withTypeInformation} 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']);
const isAsyncNonGeneratorFunction = node => isFunction(node) && node.async && !node.generator;
function isAsyncCallback(node, context) {
node = unwrapExpression(node);
if (isAsyncNonGeneratorFunction(node)) {
return true;
}
const initializer = getConstVariableInitializer(node, context);
if (initializer && isAsyncNonGeneratorFunction(unwrapExpression(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);
}
// Type information can be unavailable in incomplete projects; retain syntax-based detection.
function hasPromiseReturnType(node, context) {
return withTypeInformation(node, context, ({type, checker}) => 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);
})) ?? 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;