eslint-plugin-ava
Version:
ESLint rules for AVA
120 lines (100 loc) • 3.54 kB
JavaScript
import createAvaRule, {visitIf} from '../create-ava-rule.js';
import {findVariable} from '@eslint-community/eslint-utils';
import util from '../util.js';
const MESSAGE_ID = 'require-assertion';
function getImplementationArgument(node, sourceCode) {
return util.getTestImplementationArgument(node, sourceCode);
}
function getTestObjectVariable(implementationArgument, sourceCode) {
if (!util.isFunctionExpression(implementationArgument)) {
return undefined;
}
const [firstParameter] = implementationArgument.params;
if (
firstParameter?.type !== 'Identifier'
|| !util.isTestObject(firstParameter.name)
) {
return undefined;
}
return findVariable(sourceCode.getScope(firstParameter), firstParameter);
}
function createTestState(node, sourceCode) {
let implementationArgument = getImplementationArgument(node, sourceCode);
implementationArgument = util.getMacroExec(implementationArgument) ?? implementationArgument;
return {
hasExternalImplementation: Boolean(implementationArgument && !util.isFunctionExpression(implementationArgument)),
testObjectVariable: getTestObjectVariable(implementationArgument, sourceCode),
};
}
function isTestObjectArgument(argument, sourceCode, testObjectVariable) {
const normalizedArgument = util.unwrapTypeExpression(argument);
if (
normalizedArgument?.type !== 'Identifier'
|| !testObjectVariable
) {
return false;
}
const variable = findVariable(sourceCode.getScope(normalizedArgument), normalizedArgument);
return variable === testObjectVariable;
}
const create = context => {
const ava = createAvaRule(context.sourceCode);
const {sourceCode} = context;
let assertionCount = 0;
let currentTestState;
return ava.merge({
CallExpression: visitIf([ava.isInTestFile, ava.isInTestNode])(node => {
const isCurrentTestNode = ava.isTestNode(node);
if (isCurrentTestNode) {
currentTestState = createTestState(node, sourceCode);
}
// Macro and external test implementation references assert outside this node.
if (isCurrentTestNode && currentTestState.hasExternalImplementation) {
assertionCount++;
return;
}
// Intentionally treat passing the current test object to any call as assertion-like.
// This keeps helper patterns like `helper(t)` accepted, at the cost of known false negatives such as `console.log(t)`.
if (node.arguments.some(argument => isTestObjectArgument(argument, sourceCode, currentTestState?.testObjectVariable))) {
assertionCount++;
return;
}
if (node.callee.type !== 'MemberExpression') {
return;
}
const rootObject = util.getRootNode(node.callee).object;
if (!isTestObjectArgument(rootObject, sourceCode, currentTestState?.testObjectVariable)) {
return;
}
const methodName = util.getAssertionMethod(node.callee);
if (util.assertionMethods.has(methodName) || methodName === 'plan') {
assertionCount++;
}
}),
'CallExpression:exit': visitIf([ava.isTestNode])(node => {
if (ava.hasNoUtilityModifier() && !ava.hasTestModifier('todo') && assertionCount === 0) {
context.report({
node,
messageId: MESSAGE_ID,
});
}
currentTestState = undefined;
assertionCount = 0;
}),
});
};
export default {
create,
meta: {
type: 'problem',
docs: {
description: 'Require that tests contain at least one assertion.',
recommended: true,
url: util.getDocsUrl(import.meta.filename),
},
schema: [],
messages: {
[MESSAGE_ID]: 'Test is missing an assertion. Tests without assertions will always pass.',
},
},
};