123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- "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
|