eslint-plugin-mocha
Version:
Eslint rules for mocha.
288 lines • 11.8 kB
JavaScript
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