eslint-plugin-mocha
Version:
Eslint rules for mocha.
281 lines • 11.6 kB
JavaScript
import { extractMemberExpressionPath, isConstantPath } from "../ast/member-expression.js";
import { createMochaVisitors } from "../ast/mocha-visitors.js";
import { isFunction } from "../ast/node-types.js";
import { asRuleNode } from "../ast/rule-node.js";
import { visitWithoutNestedFunctions } from "../ast/visit-child-nodes.js";
import { getFirstMeaningfulParameter, hasCallbackParameter } from "../mocha/callback-parameter.js";
import { stripCallExpressions } from "../mocha/name-details.js";
import { isRecord } from "../record.js";
import { getRuleOption } from "../rule-options.js";
const errorCallbackNames = new Set(['err', 'error']);
const promiseMethodNames = new Set(['then', 'catch', 'finally']);
const knownScheduledAsyncMethods = new Set([
'setImmediate',
'setInterval',
'setTimeout',
'queueMicrotask',
'process.nextTick',
'global.setImmediate',
'global.setInterval',
'global.setTimeout',
'global.queueMicrotask',
'globalThis.setImmediate',
'globalThis.setInterval',
'globalThis.setTimeout',
'globalThis.queueMicrotask',
'window.setInterval',
'window.setTimeout',
'window.queueMicrotask'
]);
const optionSchema = {
type: 'object',
properties: {
allowedAsyncMethods: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
};
const defaultOption = { allowedAsyncMethods: [] };
const failedTypeLookup = Symbol('failed type lookup');
function collectExpressions(sourceCode, body, selectExpression) {
if (body.type !== 'BlockStatement') {
return [body];
}
const expressions = new Set();
for (const statement of body.body) {
visitWithoutNestedFunctions(sourceCode, statement, function (node) {
const expression = selectExpression(node);
if (expression !== undefined) {
expressions.add(expression);
}
});
}
return Array.from(expressions);
}
function getReturnedExpression(node) {
if (node.type !== 'ReturnStatement') {
return undefined;
}
return node.argument ?? undefined;
}
function collectCandidateExpressions(sourceCode, body) {
return collectExpressions(sourceCode, body, function (node) {
if (node.type === 'ExpressionStatement') {
return node.expression;
}
return getReturnedExpression(node);
});
}
function collectReturnedExpressions(sourceCode, body) {
return collectExpressions(sourceCode, body, getReturnedExpression);
}
function isGetTypeAtLocation(value) {
return typeof value === 'function';
}
function isTypeCheckerFactory(value) {
return typeof value === 'function';
}
function isPromisedTypeReader(value) {
return typeof value === 'function';
}
function getParserServicesRecord(sourceCode) {
const parserServices = sourceCode.parserServices;
return isRecord(parserServices) ? parserServices : undefined;
}
function getPromisedTypeReader(services) {
const program = isRecord(services.program) ? services.program : undefined;
if (!isTypeCheckerFactory(program?.getTypeChecker)) {
return undefined;
}
const typeChecker = program.getTypeChecker();
if (!isRecord(typeChecker) || !isPromisedTypeReader(typeChecker.getPromisedTypeOfPromise)) {
return undefined;
}
return typeChecker.getPromisedTypeOfPromise;
}
function getParserServicesWithTypeInformation(sourceCode) {
const services = getParserServicesRecord(sourceCode);
if (services === undefined || !isGetTypeAtLocation(services.getTypeAtLocation)) {
return undefined;
}
const promisedTypeReader = getPromisedTypeReader(services);
if (promisedTypeReader === undefined) {
return undefined;
}
return {
getTypeAtLocation: services.getTypeAtLocation,
getPromisedTypeOfPromise: promisedTypeReader
};
}
function getExpressionType(parserServices, expression) {
try {
return parserServices.getTypeAtLocation(asRuleNode(expression));
}
catch {
return failedTypeLookup;
}
}
function hasPromiseLikeType(parserServices, expression) {
const type = getExpressionType(parserServices, expression);
return type !== failedTypeLookup && parserServices.getPromisedTypeOfPromise(type) !== undefined;
}
function unwrapChainExpression(node) {
return node.type === 'ChainExpression' ? node.expression : node;
}
function getNamedPropertyName(node) {
return !node.computed && node.property.type === 'Identifier' ? node.property.name : undefined;
}
function getStaticPropertyName(node) {
if (node.type !== 'MemberExpression') {
return undefined;
}
if (node.property.type === 'Literal') {
return String(node.property.value);
}
return getNamedPropertyName(node);
}
function isPromiseMethodCall(node) {
const callee = unwrapChainExpression(node.callee);
return promiseMethodNames.has(getStaticPropertyName(callee) ?? '');
}
function collectCallExpressions(sourceCode, expression) {
const callExpressions = [];
visitWithoutNestedFunctions(sourceCode, expression, function (node) {
if (node.type === 'CallExpression') {
callExpressions.push(node);
}
});
return callExpressions;
}
function findPromiseMethodCall(sourceCode, expression) {
return collectCallExpressions(sourceCode, expression).find(isPromiseMethodCall);
}
function hasErrorFirstCallback(node) {
const firstParam = getFirstMeaningfulParameter(node);
const errorCallbackName = firstParam?.type === 'Identifier' ? firstParam.name : '';
return errorCallbackNames.has(errorCallbackName);
}
function hasInlineErrorFirstCallback(node) {
return node.arguments.some(function (argument) {
return isFunction(argument) && hasErrorFirstCallback(argument);
});
}
function findCallbackBasedAsyncCall(sourceCode, expression) {
return collectCallExpressions(sourceCode, expression).find(hasInlineErrorFirstCallback);
}
function normalizeCalledMethodPath(nodePath) {
return stripCallExpressions(nodePath).join('.');
}
function isScheduledAsyncCall(sourceCode, node, allowedAsyncMethods) {
const path = extractMemberExpressionPath(sourceCode, asRuleNode(node.callee));
if (!isConstantPath(path)) {
return false;
}
const calledMethod = normalizeCalledMethodPath(path);
return knownScheduledAsyncMethods.has(calledMethod) && !allowedAsyncMethods.has(calledMethod);
}
function findScheduledAsyncCall(sourceCode, expression, allowedAsyncMethods) {
return collectCallExpressions(sourceCode, expression).find(function (callExpression) {
return isScheduledAsyncCall(sourceCode, callExpression, allowedAsyncMethods);
});
}
function returnsPromiseToMocha(sourceCode, parserServices, node) {
return collectReturnedExpressions(sourceCode, node.body).some(function (expression) {
return parserServices === undefined
? findPromiseMethodCall(sourceCode, expression) !== undefined
: hasPromiseLikeType(parserServices, expression);
});
}
function isSynchronousMochaCallback(sourceCode, parserServices, node) {
return node.async !== true &&
!hasCallbackParameter(node) &&
!returnsPromiseToMocha(sourceCode, parserServices, node);
}
function createUnexpectedAsyncOperation(node, messageId) {
return { node, messageId };
}
function findUnexpectedPromiseAsyncOperation(sourceCode, parserServices, expression) {
if (parserServices !== undefined && hasPromiseLikeType(parserServices, expression)) {
return createUnexpectedAsyncOperation(expression, 'unexpectedPromiseAsyncOperation');
}
const promiseMethodCall = parserServices === undefined
? findPromiseMethodCall(sourceCode, expression)
: undefined;
return promiseMethodCall === undefined
? undefined
: createUnexpectedAsyncOperation(promiseMethodCall, 'unexpectedPromiseAsyncOperation');
}
function findUnexpectedScheduledAsyncOperation(sourceCode, expression, allowedAsyncMethods) {
const scheduledAsyncCall = findScheduledAsyncCall(sourceCode, expression, allowedAsyncMethods);
return scheduledAsyncCall === undefined
? undefined
: createUnexpectedAsyncOperation(scheduledAsyncCall, 'unexpectedScheduledAsyncOperation');
}
function findUnexpectedAsyncOperation(sourceCode, parserServices, expression, allowedAsyncMethods) {
const unexpectedPromiseAsyncOperation = findUnexpectedPromiseAsyncOperation(sourceCode, parserServices, expression);
if (unexpectedPromiseAsyncOperation !== undefined) {
return unexpectedPromiseAsyncOperation;
}
const unexpectedScheduledAsyncOperation = findUnexpectedScheduledAsyncOperation(sourceCode, expression, allowedAsyncMethods);
if (unexpectedScheduledAsyncOperation !== undefined) {
return unexpectedScheduledAsyncOperation;
}
const callbackBasedAsyncCall = findCallbackBasedAsyncCall(sourceCode, expression);
return callbackBasedAsyncCall === undefined
? undefined
: createUnexpectedAsyncOperation(callbackBasedAsyncCall, 'unexpectedCallbackAsyncOperation');
}
function reportUnexpectedAsyncOperation(context, unexpectedAsyncOperation) {
context.report({
node: asRuleNode(unexpectedAsyncOperation.node),
messageId: unexpectedAsyncOperation.messageId
});
}
export const noAsyncInSyncTestsRule = {
meta: {
type: 'problem',
docs: {
description: 'Disallow async operations in synchronous tests or hooks',
recommended: true,
url: 'https://github.com/lo1tuma/eslint-plugin-mocha/blob/main/documentation/rules/no-async-in-sync-tests.md'
},
schema: [optionSchema],
defaultOptions: [defaultOption],
messages: {
unexpectedCallbackAsyncOperation: 'Unexpected callback-based async operation in a synchronous test or hook.',
unexpectedPromiseAsyncOperation: 'Unexpected promise-based async operation in a synchronous test or hook.',
unexpectedScheduledAsyncOperation: 'Unexpected scheduled async operation in a synchronous test or hook.'
},
languages: ['js/js']
},
create(context) {
const { sourceCode } = context;
const { allowedAsyncMethods } = getRuleOption(context);
const allowedAsyncMethodSet = new Set(allowedAsyncMethods);
const parserServices = getParserServicesWithTypeInformation(sourceCode);
function reportUnexpectedAsyncOperations(node) {
if (!isFunction(node) || !isSynchronousMochaCallback(sourceCode, parserServices, node)) {
return;
}
for (const expression of collectCandidateExpressions(sourceCode, node.body)) {
const unexpectedAsyncOperation = findUnexpectedAsyncOperation(sourceCode, parserServices, expression, allowedAsyncMethodSet);
if (unexpectedAsyncOperation !== undefined) {
reportUnexpectedAsyncOperation(context, unexpectedAsyncOperation);
}
}
}
return createMochaVisitors(context, {
anyTestEntityCallback(visitorContext) {
if (visitorContext.type !== 'testCase' && visitorContext.type !== 'hook') {
return;
}
reportUnexpectedAsyncOperations(visitorContext.node);
}
});
}
};
//# sourceMappingURL=no-async-in-sync-tests.js.map