eslint-plugin-sonarjs
Version:
405 lines (404 loc) • 18 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/S3776
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 location_js_1 = require("../helpers/location.js");
const generate_meta_js_1 = require("../helpers/generate-meta.js");
const jsx_js_1 = require("../helpers/jsx.js");
const ast_js_1 = require("../helpers/ast.js");
const collection_js_1 = require("../helpers/collection.js");
const meta = __importStar(require("./generated-meta.js"));
const internal_metrics_js_1 = require("../helpers/internal-metrics.js");
const DEFAULT_THRESHOLD = 15;
const SILENCE_ISSUES_OPTION = 'silence-issues';
const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.';
exports.rule = {
meta: (0, generate_meta_js_1.generateMeta)(meta, {
messages: {
refactorFunction: message,
},
}),
create(context) {
/** Complexity threshold */
const thresholdOption = context.options[0];
const threshold = typeof thresholdOption === 'number' ? thresholdOption : DEFAULT_THRESHOLD;
const metricsSink = (0, internal_metrics_js_1.getInternalMetricsSink)(context.settings);
const shouldSilenceIssues = context.options.includes(SILENCE_ISSUES_OPTION);
const shouldReportIssues = !shouldSilenceIssues;
/** 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'() {
if (metricsSink) {
metricsSink.cognitiveComplexity = 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;
for (const secondLevelFunction of secondLevelFunctions) {
totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested);
}
fileComplexity += totalComplexity.reduce((acc, cur) => acc + cur.complexity, 0);
}
else {
fileComplexity += topLevelOwnComplexity.reduce((acc, cur) => acc + cur.complexity, 0);
for (const secondLevelFunction of secondLevelFunctions) {
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, location_js_1.getMainFunctionTokenLocation)(node, node.parent, context),
});
}
if (!shouldReportIssues) {
return;
}
checkFunction(functionComplexity, (0, location_js_1.getMainFunctionTokenLocation)(node, node.parent, context));
}
function visitIfStatement(ifStatement) {
const { parent } = ifStatement;
const { loc: ifLoc } = (0, location_js_1.getFirstToken)(ifStatement, context);
// if the current `if` statement is `else if`, do not count it in structural complexity
if ((0, ast_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, ast_js_1.isIfStatement)(ifStatement.alternate)) {
nestingNodes.add(ifStatement.alternate);
const elseTokenLoc = (0, location_js_1.getFirstTokenAfter)(ifStatement.consequent, context).loc;
addComplexity(elseTokenLoc);
}
}
function visitLoop(loop) {
addStructuralComplexity((0, location_js_1.getFirstToken)(loop, context).loc);
nestingNodes.add(loop.body);
}
function visitSwitchStatement(switchStatement) {
addStructuralComplexity((0, location_js_1.getFirstToken)(switchStatement, context).loc);
for (const switchCase of switchStatement.cases) {
nestingNodes.add(switchCase);
}
}
function visitContinueOrBreakStatement(statement) {
if (statement.label) {
addComplexity((0, location_js_1.getFirstToken)(statement, context).loc);
}
}
function visitCatchClause(catchClause) {
addStructuralComplexity((0, location_js_1.getFirstToken)(catchClause, context).loc);
nestingNodes.add(catchClause.body);
}
function visitConditionalExpression(conditionalExpression) {
const questionTokenLoc = (0, location_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, ast_js_1.isArrowFunctionExpression)(node) && node.id) {
return checkFirstLetter(node.id.name);
}
const { parent } = node;
if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
return checkFirstLetter(parent.id.name);
}
return false;
}
function visitLogicalExpression(logicalExpression) {
const jsxShortCircuitNodes = (0, jsx_js_1.getJsxShortCircuitNodes)(logicalExpression);
if (jsxShortCircuitNodes != null) {
for (const node of jsxShortCircuitNodes) {
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?.operator !== current.operator) {
const operatorTokenLoc = (0, location_js_1.getFirstTokenAfter)(current.left, context).loc;
addComplexity(operatorTokenLoc);
}
previous = current;
}
}
}
function flattenLogicalExpression(node) {
if ((0, ast_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 = (0, collection_js_1.last)(functionOwnControlFlowNesting) + 1;
(0, collection_js_1.last)(functionOwnComplexity).push({
complexity: addedWithoutFunctionNesting,
location,
});
}
}
function addComplexity(location) {
const complexityPoint = { complexity: 1, location };
if (functionOwnComplexity.length > 0) {
(0, collection_js_1.last)(functionOwnComplexity).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, location_js_1.toSecondaryLocation)({ loc: location }, message);
});
(0, location_js_1.report)(context, {
messageId: 'refactorFunction',
message,
data: {
complexityAmount: complexityAmount,
threshold: threshold, //currently typings do not accept number
},
loc,
}, secondaryLocations, complexityAmount - threshold);
}
}
},
};