UNPKG

eslint-plugin-sonarjs

Version:
299 lines (298 loc) • 13.7 kB
"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; }