eslint-plugin-mocha
Version:
Eslint rules for mocha.
202 lines • 7.5 kB
JavaScript
import { createMochaVisitors } from "../ast/mocha-visitors.js";
import { expectNodeLocation, expectNodeRange } from "../ast/node-location.js";
import { getLastOrThrow } from "../list.js";
import { getRuleOption } from "../rule-options.js";
import { getInterface } from "../settings.js";
const interfaces = ['BDD', 'TDD'];
const optionSchema = {
type: 'object',
properties: {
interface: {
type: 'string',
enum: interfaces
}
},
additionalProperties: false
};
const defaultOption = { interface: 'BDD' };
const interfaceMethodNames = new Set([
'describe',
'context',
'suite',
'it',
'specify',
'test',
'before',
'after',
'beforeEach',
'afterEach',
'suiteSetup',
'suiteTeardown',
'setup',
'teardown'
]);
function getFirstChildScope(scope) {
return scope?.childScopes[0];
}
function reportUnexpectedInterface(context, node, actualInterface, expectedInterface) {
context.report({
node,
messageId: 'unexpectedInterface',
data: {
actualInterface,
expectedInterface
}
});
}
function getMochaModuleScope(sourceCode) {
const { globalScope } = sourceCode.scopeManager;
const maybeModuleScope = getFirstChildScope(globalScope);
return maybeModuleScope?.type === 'module' ? maybeModuleScope : null;
}
function isMochaImportDefinition(importDef) {
return importDef?.type === 'ImportBinding' &&
importDef.parent.source.value === 'mocha' &&
importDef.node.type === 'ImportSpecifier';
}
function getMochaImportBinding(variable) {
const importDef = variable.defs[0];
if (!isMochaImportDefinition(importDef)) {
return null;
}
return {
importDeclaration: importDef.parent,
specifier: importDef.node
};
}
function getImportedName(specifier) {
return specifier.imported.type === 'Identifier'
? specifier.imported.name
: String(specifier.imported.value);
}
function isInterfaceMethodImport(specifier) {
return interfaceMethodNames.has(getImportedName(specifier));
}
function isCanonicalNamedImportSpecifier(specifier) {
return specifier.local.name === getImportedName(specifier);
}
function isImportSpecifier(specifier) {
return specifier.type === 'ImportSpecifier';
}
function isFixableImportSpecifier(specifier) {
return isCanonicalNamedImportSpecifier(specifier) && isInterfaceMethodImport(specifier);
}
function hasOnlyNamedImportSpecifiers(importDeclaration) {
return importDeclaration.specifiers.every(isImportSpecifier);
}
function removeFullImportDeclaration(fixer, sourceCode, importDeclaration) {
const range = expectNodeRange(importDeclaration);
const trailingSource = sourceCode.text.slice(range[1]);
const nextTokenOffset = trailingSource.search(/\S/u);
const nextRangeStart = range[1] + Math.max(nextTokenOffset, 0);
return fixer.removeRange([range[0], nextRangeStart]);
}
function shouldRemoveFullImportDeclaration(importDeclaration) {
return importDeclaration.specifiers.length > 0 &&
importDeclaration.specifiers.every(function (currentSpecifier) {
return isImportSpecifier(currentSpecifier) && isFixableImportSpecifier(currentSpecifier);
});
}
function getRangeBeforeNextSpecifier(specifier, nextSpecifier) {
const specifierRange = expectNodeRange(specifier);
const nextSpecifierRange = expectNodeRange(nextSpecifier);
return [specifierRange[0], nextSpecifierRange[0]];
}
function getRangeAfterPreviousSpecifier(previousSpecifier, specifier) {
const previousSpecifierRange = expectNodeRange(previousSpecifier);
const specifierRange = expectNodeRange(specifier);
return [previousSpecifierRange[1], specifierRange[1]];
}
function getImportSpecifierRemovalRange(specifier, specifiers) {
const index = specifiers.indexOf(specifier);
if (index === 0) {
return getRangeBeforeNextSpecifier(specifier, getLastOrThrow(specifiers.slice(1)));
}
return getRangeAfterPreviousSpecifier(getLastOrThrow(specifiers.slice(index - 1, index)), specifier);
}
function fixImportSpecifier(fixer, sourceCode, specifier, importDeclaration) {
if (shouldRemoveFullImportDeclaration(importDeclaration)) {
return removeFullImportDeclaration(fixer, sourceCode, importDeclaration);
}
return fixer.removeRange(getImportSpecifierRemovalRange(specifier, importDeclaration.specifiers));
}
function createUnexpectedImportDescriptor(binding, configuredMochaInterface) {
const { specifier } = binding;
const loc = expectNodeLocation(specifier.local);
return {
loc,
messageId: 'unexpectedInterface',
data: {
actualInterface: 'require',
expectedInterface: `global ${configuredMochaInterface}`
}
};
}
function reportUnexpectedImportBinding(context, binding, configuredMochaInterface) {
if (!isInterfaceMethodImport(binding.specifier)) {
return;
}
const descriptor = createUnexpectedImportDescriptor(binding, configuredMochaInterface);
const { importDeclaration, specifier } = binding;
if (hasOnlyNamedImportSpecifiers(importDeclaration) && isCanonicalNamedImportSpecifier(specifier)) {
context.report({
...descriptor,
fix(fixer) {
return fixImportSpecifier(fixer, context.sourceCode, specifier, importDeclaration);
}
});
return;
}
context.report(descriptor);
}
function reportUnexpectedImportBindingsInModule(context, configuredMochaInterface) {
const moduleScope = getMochaModuleScope(context.sourceCode);
if (moduleScope === null) {
return;
}
for (const variable of moduleScope.variables) {
const binding = getMochaImportBinding(variable);
if (binding !== null && variable.references.length > 0) {
reportUnexpectedImportBinding(context, binding, configuredMochaInterface);
}
}
}
export const consistentInterfaceRule = {
meta: {
type: 'problem',
docs: {
description: 'Enforces consistent use of mocha interfaces',
recommended: false,
url: 'https://github.com/lo1tuma/eslint-plugin-mocha/blob/main/documentation/rules/consistent-interface.md'
},
fixable: 'code',
schema: [optionSchema],
defaultOptions: [defaultOption],
messages: {
unexpectedInterface: 'Unexpected use of {{actualInterface}} interface instead of {{expectedInterface}}'
},
languages: ['js/js']
},
create(context) {
const { interface: interfaceToUse } = getRuleOption(context);
const configuredMochaInterface = getInterface(context.settings);
return {
Program() {
if (configuredMochaInterface === 'require') {
return;
}
reportUnexpectedImportBindingsInModule(context, configuredMochaInterface);
},
...createMochaVisitors(context, {
anyTestEntity(visitorContext) {
if (visitorContext.interface !== interfaceToUse) {
reportUnexpectedInterface(context, visitorContext.node, visitorContext.interface, interfaceToUse);
}
}
}, {
includeAllInterfaces: true
})
};
}
};
//# sourceMappingURL=consistent-interface.js.map