UNPKG

eslint-plugin-sonarjs

Version:
257 lines (256 loc) 11.5 kB
"use strict"; /* * SonarQube JavaScript Plugin * Copyright (C) 2011-2024 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 Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const helpers_1 = require("../helpers"); const meta_1 = require("./meta"); const DEFAULT_THRESHOLD = 15; const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.'; exports.rule = { meta: (0, helpers_1.generateMeta)(meta_1.meta, { messages: { refactorFunction: message, fileComplexity: '{{complexityAmount}}', }, schema: meta_1.schema, }, true), create(context) { /** Complexity threshold */ const threshold = context.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: 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, helpers_1.getMainFunctionTokenLocation)(node, node.parent, context)); } function visitIfStatement(ifStatement) { const { parent } = ifStatement; const { loc: ifLoc } = (0, helpers_1.getFirstToken)(ifStatement, context); // if the current `if` statement is `else if`, do not count it in structural complexity if ((0, helpers_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, helpers_1.isIfStatement)(ifStatement.alternate)) { scopes[scopes.length - 1].nestingNodes.add(ifStatement.alternate); const elseTokenLoc = (0, helpers_1.getFirstTokenAfter)(ifStatement.consequent, context).loc; addComplexity(elseTokenLoc); } } function visitLoop(loop) { addStructuralComplexity((0, helpers_1.getFirstToken)(loop, context).loc); scopes[scopes.length - 1].nestingNodes.add(loop.body); } function visitSwitchStatement(switchStatement) { addStructuralComplexity((0, helpers_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, helpers_1.getFirstToken)(statement, context).loc); } } function visitCatchClause(catchClause) { addStructuralComplexity((0, helpers_1.getFirstToken)(catchClause, context).loc); scopes[scopes.length - 1].nestingNodes.add(catchClause.body); } function visitConditionalExpression(conditionalExpression) { const questionTokenLoc = (0, helpers_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, helpers_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, helpers_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.sourceCode.getText(parent.left) === context.sourceCode.getText(left)); default: return false; } } function flattenLogicalExpression(node) { if ((0, helpers_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, helpers_1.toSecondaryLocation)({ loc: location }, message); }); (0, helpers_1.report)(context, { messageId: 'refactorFunction', message, data: { complexityAmount: complexityAmount, threshold: threshold, //currently typings do not accept number }, loc, }, secondaryLocations, complexityAmount - threshold); } } }, };