UNPKG

eslint-plugin-mocha

Version:

Eslint rules for mocha.

288 lines 11.8 kB
import { createMochaVisitors } from "../ast/mocha-visitors.js"; import { isBlockStatement, isFunction, isProgram } from "../ast/node-types.js"; import { getLastOrThrow } from "../list.js"; import { getRuleOption } from "../rule-options.js"; import { getTopLevelMochaExpression as getDirectTopLevelMochaExpression, isDirectStatementInScope } from "./direct-mocha-statement.js"; const optionSchema = { type: 'object', properties: { hookOrder: { type: 'string', enum: ['setup-teardown', 'off'] }, order: { type: 'string', enum: ['hooks-tests-suites', 'off'] }, disallowDuplicateHooks: { type: 'boolean' }, disallowMixedTestsAndSuites: { type: 'boolean' } }, additionalProperties: false }; const defaultOption = { disallowDuplicateHooks: false, hookOrder: 'off', order: 'off', disallowMixedTestsAndSuites: false }; const entityRank = { hook: 0, testCase: 1, suite: 2 }; const hookOrderRank = { 'before()': 0, 'suiteSetup()': 0, 'beforeEach()': 1, 'setup()': 1, 'afterEach()': 2, 'teardown()': 2, 'after()': 3, 'suiteTeardown()': 3 }; function createStructureLayer(scopeNode) { return { hasReportedMixedStructure: false, hasSeenSuite: false, hasSeenTestCase: false, highestSeenHookOrderName: null, highestSeenHookOrderRank: null, highestSeenKind: null, scopeNode, usedHookNames: new Set() }; } function reportUnexpectedOrder(context, visitorContext, highestSeenKind, currentKind) { if (currentKind === 'hook') { context.report({ node: visitorContext.node, messageId: highestSeenKind === 'suite' ? 'unexpectedHookAfterSuite' : 'unexpectedHookAfterTest' }); return; } context.report({ node: visitorContext.node, messageId: 'unexpectedTestAfterSuite' }); } function getHighestSeenKind(highestSeenKind, currentKind) { if (highestSeenKind === null || entityRank[currentKind] > entityRank[highestSeenKind]) { return currentKind; } return highestSeenKind; } function hasUnexpectedOrder(configuredOrder, highestSeenKind, currentKind) { return configuredOrder === 'hooks-tests-suites' && highestSeenKind !== null && entityRank[currentKind] < entityRank[highestSeenKind]; } function shouldReportMixedStructure(layer, currentKind) { return !layer.hasReportedMixedStructure && (currentKind === 'suite' && layer.hasSeenTestCase || currentKind === 'testCase' && layer.hasSeenSuite); } function shouldReportDuplicateHook(layer, currentKind, visitorContext) { return currentKind === 'hook' && layer.usedHookNames.has(visitorContext.name); } function getTerminalName(name) { return name.slice(name.lastIndexOf('.') + 1); } function getOrderedHook(name) { const rank = hookOrderRank[getTerminalName(name)]; if (rank === undefined) { return null; } return { name, rank }; } function reportUnexpectedHookOrder(context, visitorContext, previousHook) { context.report({ node: visitorContext.node, messageId: 'unexpectedHookOrder', data: { currentHook: visitorContext.name, previousHook } }); } function getTrackedOrderedHook(currentKind, visitorContext) { if (currentKind !== 'hook') { return null; } return getOrderedHook(visitorContext.name); } function shouldReportHookOrder(layer, hookOrder, orderedHook) { return hookOrder === 'setup-teardown' && orderedHook !== null && layer.highestSeenHookOrderRank !== null && orderedHook.rank < layer.highestSeenHookOrderRank; } function isSuiteBodyLayer(layer) { return !isProgram(layer.scopeNode); } function shouldRememberHighestSeenHookOrder(layer, orderedHook) { return orderedHook !== null && (layer.highestSeenHookOrderRank === null || orderedHook.rank > layer.highestSeenHookOrderRank); } function getUsedHookNames(layer, currentKind, visitorContext) { const usedHookNames = new Set(layer.usedHookNames); if (currentKind === 'hook') { usedHookNames.add(visitorContext.name); } return usedHookNames; } function getTrackedOrderedHookForLayer(trackedStructureContext, hookOrder) { return hookOrder === 'setup-teardown' ? getTrackedOrderedHook(trackedStructureContext.currentKind, trackedStructureContext.visitorContext) : null; } function createTrackedLayer(layer, trackedStructureContext, hasReportedMixedStructure, hookOrder) { const { currentKind, visitorContext } = trackedStructureContext; const trackedOrderedHook = getTrackedOrderedHookForLayer(trackedStructureContext, hookOrder); const remembersHighestSeenHookOrder = shouldRememberHighestSeenHookOrder(layer, trackedOrderedHook); return { hasReportedMixedStructure: layer.hasReportedMixedStructure || hasReportedMixedStructure, hasSeenSuite: layer.hasSeenSuite || currentKind === 'suite', hasSeenTestCase: layer.hasSeenTestCase || currentKind === 'testCase', highestSeenHookOrderName: remembersHighestSeenHookOrder ? trackedOrderedHook.name : layer.highestSeenHookOrderName, highestSeenHookOrderRank: remembersHighestSeenHookOrder ? trackedOrderedHook.rank : layer.highestSeenHookOrderRank, highestSeenKind: getHighestSeenKind(layer.highestSeenKind, currentKind), scopeNode: layer.scopeNode, usedHookNames: getUsedHookNames(layer, currentKind, visitorContext) }; } function replaceCurrentLayer(layers, nextLayer) { return [...layers.slice(0, -1), nextLayer]; } function getDirectStructureContext(layers, visitorContext, currentKind) { const currentLayer = getLastOrThrow(layers); if (!isDirectStatementInScope(currentLayer.scopeNode, getDirectTopLevelMochaExpression(visitorContext.node))) { return null; } return { currentKind, currentLayer }; } function reportMixedStructure(context, visitorContext) { context.report({ node: visitorContext.node, messageId: 'unexpectedMixedTestsAndSuites' }); } function reportDuplicateHook(context, visitorContext) { context.report({ node: visitorContext.node, messageId: 'unexpectedDuplicateHook', data: { name: visitorContext.name } }); } function reportMixedStructureIfNeeded(context, trackedStructureContext, disallowMixedTestsAndSuites) { const { currentKind, currentLayer, visitorContext } = trackedStructureContext; const shouldReport = isSuiteBodyLayer(currentLayer) && disallowMixedTestsAndSuites && shouldReportMixedStructure(currentLayer, currentKind); if (shouldReport) { reportMixedStructure(context, visitorContext); } return shouldReport; } function reportDuplicateHookIfNeeded(context, trackedStructureContext, disallowDuplicateHooks) { const { currentKind, currentLayer, visitorContext } = trackedStructureContext; if (disallowDuplicateHooks && shouldReportDuplicateHook(currentLayer, currentKind, visitorContext)) { reportDuplicateHook(context, visitorContext); } } function reportUnexpectedHookOrderIfNeeded(context, trackedStructureContext, hookOrder) { const { currentKind, currentLayer, visitorContext } = trackedStructureContext; if (hookOrder === 'off' || currentLayer.highestSeenHookOrderName === null || currentLayer.highestSeenHookOrderRank === null) { return; } const orderedHook = getTrackedOrderedHook(currentKind, visitorContext); if (shouldReportHookOrder(currentLayer, hookOrder, orderedHook)) { reportUnexpectedHookOrder(context, visitorContext, currentLayer.highestSeenHookOrderName); } } function trackStructureLayer(context, layers, trackedStructureContext, options) { const { currentLayer } = trackedStructureContext; const { disallowDuplicateHooks, disallowMixedTestsAndSuites, hookOrder } = options; const reportsMixedStructure = reportMixedStructureIfNeeded(context, trackedStructureContext, disallowMixedTestsAndSuites); reportDuplicateHookIfNeeded(context, trackedStructureContext, disallowDuplicateHooks); reportUnexpectedHookOrderIfNeeded(context, trackedStructureContext, hookOrder); return replaceCurrentLayer(layers, createTrackedLayer(currentLayer, trackedStructureContext, reportsMixedStructure, hookOrder)); } export const consistentStructureRule = { meta: { type: 'suggestion', docs: { description: 'Require consistent structure for Mocha test entities', recommended: true, url: 'https://github.com/lo1tuma/eslint-plugin-mocha/blob/main/documentation/rules/consistent-structure.md' }, schema: [optionSchema], defaultOptions: [defaultOption], messages: { unexpectedHookAfterSuite: 'Unexpected hook after a child suite.', unexpectedHookAfterTest: 'Unexpected hook after a test case.', unexpectedDuplicateHook: 'Unexpected use of duplicate Mocha `{{name}}` hook', unexpectedHookOrder: 'Unexpected Mocha `{{currentHook}}` hook after `{{previousHook}}` hook.', unexpectedMixedTestsAndSuites: 'Unexpected mix of test cases and child suites at the same level.', unexpectedTestAfterSuite: 'Unexpected test case after a child suite.' }, languages: ['js/js'] }, create(context) { const { order, disallowDuplicateHooks, disallowMixedTestsAndSuites, hookOrder } = getRuleOption(context); let layers = []; function registerStructure(visitorContext, currentKind) { const directStructureContext = getDirectStructureContext(layers, visitorContext, currentKind); if (directStructureContext === null) { return; } const { currentKind: directKind, currentLayer } = directStructureContext; const { highestSeenKind } = currentLayer; if (isSuiteBodyLayer(currentLayer) && hasUnexpectedOrder(order, highestSeenKind, directKind)) { reportUnexpectedOrder(context, visitorContext, highestSeenKind, directKind); } layers = trackStructureLayer(context, layers, { ...directStructureContext, visitorContext }, { disallowDuplicateHooks, disallowMixedTestsAndSuites, hookOrder }); } return createMochaVisitors(context, { Program(node) { layers = [...layers, createStructureLayer(node)]; }, 'Program:exit'() { layers = layers.slice(0, -1); }, suite(visitorContext) { registerStructure(visitorContext, 'suite'); }, suiteCallback(visitorContext) { const { node } = visitorContext; if (isFunction(node) && isBlockStatement(node.body)) { layers = [...layers, createStructureLayer(node.body)]; } }, 'suiteCallback:exit'(visitorContext) { const { node } = visitorContext; if (isFunction(node) && isBlockStatement(node.body)) { layers = layers.slice(0, -1); } }, testCase(visitorContext) { registerStructure(visitorContext, 'testCase'); }, hook(visitorContext) { registerStructure(visitorContext, 'hook'); } }); } }; //# sourceMappingURL=consistent-structure.js.map