UNPKG

eslint-plugin-sonarjs

Version:
370 lines (369 loc) 16.5 kB
"use strict"; /* * SonarQube JavaScript Plugin * Copyright (C) 2011-2025 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 Sonar Source-Available License Version 1, as published by SonarSource SA. * * 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 Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const index_js_1 = require("../helpers/index.js"); const meta_js_1 = require("./meta.js"); const DEFAULT_THRESHOLD = 15; const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.'; exports.rule = { meta: (0, index_js_1.generateMeta)(meta_js_1.meta, { messages: { refactorFunction: message, fileComplexity: '{{complexityAmount}}', }, schema: meta_js_1.schema, }, true), create(context) { /** Complexity threshold */ const thresholdOption = context.options[0]; const threshold = typeof thresholdOption === 'number' ? thresholdOption : DEFAULT_THRESHOLD; /** Indicator if the file complexity should be reported */ 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 = []; /** 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'(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); 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; secondLevelFunctions.forEach(secondLevelFunction => { totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested); }); fileComplexity += totalComplexity.reduce((acc, cur) => acc + cur.complexity, 0); } else { fileComplexity += topLevelOwnComplexity.reduce((acc, cur) => acc + cur.complexity, 0); secondLevelFunctions.forEach(secondLevelFunction => { 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, index_js_1.getMainFunctionTokenLocation)(node, node.parent, context), }); } if (isFileComplexity) { return; } checkFunction(functionComplexity, (0, index_js_1.getMainFunctionTokenLocation)(node, node.parent, context)); } function visitIfStatement(ifStatement) { const { parent } = ifStatement; const { loc: ifLoc } = (0, index_js_1.getFirstToken)(ifStatement, context); // if the current `if` statement is `else if`, do not count it in structural complexity if ((0, index_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, index_js_1.isIfStatement)(ifStatement.alternate)) { nestingNodes.add(ifStatement.alternate); const elseTokenLoc = (0, index_js_1.getFirstTokenAfter)(ifStatement.consequent, context).loc; addComplexity(elseTokenLoc); } } function visitLoop(loop) { addStructuralComplexity((0, index_js_1.getFirstToken)(loop, context).loc); nestingNodes.add(loop.body); } function visitSwitchStatement(switchStatement) { addStructuralComplexity((0, index_js_1.getFirstToken)(switchStatement, context).loc); for (const switchCase of switchStatement.cases) { nestingNodes.add(switchCase); } } function visitContinueOrBreakStatement(statement) { if (statement.label) { addComplexity((0, index_js_1.getFirstToken)(statement, context).loc); } } function visitCatchClause(catchClause) { addStructuralComplexity((0, index_js_1.getFirstToken)(catchClause, context).loc); nestingNodes.add(catchClause.body); } function visitConditionalExpression(conditionalExpression) { const questionTokenLoc = (0, index_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, index_js_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) { const jsxShortCircuitNodes = (0, index_js_1.getJsxShortCircuitNodes)(logicalExpression); if (jsxShortCircuitNodes != null) { jsxShortCircuitNodes.forEach(node => 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 || previous.operator !== current.operator)) { const operatorTokenLoc = (0, index_js_1.getFirstTokenAfter)(current.left, context).loc; addComplexity(operatorTokenLoc); } previous = current; } } } function flattenLogicalExpression(node) { if ((0, index_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 = functionOwnControlFlowNesting[functionOwnControlFlowNesting.length - 1] + 1; functionOwnComplexity[functionOwnComplexity.length - 1].push({ complexity: addedWithoutFunctionNesting, location, }); } } function addComplexity(location) { const complexityPoint = { complexity: 1, location }; if (functionOwnComplexity.length > 0) { functionOwnComplexity[functionOwnComplexity.length - 1].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, index_js_1.toSecondaryLocation)({ loc: location }, message); }); (0, index_js_1.report)(context, { messageId: 'refactorFunction', message, data: { complexityAmount: complexityAmount, threshold: threshold, //currently typings do not accept number }, loc, }, secondaryLocations, complexityAmount - threshold); } } }, };