eslint-plugin-sonarjs
Version:
SonarJS rules for ESLint
298 lines • 13 kB
JavaScript
;
/*
* eslint-plugin-sonarjs
* Copyright (C) 2018 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://jira.sonarsource.com/browse/RSPEC-3776
const nodes_1 = require("../utils/nodes");
const locations_1 = require("../utils/locations");
const DEFAULT_THRESHOLD = 15;
const rule = {
meta: {
type: "suggestion",
schema: [
{ type: "integer", minimum: 0 },
{
// internal parameter
enum: ["sonar-runtime", "metric"],
},
],
},
create(context) {
const threshold = getThreshold();
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;
/** 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 = [];
let secondLevelFunctions = [];
return {
":function": (node) => {
onEnterFunction(node);
},
":function:exit"(node) {
onLeaveFunction(node);
},
"*"(node) {
if (nestingNodes.has(node)) {
nesting++;
}
},
"*:exit"(node) {
if (nestingNodes.has(node)) {
nesting--;
nestingNodes.delete(node);
}
},
Program() {
fileComplexity = 0;
},
"Program:exit"(node) {
if (isFileComplexity) {
// as issues are the only communication channel of a rule
// we pass data as serialized json as an issue message
context.report({ node, message: fileComplexity.toString() });
}
},
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 getThreshold() {
return context.options[0] !== undefined ? context.options[0] : DEFAULT_THRESHOLD;
}
function onEnterFunction(node) {
if (enclosingFunctions.length === 0) {
// top level function
topLevelHasStructuralComplexity = false;
topLevelOwnComplexity = [];
secondLevelFunctions = [];
}
else if (enclosingFunctions.length === 1) {
// second level function
complexityIfNotNested = [];
complexityIfNested = [];
}
else {
nesting++;
nestingNodes.add(node);
}
enclosingFunctions.push(node);
}
function onLeaveFunction(node) {
enclosingFunctions.pop();
if (enclosingFunctions.length === 0) {
// top level function
if (topLevelHasStructuralComplexity) {
let totalComplexity = topLevelOwnComplexity;
secondLevelFunctions.forEach(secondLevelFunction => {
totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested);
});
checkFunction(totalComplexity, locations_1.getMainFunctionTokenLocation(node, nodes_1.getParent(context), context));
}
else {
checkFunction(topLevelOwnComplexity, locations_1.getMainFunctionTokenLocation(node, nodes_1.getParent(context), context));
secondLevelFunctions.forEach(secondLevelFunction => {
checkFunction(secondLevelFunction.complexityIfThisSecondaryIsTopLevel, locations_1.getMainFunctionTokenLocation(secondLevelFunction.node, secondLevelFunction.parent, context));
});
}
}
else if (enclosingFunctions.length === 1) {
// second level function
secondLevelFunctions.push({
node,
parent: nodes_1.getParent(context),
complexityIfNested,
complexityIfThisSecondaryIsTopLevel: complexityIfNotNested,
loc: locations_1.getMainFunctionTokenLocation(node, nodes_1.getParent(context), context),
});
}
else {
// complexity of third+ level functions is computed in their parent functions
// so we never raise an issue for them
}
}
function visitIfStatement(ifStatement) {
const parent = nodes_1.getParent(context);
const { loc: ifLoc } = locations_1.getFirstToken(ifStatement, context);
// if the current `if` statement is `else if`, do not count it in structural complexity
if (nodes_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 && !nodes_1.isIfStatement(ifStatement.alternate)) {
nestingNodes.add(ifStatement.alternate);
const elseTokenLoc = locations_1.getFirstTokenAfter(ifStatement.consequent, context).loc;
addComplexity(elseTokenLoc);
}
}
function visitLoop(loop) {
addStructuralComplexity(locations_1.getFirstToken(loop, context).loc);
nestingNodes.add(loop.body);
}
function visitSwitchStatement(switchStatement) {
addStructuralComplexity(locations_1.getFirstToken(switchStatement, context).loc);
for (const switchCase of switchStatement.cases) {
nestingNodes.add(switchCase);
}
}
function visitContinueOrBreakStatement(statement) {
if (statement.label) {
addComplexity(locations_1.getFirstToken(statement, context).loc);
}
}
function visitCatchClause(catchClause) {
addStructuralComplexity(locations_1.getFirstToken(catchClause, context).loc);
nestingNodes.add(catchClause.body);
}
function visitConditionalExpression(conditionalExpression) {
const questionTokenLoc = locations_1.getFirstTokenAfter(conditionalExpression.test, context).loc;
addStructuralComplexity(questionTokenLoc);
nestingNodes.add(conditionalExpression.consequent);
nestingNodes.add(conditionalExpression.alternate);
}
function visitLogicalExpression(logicalExpression) {
if (!consideredLogicalExpressions.has(logicalExpression)) {
const flattenedLogicalExpressions = flattenLogicalExpression(logicalExpression);
let previous;
for (const current of flattenedLogicalExpressions) {
if (!previous || previous.operator !== current.operator) {
const operatorTokenLoc = locations_1.getFirstTokenAfter(logicalExpression.left, context).loc;
addComplexity(operatorTokenLoc);
}
previous = current;
}
}
}
function flattenLogicalExpression(node) {
if (nodes_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);
}
}
function addComplexity(location) {
const complexityPoint = { complexity: 1, location };
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);
fileComplexity += complexityAmount;
if (isFileComplexity) {
return;
}
if (complexityAmount > threshold) {
const secondaryLocations = complexity.map(complexityPoint => {
const { complexity, location } = complexityPoint;
const message = complexity === 1 ? "+1" : `+${complexity} (incl. ${complexity - 1} for nesting)`;
return locations_1.issueLocation(location, undefined, message);
});
locations_1.report(context, {
message: `Refactor this function to reduce its Cognitive Complexity from ${complexityAmount} to the ${threshold} allowed.`,
loc,
}, secondaryLocations, complexityAmount - threshold);
}
}
},
};
module.exports = rule;
//# sourceMappingURL=cognitive-complexity.js.map