"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 jsx_1 = require("../utils/jsx"); 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 { options } = context; /** Complexity threshold */ const threshold = typeof options[0] === 'number' ? 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, 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, locations_1.getMainFunctionTokenLocation)(node, node.parent, context)); } 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 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, nodes_1.isIfStatement)(ifStatement.alternate)) { scopes[scopes.length - 1].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); scopes[scopes.length - 1].nestingNodes.add(loop.body); } function visitSwitchStatement(switchStatement) { addStructuralComplexity((0, locations_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, locations_1.getFirstToken)(statement, context).loc); } } function visitCatchClause(catchClause) { addStructuralComplexity((0, locations_1.getFirstToken)(catchClause, context).loc); scopes[scopes.length - 1].nestingNodes.add(catchClause.body); } function visitConditionalExpression(conditionalExpression) { const questionTokenLoc = (0, locations_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, jsx_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, locations_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.getSourceCode().getText(parent.left) === context.getSourceCode().getText(left)); default: return false; } } 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 = 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, 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