eslint-plugin-sonarjs
Version:
299 lines (298 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLAYWRIGHT_DESCRIBE_FOCUS_MODIFIER = exports.PLAYWRIGHT_DESCRIBE_MODIFIERS = exports.PLAYWRIGHT_TEST_MODIFIERS = exports.SUITE_FUNCTION_NAMES = exports.TEST_FUNCTION_NAMES = exports.SUPPORTED_TEST_FRAMEWORKS = void 0;
exports.getParameterizedTestFrameworks = getParameterizedTestFrameworks;
exports.hasSupportedParameterizedCallback = hasSupportedParameterizedCallback;
exports.isSupportedParameterizedDeclaration = isSupportedParameterizedDeclaration;
exports.isMochaTestConstruct = isMochaTestConstruct;
exports.isConcreteMochaTestModifier = isConcreteMochaTestModifier;
exports.getMochaCalleeParts = getMochaCalleeParts;
exports.collectMemberChain = collectMemberChain;
exports.getMochaConstructAndModifiers = getMochaConstructAndModifiers;
exports.hasCallback = hasCallback;
exports.getStaticTitle = getStaticTitle;
exports.getPlaywrightTestQualifiers = getPlaywrightTestQualifiers;
exports.getPlaywrightDescribeQualifiers = getPlaywrightDescribeQualifiers;
const ast_js_1 = require("../ast.js");
const module_js_1 = require("../module.js");
exports.SUPPORTED_TEST_FRAMEWORKS = [
'bun:test',
'jest',
'mocha',
'node:test',
'vitest',
'@playwright/test',
];
const MOCHA_STYLE_TEST_FRAMEWORKS = new Set(['bun:test', 'jest', 'mocha', 'test', 'vitest']);
exports.TEST_FUNCTION_NAMES = ['it', 'specify', 'test'];
exports.SUITE_FUNCTION_NAMES = ['describe', 'context', 'suite'];
const COMMON_MOCHA_TEST_MODIFIERS = new Set(['only', 'concurrent']);
const JEST_TEST_MODIFIERS = new Set(['failing']);
const VITEST_TEST_MODIFIERS = new Set(['sequential']);
const PLAYWRIGHT_TEST_FQN = '@playwright.test.test';
exports.PLAYWRIGHT_TEST_MODIFIERS = new Set(['only', 'fail']);
exports.PLAYWRIGHT_DESCRIBE_MODIFIERS = new Set(['parallel', 'serial']);
exports.PLAYWRIGHT_DESCRIBE_FOCUS_MODIFIER = 'only';
const PARAMETERIZED_TEST_JEST_IMPORTS = ['jest', '@jest/globals'];
const PARAMETERIZED_TEST_JEST_DEPENDENCIES = ['jest'];
const PARAMETERIZED_TEST_VITEST_MODULES = ['vitest'];
const PARAMETERIZED_TEST_BUN_MODULES = ['bun:test'];
const PARAMETERIZED_TEST_BUN_FQN_PREFIX = 'bun:test.';
const PARAMETERIZED_TEST_JEST_GLOBALS_FQN_PREFIX = '@jest.globals.';
const PARAMETERIZED_TEST_COMMON_MODIFIERS = new Set(['only', 'concurrent']);
const PARAMETERIZED_TEST_JEST_MODIFIERS = new Set(['failing']);
const PARAMETERIZED_TEST_VITEST_MODIFIERS = new Set(['fails', 'sequential']);
const PARAMETERIZED_TEST_NAMES = new Set(['test', 'it']);
const PARAMETERIZED_SUITE_NAMES = new Set(['describe', 'suite']);
const PARAMETERIZED_BUN_CONSTRUCTS = new Set(['test', 'describe']);
function getParameterizedTestFrameworks(context) {
return {
bun: (0, module_js_1.importsOrDependsOnModule)(context, PARAMETERIZED_TEST_BUN_MODULES, PARAMETERIZED_TEST_BUN_MODULES),
jest: (0, module_js_1.importsOrDependsOnModule)(context, PARAMETERIZED_TEST_JEST_IMPORTS, PARAMETERIZED_TEST_JEST_DEPENDENCIES),
vitest: (0, module_js_1.importsOrDependsOnModule)(context, PARAMETERIZED_TEST_VITEST_MODULES, PARAMETERIZED_TEST_VITEST_MODULES),
};
}
function hasSupportedParameterizedCallback(context, node) {
if (hasCallback(node)) {
return true;
}
const callback = node.arguments[1];
if (callback?.type !== 'Identifier') {
return false;
}
const variable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(callback), callback.name);
if (variable?.defs.length !== 1) {
return false;
}
const [definition] = variable.defs;
if (definition.type === 'FunctionName') {
return !variable.references.some(reference => reference.isWrite());
}
if (definition.type !== 'Variable') {
return false;
}
const initializer = definition.node.init;
return (initializer != null &&
ast_js_1.FUNCTION_NODES.includes(initializer.type) &&
(0, ast_js_1.getUniqueWriteReference)(variable) === initializer);
}
function isSupportedParameterizedDeclaration(context, node, activeFrameworks) {
const parts = getMochaCalleeParts(node.callee);
if (parts?.modifiers.at(-1) !== 'each') {
return false;
}
const name = getParameterizedConstructName(context, parts.base);
if (name === undefined ||
(!PARAMETERIZED_TEST_NAMES.has(name) && !PARAMETERIZED_SUITE_NAMES.has(name))) {
return false;
}
const framework = getParameterizedFramework(context, parts.base, activeFrameworks);
if (framework === undefined ||
(name === 'suite' && framework !== 'vitest') ||
(framework === 'bun' && !PARAMETERIZED_BUN_CONSTRUCTS.has(name))) {
return false;
}
const modifiers = parts.modifiers.slice(0, -1);
return modifiers.every(modifier => isSupportedParameterizedModifier(modifier, name, framework));
}
function getParameterizedConstructName(context, identifier) {
const fqn = (0, module_js_1.getFullyQualifiedName)(context, identifier);
if (fqn?.startsWith(PARAMETERIZED_TEST_BUN_FQN_PREFIX)) {
return fqn.slice(PARAMETERIZED_TEST_BUN_FQN_PREFIX.length);
}
if (fqn?.startsWith(PARAMETERIZED_TEST_JEST_GLOBALS_FQN_PREFIX)) {
return fqn.slice(PARAMETERIZED_TEST_JEST_GLOBALS_FQN_PREFIX.length);
}
if (fqn?.startsWith('jest.')) {
return fqn.slice('jest.'.length);
}
if (fqn?.startsWith('vitest.')) {
return fqn.slice('vitest.'.length);
}
const variable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(identifier), identifier.name);
return variable == null || variable.defs.length === 0 ? identifier.name : undefined;
}
function getParameterizedFramework(context, base, activeFrameworks) {
const fqn = (0, module_js_1.getFullyQualifiedName)(context, base);
if (fqn?.startsWith(PARAMETERIZED_TEST_BUN_FQN_PREFIX)) {
return 'bun';
}
if (fqn?.startsWith('vitest.')) {
return 'vitest';
}
if (fqn?.startsWith('jest.') || fqn?.startsWith(PARAMETERIZED_TEST_JEST_GLOBALS_FQN_PREFIX)) {
return 'jest';
}
if (base.name === 'suite') {
return activeFrameworks.vitest ? 'vitest' : undefined;
}
const active = Object.entries(activeFrameworks).filter(([, isActive]) => isActive);
return active.length === 1 ? active[0][0] : undefined;
}
function isSupportedParameterizedModifier(modifier, name, framework) {
if (PARAMETERIZED_TEST_COMMON_MODIFIERS.has(modifier)) {
return framework !== 'bun' || modifier === 'only';
}
if (!PARAMETERIZED_TEST_NAMES.has(name)) {
return framework === 'vitest' && modifier === 'sequential';
}
return ((framework === 'jest' && PARAMETERIZED_TEST_JEST_MODIFIERS.has(modifier)) ||
(framework === 'vitest' && PARAMETERIZED_TEST_VITEST_MODIFIERS.has(modifier)));
}
function isMochaTestConstruct(context, node, constructs,
// The parameterized `each` form (`describe.each(table)(name, fn)`) is opt-in: it
// is an executing test/suite, but some rules deliberately exclude parameterized
// titles (e.g. S8781 `no-empty-test-title`), so it is off by default.
{ allowParameterized = false } = {}) {
const calleeParts = getMochaCalleeParts(node.callee);
if (calleeParts === undefined) {
return false;
}
const { constructName, modifiers } = getMochaConstructAndModifiers(context, calleeParts);
if (constructName === undefined || !constructs.includes(constructName)) {
return false;
}
return modifiers.every(modifier => (allowParameterized && modifier === 'each') || isConcreteMochaTestModifier(context, modifier));
}
function isConcreteMochaTestModifier(context, modifier) {
return (COMMON_MOCHA_TEST_MODIFIERS.has(modifier) ||
(JEST_TEST_MODIFIERS.has(modifier) && isTestFrameworkActive(context, 'jest')) ||
(VITEST_TEST_MODIFIERS.has(modifier) && isTestFrameworkActive(context, 'vitest')));
}
function isTestFrameworkActive(context, framework) {
return (0, module_js_1.importsOrDependsOnModule)(context, [framework], [framework]);
}
function getMochaCalleeParts(node) {
const modifiers = [];
let current = node;
// Tracks whether the node we just descended from was a call application, so the
// curried `each` form (`X.each(table)(...)`) can be told apart from the bare
// factory (`X.each` / `X.each(table)`), which is not itself a test/suite.
let appliedAsCall = false;
while (true) {
if (current.type === 'CallExpression') {
current = current.callee;
appliedAsCall = true;
}
else if (isQualifyingMember(current)) {
if (current.property.name === 'each' && !appliedAsCall) {
return undefined;
}
modifiers.unshift(current.property.name);
current = current.object;
appliedAsCall = false;
}
else {
break;
}
}
return current.type === 'Identifier' ? { base: current, modifiers } : undefined;
}
function collectMemberChain(node) {
const qualifiers = [];
let current = node;
while (isQualifyingMember(current)) {
qualifiers.unshift(current.property.name);
current = current.object;
}
return { base: current, qualifiers };
}
function isQualifyingMember(node) {
return node.type === 'MemberExpression' && !node.computed && (0, ast_js_1.isIdentifier)(node.property);
}
function getMochaBaseConstructName(context, identifier) {
const variable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(identifier), identifier.name);
const definition = variable?.defs.find(def => def.type === 'ImportBinding' || isSupportedRequireBinding(def.node));
if (definition !== undefined) {
const fqn = (0, module_js_1.getFullyQualifiedName)(context, identifier);
return (getMochaConstructNameFromFqn(fqn) ??
(fqn === 'test' && isNodeTestDefaultBindingDefinition(definition) ? 'test' : undefined));
}
return variable == null || variable.defs.length === 0 ? identifier.name : undefined;
}
function getMochaConstructAndModifiers(context, calleeParts) {
const constructName = getMochaBaseConstructName(context, calleeParts.base);
if (constructName === 'test' &&
isNodeTestDefaultBinding(context, calleeParts.base) &&
calleeParts.modifiers.length > 0) {
const [nodeTestConstructName, ...modifiers] = calleeParts.modifiers;
if (exports.TEST_FUNCTION_NAMES.includes(nodeTestConstructName) ||
exports.SUITE_FUNCTION_NAMES.includes(nodeTestConstructName)) {
return { constructName: nodeTestConstructName, modifiers };
}
}
return { constructName, modifiers: calleeParts.modifiers };
}
function isNodeTestDefaultBinding(context, identifier) {
const variable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(identifier), identifier.name);
const definition = variable?.defs.find(def => def.type === 'ImportBinding' || isSupportedRequireBinding(def.node));
return definition !== undefined && isNodeTestDefaultBindingDefinition(definition);
}
function isNodeTestDefaultBindingDefinition(definition) {
if (definition.type === 'ImportBinding') {
return (definition.node.type === 'ImportDefaultSpecifier' &&
definition.parent.type === 'ImportDeclaration' &&
definition.parent.source.value === 'node:test');
}
if (definition.type !== 'Variable' || definition.node.type !== 'VariableDeclarator') {
return false;
}
const requireCall = definition.node.init && getRequireCall(definition.node.init);
const moduleName = requireCall?.arguments[0];
return (definition.node.id.type === 'Identifier' &&
moduleName?.type === 'Literal' &&
moduleName.value === 'node:test');
}
function getMochaConstructNameFromFqn(fqn) {
const parts = fqn?.split('.');
if (parts?.length !== 2) {
return undefined;
}
const [framework, constructName] = parts;
return MOCHA_STYLE_TEST_FRAMEWORKS.has(framework) ? constructName : undefined;
}
function isSupportedRequireBinding(node) {
if (node.type !== 'VariableDeclarator' || node.init == null) {
return false;
}
const requireCall = getRequireCall(node.init);
const moduleName = requireCall?.arguments[0];
return (moduleName?.type === 'Literal' &&
typeof moduleName.value === 'string' &&
exports.SUPPORTED_TEST_FRAMEWORKS.includes(moduleName.value));
}
function getRequireCall(node) {
if (isRequireCall(node)) {
return node;
}
if (node.type === 'MemberExpression') {
return getRequireCall(node.object);
}
return undefined;
}
function isRequireCall(node) {
return (node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require');
}
function hasCallback(node) {
return node.arguments.some(argument => ast_js_1.FUNCTION_NODES.includes(argument.type));
}
function getStaticTitle(node) {
if ((0, ast_js_1.isStringLiteral)(node)) {
return node.value;
}
if ((0, ast_js_1.isStaticTemplateLiteral)(node)) {
const value = node.quasis[0].value;
return value.cooked ?? value.raw;
}
return undefined;
}
function getPlaywrightTestQualifiers(context, node) {
const { base, qualifiers } = collectMemberChain(node);
return (0, module_js_1.getFullyQualifiedName)(context, base) === PLAYWRIGHT_TEST_FQN ? qualifiers : undefined;
}
function getPlaywrightDescribeQualifiers(node) {
const { base, qualifiers } = collectMemberChain(node);
return (0, ast_js_1.isIdentifier)(base, 'test') ? qualifiers : undefined;
}