eslint-plugin-sonarjs
Version:
290 lines (289 loc) • 12.5 kB
JavaScript
;
/*
* SonarQube JavaScript Plugin
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
// https://sonarsource.github.io/rspec/#/rspec/S8754/javascript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const ast_js_1 = require("../helpers/ast.js");
const ancestor_js_1 = require("../helpers/ancestor.js");
const generate_meta_js_1 = require("../helpers/generate-meta.js");
const location_js_1 = require("../helpers/location.js");
const module_js_1 = require("../helpers/module.js");
const mocha_style_test_frameworks_js_1 = require("../helpers/mocha-style-test-frameworks.js");
const meta = __importStar(require("./generated-meta.js"));
const PLAYWRIGHT_DISABLED_DESCRIBE_MODIFIERS = new Set(['skip', 'fixme']);
const MESSAGE = 'Rename this test title to make it unique within the suite.';
const MESSAGE_ID = 'renameDuplicateTitle';
exports.rule = {
meta: (0, generate_meta_js_1.generateMeta)(meta, {
messages: {
[MESSAGE_ID]: MESSAGE,
},
}),
create(context) {
if (!(0, module_js_1.importsOrDependsOnModule)(context, mocha_style_test_frameworks_js_1.SUPPORTED_TEST_FRAMEWORKS, mocha_style_test_frameworks_js_1.SUPPORTED_TEST_FRAMEWORKS)) {
return {};
}
let suiteStack = [createSuiteFrame()];
const pushedSuiteCalls = new Set();
const concreteSuiteCallbacks = new Set();
const helperExpansionPath = new Set();
let testNesting = 0;
let ignoredSuiteNesting = 0;
let functionNesting = 0;
let concreteSuiteCallbackNesting = 0;
return {
CallExpression(node) {
if (isIgnoredSuiteDeclaration(context, node)) {
ignoredSuiteNesting++;
}
else if (isSuiteDeclaration(context, node)) {
pushedSuiteCalls.add(node);
const callback = getCallback(node);
if (callback !== undefined) {
concreteSuiteCallbacks.add(callback);
}
suiteStack.push(createSuiteFrame());
}
const currentSuiteFrame = suiteStack.at(-1);
if (currentSuiteFrame !== undefined &&
ignoredSuiteNesting === 0 &&
testNesting === 0 &&
isInConcreteCollectionCallback(functionNesting, concreteSuiteCallbackNesting)) {
if (isTestDeclaration(context, node)) {
checkTestTitle(context, node, currentSuiteFrame);
}
else {
checkHelperDefinedTests(context, node, currentSuiteFrame, helperExpansionPath);
}
}
if (isConcreteTestDeclaration(context, node)) {
testNesting++;
}
},
'CallExpression:exit'(node) {
if (isIgnoredSuiteDeclaration(context, node)) {
ignoredSuiteNesting--;
}
if (isConcreteTestDeclaration(context, node)) {
testNesting--;
}
if (pushedSuiteCalls.delete(node)) {
suiteStack.pop();
}
},
':function'(node) {
functionNesting++;
if (concreteSuiteCallbacks.has(node)) {
concreteSuiteCallbackNesting++;
}
},
':function:exit'(node) {
if (concreteSuiteCallbacks.delete(node)) {
concreteSuiteCallbackNesting--;
}
functionNesting--;
},
'Program:exit'() {
suiteStack = [createSuiteFrame()];
pushedSuiteCalls.clear();
concreteSuiteCallbacks.clear();
helperExpansionPath.clear();
testNesting = 0;
ignoredSuiteNesting = 0;
functionNesting = 0;
concreteSuiteCallbackNesting = 0;
},
};
},
};
function createSuiteFrame() {
return { titles: new Map() };
}
function checkTestTitle(context, node, suiteFrame) {
const titleNode = node.arguments[0];
const title = titleNode && (0, mocha_style_test_frameworks_js_1.getStaticTitle)(titleNode);
if (title === undefined) {
return;
}
const originalTitleNode = suiteFrame.titles.get(title);
if (originalTitleNode) {
(0, location_js_1.report)(context, {
node: titleNode,
messageId: MESSAGE_ID,
message: MESSAGE,
}, [(0, location_js_1.toSecondaryLocation)(originalTitleNode, 'Original test title.')]);
return;
}
suiteFrame.titles.set(title, titleNode);
}
function checkHelperDefinedTests(context, node, suiteFrame, helperExpansionPath) {
const helper = getLocalHelperFunction(context, node);
if (helper === undefined || helperExpansionPath.has(helper)) {
return;
}
helperExpansionPath.add(helper);
checkHelperNode(context, helper.body, suiteFrame, helperExpansionPath);
helperExpansionPath.delete(helper);
}
function checkHelperNode(context, node, suiteFrame, helperExpansionPath) {
if (node.type === 'CallExpression') {
if (isIgnoredSuiteDeclaration(context, node)) {
return;
}
if (isSuiteDeclaration(context, node)) {
const nestedSuiteFrame = createSuiteFrame();
const callback = getCallback(node);
if (callback !== undefined) {
checkHelperNode(context, callback.body, nestedSuiteFrame, helperExpansionPath);
}
return;
}
if (isTestDeclaration(context, node)) {
checkTestTitle(context, node, suiteFrame);
return;
}
checkHelperDefinedTests(context, node, suiteFrame, helperExpansionPath);
}
for (const child of (0, ancestor_js_1.childrenOf)(node, context.sourceCode.visitorKeys)) {
if (!ast_js_1.FUNCTION_NODES.includes(child.type)) {
checkHelperNode(context, child, suiteFrame, helperExpansionPath);
}
}
}
function isSuiteDeclaration(context, node) {
return ((0, mocha_style_test_frameworks_js_1.isMochaTestConstruct)(context, node, mocha_style_test_frameworks_js_1.SUITE_FUNCTION_NAMES) ||
isPlaywrightDescribe(context, node.callee));
}
function isTestDeclaration(context, node) {
return isConcreteTestDeclaration(context, node) || isPlaywrightTest(context, node);
}
function isConcreteTestDeclaration(context, node) {
return (0, mocha_style_test_frameworks_js_1.isMochaTestConstruct)(context, node, mocha_style_test_frameworks_js_1.TEST_FUNCTION_NAMES) && (0, mocha_style_test_frameworks_js_1.hasCallback)(node);
}
function isIgnoredSuiteDeclaration(context, node) {
if (isNonConcreteMochaSuite(context, node.callee)) {
return true;
}
return getPlaywrightDescribeClassification(context, node.callee) === 'ignored';
}
function isNonConcreteMochaSuite(context, node) {
const calleeParts = (0, mocha_style_test_frameworks_js_1.getMochaCalleeParts)(node);
if (calleeParts === undefined) {
return false;
}
const constructName = (0, mocha_style_test_frameworks_js_1.getMochaConstructName)(context, calleeParts.base);
if (constructName === undefined || !mocha_style_test_frameworks_js_1.SUITE_FUNCTION_NAMES.includes(constructName)) {
return false;
}
return !calleeParts.modifiers.every(modifier => (0, mocha_style_test_frameworks_js_1.isConcreteMochaTestModifier)(context, modifier));
}
function getCallback(node) {
return node.arguments.find(isCallbackFunctionNode);
}
function isCallbackFunctionNode(node) {
return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
}
function isFunctionNode(node) {
return ast_js_1.FUNCTION_NODES.includes(node.type);
}
function getLocalHelperFunction(context, node) {
if (node.callee.type !== 'Identifier') {
return undefined;
}
const variable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(node.callee), node.callee.name);
const definition = variable?.defs.find(isLocalFunctionDefinition);
if (definition?.node.type === 'FunctionDeclaration') {
return definition.node;
}
if (definition?.node.type === 'VariableDeclarator' &&
definition.node.init != null &&
isFunctionNode(definition.node.init)) {
return definition.node.init;
}
return undefined;
}
function isLocalFunctionDefinition(definition) {
if (definition.type === 'FunctionName') {
return true;
}
return (definition.type === 'Variable' &&
definition.node.type === 'VariableDeclarator' &&
definition.node.init != null &&
isFunctionNode(definition.node.init));
}
function isInConcreteCollectionCallback(functionNesting, concreteSuiteCallbackNesting) {
return functionNesting === concreteSuiteCallbackNesting;
}
function isPlaywrightDescribe(context, callee) {
return getPlaywrightDescribeClassification(context, callee) === 'concrete';
}
function isPlaywrightTest(context, node) {
const qualifiers = (0, mocha_style_test_frameworks_js_1.getPlaywrightTestQualifiers)(context, node.callee);
if (qualifiers === undefined) {
return false;
}
return (qualifiers.every(qualifier => mocha_style_test_frameworks_js_1.PLAYWRIGHT_TEST_MODIFIERS.has(qualifier)) &&
(!qualifiers.includes('fail') || (0, mocha_style_test_frameworks_js_1.hasCallback)(node)));
}
function getPlaywrightDescribeClassification(context, callee) {
const qualifiers = (0, mocha_style_test_frameworks_js_1.getPlaywrightTestQualifiers)(context, callee) ?? (0, mocha_style_test_frameworks_js_1.getPlaywrightDescribeQualifiers)(callee);
if (qualifiers?.[0] !== 'describe') {
return 'unknown';
}
const modifiers = qualifiers.slice(1);
if (modifiers.some(modifier => PLAYWRIGHT_DISABLED_DESCRIBE_MODIFIERS.has(modifier))) {
return 'ignored';
}
const runnableModifiers = modifiers.at(-1) === mocha_style_test_frameworks_js_1.PLAYWRIGHT_DESCRIBE_FOCUS_MODIFIER ? modifiers.slice(0, -1) : modifiers;
return runnableModifiers.every(modifier => mocha_style_test_frameworks_js_1.PLAYWRIGHT_DESCRIBE_MODIFIERS.has(modifier))
? 'concrete'
: 'unknown';
}