"use strict"; /* * eslint-plugin-sonarjs * Copyright (C) 2018-2021 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 const nodes_1 = require("../utils/nodes"); const locations_1 = require("../utils/locations"); const docs_url_1 = require("../utils/docs-url"); const DEFAULT_THRESHOLD = 15; const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.'; const rule = { meta: { messages: { refactorFunction: message, sonarRuntime: '{{sonarRuntimeData}}', fileComplexity: '{{complexityAmount}}', }, type: 'suggestion', docs: { description: 'Cognitive Complexity of functions should not be too high', recommended: 'error', url: (0, docs_url_1.default)(__filename), }, schema: [ { type: 'integer', minimum: 0 }, { // internal parameter enum: ['sonar-runtime', 'metric'], }, ], }, create(context) { const threshold = typeof context.options[0] === 'number' ? context.options[0] : DEFAULT_THRESHOLD; 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 = []; 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) { // 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); } function onLeaveFunction(node) { enclosingFunctions.pop(); if (enclosingFunctions.length === 0) { // top level function if (topLevelHasStructuralComplexity && !reactFunctionalComponent.isConfirmed()) { let totalComplexity = topLevelOwnComplexity; secondLevelFunctions.forEach(secondLevelFunction => { totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested); }); checkFunction(totalComplexity, (0, locations_1.getMainFunctionTokenLocation)(node, node.parent, context)); } else { checkFunction(topLevelOwnComplexity, (0, locations_1.getMainFunctionTokenLocation)(node, node.parent, context)); secondLevelFunctions.forEach(secondLevelFunction => { checkFunction(secondLevelFunction.complexityIfThisSecondaryIsTopLevel, (0, locations_1.getMainFunctionTokenLocation)(secondLevelFunction.node, secondLevelFunction.parent, context)); }); } } else if (enclosingFunctions.length === 1) { // second level function secondLevelFunctions.push({ node, parent: node.parent, complexityIfNested, complexityIfThisSecondaryIsTopLevel: complexityIfNotNested, loc: (0, locations_1.getMainFunctionTokenLocation)(node, node.parent, 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 } = ifStatement; const { loc: ifLoc } = (0, locations_1.getFirstToken)(ifStatement, context); // if the current `if` statement is `else if`, do not count it in structural complexity if ((0, 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 && !(0, nodes_1.isIfStatement)(ifStatement.alternate)) { nestingNodes.add(ifStatement.alternate); const elseTokenLoc = (0, locations_1.getFirstTokenAfter)(ifStatement.consequent, context).loc; addComplexity(elseTokenLoc); } } function visitLoop(loop) { addStructuralComplexity((0, locations_1.getFirstToken)(loop, context).loc); nestingNodes.add(loop.body); } function visitSwitchStatement(switchStatement) { addStructuralComplexity((0, locations_1.getFirstToken)(switchStatement, context).loc); for (const switchCase of switchStatement.cases) { nestingNodes.add(switchCase); } } function visitContinueOrBreakStatement(statement) { if (statement.label) { addComplexity((0, locations_1.getFirstToken)(statement, context).loc); } } function visitCatchClause(catchClause) { addStructuralComplexity((0, locations_1.getFirstToken)(catchClause, context).loc); nestingNodes.add(catchClause.body); } function visitConditionalExpression(conditionalExpression) { const questionTokenLoc = (0, locations_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, nodes_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) { if (!consideredLogicalExpressions.has(logicalExpression)) { const flattenedLogicalExpressions = flattenLogicalExpression(logicalExpression); let previous; for (const current of flattenedLogicalExpressions) { if (!previous || previous.operator !== current.operator) { const operatorTokenLoc = (0, locations_1.getFirstTokenAfter)(logicalExpression.left, context).loc; addComplexity(operatorTokenLoc); } previous = current; } } } function flattenLogicalExpression(node) { if ((0, 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 (0, locations_1.issueLocation)(location, undefined, message); }); (0, locations_1.report)(context, { messageId: 'refactorFunction', data: { complexityAmount, threshold, }, loc, }, secondaryLocations, message, complexityAmount - threshold); } } }, }; module.exports = rule; //# sourceMappingURL=cognitive-complexity.js.map