eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
257 lines (256 loc) • 11.5 kB
JavaScript
;
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2024 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 GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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 GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// https://sonarsource.github.io/rspec/#/rspec/S3776
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const helpers_1 = require("../helpers");
const meta_1 = require("./meta");
const DEFAULT_THRESHOLD = 15;
const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.';
exports.rule = {
meta: (0, helpers_1.generateMeta)(meta_1.meta, {
messages: {
refactorFunction: message,
fileComplexity: '{{complexityAmount}}',
},
schema: meta_1.schema,
}, true),
create(context) {
/** Complexity threshold */
const threshold = context.options[0] ?? DEFAULT_THRESHOLD;
/** Indicator if the file complexity should be reported */
const isFileComplexity = context.options.includes('metric');
/** Set of already considered (with already computed complexity) logical expressions */
const consideredLogicalExpressions = new Set();
/** Stack of scopes that are either functions or the program */
const scopes = [];
return {
':function': (node) => {
onEnterFunction(node);
},
':function:exit'(node) {
onLeaveFunction(node);
},
'*'(node) {
if (scopes[scopes.length - 1]?.nestingNodes.has(node)) {
scopes[scopes.length - 1].nestingLevel++;
}
},
'*:exit'(node) {
if (scopes[scopes.length - 1]?.nestingNodes.has(node)) {
scopes[scopes.length - 1].nestingLevel--;
scopes[scopes.length - 1].nestingNodes.delete(node);
}
},
Program(node) {
scopes.push({
node: node,
nestingLevel: 0,
nestingNodes: new Set(),
complexityPoints: [],
});
},
'Program:exit'(node) {
const programComplexity = scopes.pop();
if (isFileComplexity) {
// value from the message will be saved in SonarQube as file complexity metric
context.report({
node,
messageId: 'fileComplexity',
data: {
complexityAmount: programComplexity.complexityPoints.reduce((acc, cur) => acc + cur.complexity, 0),
},
});
}
},
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);
},
};
function onEnterFunction(node) {
scopes.push({ node, nestingLevel: 0, nestingNodes: new Set(), complexityPoints: [] });
}
function onLeaveFunction(node) {
const functionComplexity = scopes.pop();
checkFunction(functionComplexity.complexityPoints, (0, helpers_1.getMainFunctionTokenLocation)(node, node.parent, context));
}
function visitIfStatement(ifStatement) {
const { parent } = ifStatement;
const { loc: ifLoc } = (0, helpers_1.getFirstToken)(ifStatement, context);
// if the current `if` statement is `else if`, do not count it in structural complexity
if ((0, helpers_1.isIfStatement)(parent) && parent.alternate === ifStatement) {
addComplexity(ifLoc);
}
else {
addStructuralComplexity(ifLoc);
}
// always increase nesting level inside `then` statement
scopes[scopes.length - 1].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, helpers_1.isIfStatement)(ifStatement.alternate)) {
scopes[scopes.length - 1].nestingNodes.add(ifStatement.alternate);
const elseTokenLoc = (0, helpers_1.getFirstTokenAfter)(ifStatement.consequent, context).loc;
addComplexity(elseTokenLoc);
}
}
function visitLoop(loop) {
addStructuralComplexity((0, helpers_1.getFirstToken)(loop, context).loc);
scopes[scopes.length - 1].nestingNodes.add(loop.body);
}
function visitSwitchStatement(switchStatement) {
addStructuralComplexity((0, helpers_1.getFirstToken)(switchStatement, context).loc);
for (const switchCase of switchStatement.cases) {
scopes[scopes.length - 1].nestingNodes.add(switchCase);
}
}
function visitContinueOrBreakStatement(statement) {
if (statement.label) {
addComplexity((0, helpers_1.getFirstToken)(statement, context).loc);
}
}
function visitCatchClause(catchClause) {
addStructuralComplexity((0, helpers_1.getFirstToken)(catchClause, context).loc);
scopes[scopes.length - 1].nestingNodes.add(catchClause.body);
}
function visitConditionalExpression(conditionalExpression) {
const questionTokenLoc = (0, helpers_1.getFirstTokenAfter)(conditionalExpression.test, context).loc;
addStructuralComplexity(questionTokenLoc);
scopes[scopes.length - 1].nestingNodes.add(conditionalExpression.consequent);
scopes[scopes.length - 1].nestingNodes.add(conditionalExpression.alternate);
}
function visitLogicalExpression(logicalExpression) {
const jsxShortCircuitNodes = (0, helpers_1.getJsxShortCircuitNodes)(logicalExpression);
if (jsxShortCircuitNodes != null) {
jsxShortCircuitNodes.forEach(node => consideredLogicalExpressions.add(node));
return;
}
if (isDefaultValuePattern(logicalExpression)) {
return;
}
if (!consideredLogicalExpressions.has(logicalExpression)) {
const flattenedLogicalExpressions = flattenLogicalExpression(logicalExpression);
let previous;
for (const current of flattenedLogicalExpressions) {
if (!previous || previous.operator !== current.operator) {
const operatorTokenLoc = (0, helpers_1.getFirstTokenAfter)(current.left, context).loc;
addComplexity(operatorTokenLoc);
}
previous = current;
}
}
}
function isDefaultValuePattern(node) {
const { left, right, operator, parent } = node;
const operators = ['||', '??'];
const literals = ['Literal', 'ArrayExpression', 'ObjectExpression'];
switch (parent?.type) {
/* Matches: const x = a || literal */
case 'VariableDeclarator':
return operators.includes(operator) && literals.includes(right.type);
/* Matches: a = a || literal */
case 'AssignmentExpression':
return (operators.includes(operator) &&
literals.includes(right.type) &&
context.sourceCode.getText(parent.left) ===
context.sourceCode.getText(left));
default:
return false;
}
}
function flattenLogicalExpression(node) {
if ((0, helpers_1.isLogicalExpression)(node)) {
consideredLogicalExpressions.add(node);
return [
...flattenLogicalExpression(node.left),
node,
...flattenLogicalExpression(node.right),
];
}
return [];
}
function addStructuralComplexity(location) {
const added = scopes[scopes.length - 1].nestingLevel + 1;
const complexityPoint = { complexity: added, location };
scopes[scopes.length - 1].complexityPoints.push(complexityPoint);
}
function addComplexity(location) {
const complexityPoint = { complexity: 1, location };
scopes[scopes.length - 1].complexityPoints.push(complexityPoint);
}
function checkFunction(complexity = [], loc) {
if (isFileComplexity) {
return;
}
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, helpers_1.toSecondaryLocation)({ loc: location }, message);
});
(0, helpers_1.report)(context, {
messageId: 'refactorFunction',
message,
data: {
complexityAmount: complexityAmount,
threshold: threshold, //currently typings do not accept number
},
loc,
}, secondaryLocations, complexityAmount - threshold);
}
}
},
};