eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
370 lines (369 loc) • 16.5 kB
JavaScript
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* 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/S3776
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const index_js_1 = require("../helpers/index.js");
const meta_js_1 = require("./meta.js");
const DEFAULT_THRESHOLD = 15;
const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.';
exports.rule = {
meta: (0, index_js_1.generateMeta)(meta_js_1.meta, {
messages: {
refactorFunction: message,
fileComplexity: '{{complexityAmount}}',
},
schema: meta_js_1.schema,
}, true),
create(context) {
/** Complexity threshold */
const thresholdOption = context.options[0];
const threshold = typeof thresholdOption === 'number' ? thresholdOption : DEFAULT_THRESHOLD;
/** Indicator if the file complexity should be reported */
const isFileComplexity = context.options.includes('metric');
/** Complexity of the file */
let fileComplexity = 0;
/** Complexity of the current function if it is *not* considered nested to the first level function */
let complexityIfNotNested = [];
/** Complexity of the current function if it is considered nested to the first level function */
let complexityIfNested = [];
/** Current nesting level (number of enclosing control flow statements and functions) */
let nesting = 0;
/** Indicator if the current top level function has a structural (generated by control flow statements) complexity */
let topLevelHasStructuralComplexity = false;
/** Indicator if the current top level function is React functional component */
const reactFunctionalComponent = {
nameStartsWithCapital: false,
returnsJsx: false,
isConfirmed() {
return this.nameStartsWithCapital && this.returnsJsx;
},
init(node) {
this.nameStartsWithCapital = nameStartsWithCapital(node);
this.returnsJsx = false;
},
};
/** Own (not including nested functions) complexity of the current top function */
let topLevelOwnComplexity = [];
/** Nodes that should increase nesting level */
const nestingNodes = new Set();
/** Set of already considered (with already computed complexity) logical expressions */
const consideredLogicalExpressions = new Set();
/** Stack of enclosing functions */
const enclosingFunctions = [];
/** Stack of complexity points for each function without accumulated nested complexity */
const functionOwnComplexity = [];
const functionOwnControlFlowNesting = [];
let secondLevelFunctions = [];
return {
':function': (node) => {
onEnterFunction(node);
},
':function:exit'(node) {
onLeaveFunction(node);
},
'*'(node) {
if (nestingNodes.has(node)) {
nesting++;
if (functionOwnControlFlowNesting.length > 0) {
functionOwnControlFlowNesting[functionOwnControlFlowNesting.length - 1]++;
}
}
},
'*:exit'(node) {
if (nestingNodes.has(node)) {
nesting--;
nestingNodes.delete(node);
if (functionOwnControlFlowNesting.length > 0) {
functionOwnControlFlowNesting[functionOwnControlFlowNesting.length - 1]--;
}
}
},
Program() {
fileComplexity = 0;
},
'Program:exit'(node) {
if (isFileComplexity) {
// value from the message will be saved in SonarQube as file complexity metric
context.report({
node,
messageId: 'fileComplexity',
data: { complexityAmount: fileComplexity },
});
}
},
IfStatement(node) {
visitIfStatement(node);
},
ForStatement(node) {
visitLoop(node);
},
ForInStatement(node) {
visitLoop(node);
},
ForOfStatement(node) {
visitLoop(node);
},
DoWhileStatement(node) {
visitLoop(node);
},
WhileStatement(node) {
visitLoop(node);
},
SwitchStatement(node) {
visitSwitchStatement(node);
},
ContinueStatement(node) {
visitContinueOrBreakStatement(node);
},
BreakStatement(node) {
visitContinueOrBreakStatement(node);
},
CatchClause(node) {
visitCatchClause(node);
},
LogicalExpression(node) {
visitLogicalExpression(node);
},
ConditionalExpression(node) {
visitConditionalExpression(node);
},
ReturnStatement(node) {
visitReturnStatement(node);
},
};
function onEnterFunction(node) {
if (enclosingFunctions.length === 0) {
// top level function
topLevelHasStructuralComplexity = false;
reactFunctionalComponent.init(node);
topLevelOwnComplexity = [];
secondLevelFunctions = [];
}
else if (enclosingFunctions.length === 1) {
// second level function
complexityIfNotNested = [];
complexityIfNested = [];
}
else {
nesting++;
nestingNodes.add(node);
}
enclosingFunctions.push(node);
functionOwnComplexity.push([]);
functionOwnControlFlowNesting.push(0);
}
function onLeaveFunction(node) {
const functionComplexity = functionOwnComplexity.pop();
functionOwnControlFlowNesting.pop();
enclosingFunctions.pop();
if (enclosingFunctions.length === 0) {
// top level function
if (topLevelHasStructuralComplexity && !reactFunctionalComponent.isConfirmed()) {
let totalComplexity = topLevelOwnComplexity;
secondLevelFunctions.forEach(secondLevelFunction => {
totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested);
});
fileComplexity += totalComplexity.reduce((acc, cur) => acc + cur.complexity, 0);
}
else {
fileComplexity += topLevelOwnComplexity.reduce((acc, cur) => acc + cur.complexity, 0);
secondLevelFunctions.forEach(secondLevelFunction => {
fileComplexity += secondLevelFunction.complexityIfThisSecondaryIsTopLevel.reduce((acc, cur) => acc + cur.complexity, 0);
});
}
}
else if (enclosingFunctions.length === 1) {
// second level function
secondLevelFunctions.push({
node,
parent: node.parent,
complexityIfNested,
complexityIfThisSecondaryIsTopLevel: complexityIfNotNested,
loc: (0, index_js_1.getMainFunctionTokenLocation)(node, node.parent, context),
});
}
if (isFileComplexity) {
return;
}
checkFunction(functionComplexity, (0, index_js_1.getMainFunctionTokenLocation)(node, node.parent, context));
}
function visitIfStatement(ifStatement) {
const { parent } = ifStatement;
const { loc: ifLoc } = (0, index_js_1.getFirstToken)(ifStatement, context);
// if the current `if` statement is `else if`, do not count it in structural complexity
if ((0, index_js_1.isIfStatement)(parent) && parent.alternate === ifStatement) {
addComplexity(ifLoc);
}
else {
addStructuralComplexity(ifLoc);
}
// always increase nesting level inside `then` statement
nestingNodes.add(ifStatement.consequent);
// if `else` branch is not `else if` then
// - increase nesting level inside `else` statement
// - add +1 complexity
if (ifStatement.alternate && !(0, index_js_1.isIfStatement)(ifStatement.alternate)) {
nestingNodes.add(ifStatement.alternate);
const elseTokenLoc = (0, index_js_1.getFirstTokenAfter)(ifStatement.consequent, context).loc;
addComplexity(elseTokenLoc);
}
}
function visitLoop(loop) {
addStructuralComplexity((0, index_js_1.getFirstToken)(loop, context).loc);
nestingNodes.add(loop.body);
}
function visitSwitchStatement(switchStatement) {
addStructuralComplexity((0, index_js_1.getFirstToken)(switchStatement, context).loc);
for (const switchCase of switchStatement.cases) {
nestingNodes.add(switchCase);
}
}
function visitContinueOrBreakStatement(statement) {
if (statement.label) {
addComplexity((0, index_js_1.getFirstToken)(statement, context).loc);
}
}
function visitCatchClause(catchClause) {
addStructuralComplexity((0, index_js_1.getFirstToken)(catchClause, context).loc);
nestingNodes.add(catchClause.body);
}
function visitConditionalExpression(conditionalExpression) {
const questionTokenLoc = (0, index_js_1.getFirstTokenAfter)(conditionalExpression.test, context).loc;
addStructuralComplexity(questionTokenLoc);
nestingNodes.add(conditionalExpression.consequent);
nestingNodes.add(conditionalExpression.alternate);
}
function visitReturnStatement({ argument }) {
// top level function
if (enclosingFunctions.length === 1 &&
argument &&
['JSXElement', 'JSXFragment'].includes(argument.type)) {
reactFunctionalComponent.returnsJsx = true;
}
}
function nameStartsWithCapital(node) {
const checkFirstLetter = (name) => {
const firstLetter = name[0];
return firstLetter === firstLetter.toUpperCase();
};
if (!(0, index_js_1.isArrowFunctionExpression)(node) && node.id) {
return checkFirstLetter(node.id.name);
}
const { parent } = node;
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
return checkFirstLetter(parent.id.name);
}
return false;
}
function visitLogicalExpression(logicalExpression) {
const jsxShortCircuitNodes = (0, index_js_1.getJsxShortCircuitNodes)(logicalExpression);
if (jsxShortCircuitNodes != null) {
jsxShortCircuitNodes.forEach(node => consideredLogicalExpressions.add(node));
return;
}
if (!consideredLogicalExpressions.has(logicalExpression)) {
const flattenedLogicalExpressions = flattenLogicalExpression(logicalExpression);
let previous;
for (const current of flattenedLogicalExpressions) {
if (current.operator !== '||' &&
current.operator !== '??' &&
(!previous || previous.operator !== current.operator)) {
const operatorTokenLoc = (0, index_js_1.getFirstTokenAfter)(current.left, context).loc;
addComplexity(operatorTokenLoc);
}
previous = current;
}
}
}
function flattenLogicalExpression(node) {
if ((0, index_js_1.isLogicalExpression)(node)) {
consideredLogicalExpressions.add(node);
return [
...flattenLogicalExpression(node.left),
node,
...flattenLogicalExpression(node.right),
];
}
return [];
}
function addStructuralComplexity(location) {
const added = nesting + 1;
const complexityPoint = { complexity: added, location };
if (enclosingFunctions.length === 0) {
// top level scope
fileComplexity += added;
}
else if (enclosingFunctions.length === 1) {
// top level function
topLevelHasStructuralComplexity = true;
topLevelOwnComplexity.push(complexityPoint);
}
else {
// second+ level function
complexityIfNested.push({ complexity: added + 1, location });
complexityIfNotNested.push(complexityPoint);
}
if (functionOwnComplexity.length > 0) {
const addedWithoutFunctionNesting = functionOwnControlFlowNesting[functionOwnControlFlowNesting.length - 1] + 1;
functionOwnComplexity[functionOwnComplexity.length - 1].push({
complexity: addedWithoutFunctionNesting,
location,
});
}
}
function addComplexity(location) {
const complexityPoint = { complexity: 1, location };
if (functionOwnComplexity.length > 0) {
functionOwnComplexity[functionOwnComplexity.length - 1].push(complexityPoint);
}
if (enclosingFunctions.length === 0) {
// top level scope
fileComplexity += 1;
}
else if (enclosingFunctions.length === 1) {
// top level function
topLevelOwnComplexity.push(complexityPoint);
}
else {
// second+ level function
complexityIfNested.push(complexityPoint);
complexityIfNotNested.push(complexityPoint);
}
}
function checkFunction(complexity = [], loc) {
const complexityAmount = complexity.reduce((acc, cur) => acc + cur.complexity, 0);
if (complexityAmount > threshold) {
const secondaryLocations = complexity.map(complexityPoint => {
const { complexity, location } = complexityPoint;
const message = complexity === 1 ? '+1' : `+${complexity} (incl. ${complexity - 1} for nesting)`;
return (0, index_js_1.toSecondaryLocation)({ loc: location }, message);
});
(0, index_js_1.report)(context, {
messageId: 'refactorFunction',
message,
data: {
complexityAmount: complexityAmount,
threshold: threshold, //currently typings do not accept number
},
loc,
}, secondaryLocations, complexityAmount - threshold);
}
}
},
};
;