UNPKG

eslint-plugin-mocha

Version:

Eslint rules for mocha.

281 lines 11.6 kB
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