UNPKG

eslint-plugin-sonarjs

Version:
298 lines 13 kB
"use strict"; /* * 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