eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
116 lines (99 loc) • 3.38 kB
JavaScript
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;