cognitive-complexity.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. "use strict";
  2. /*
  3. * eslint-plugin-sonarjs
  4. * Copyright (C) 2018-2021 SonarSource SA
  5. * mailto:info AT sonarsource DOT com
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU Lesser General Public
  9. * License as published by the Free Software Foundation; either
  10. * version 3 of the License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. * Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with this program; if not, write to the Free Software Foundation,
  19. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. */
  21. // https://sonarsource.github.io/rspec/#/rspec/S3776
  22. const nodes_1 = require("../utils/nodes");
  23. const locations_1 = require("../utils/locations");
  24. const docs_url_1 = require("../utils/docs-url");
  25. const DEFAULT_THRESHOLD = 15;
  26. const message = 'Refactor this function to reduce its Cognitive Complexity from {{complexityAmount}} to the {{threshold}} allowed.';
  27. const rule = {
  28. meta: {
  29. messages: {
  30. refactorFunction: message,
  31. sonarRuntime: '{{sonarRuntimeData}}',
  32. fileComplexity: '{{complexityAmount}}',
  33. },
  34. type: 'suggestion',
  35. docs: {
  36. description: 'Cognitive Complexity of functions should not be too high',
  37. recommended: 'error',
  38. url: (0, docs_url_1.default)(__filename),
  39. },
  40. schema: [
  41. { type: 'integer', minimum: 0 },
  42. {
  43. // internal parameter
  44. enum: ['sonar-runtime', 'metric'],
  45. },
  46. ],
  47. },
  48. create(context) {
  49. const threshold = typeof context.options[0] === 'number' ? context.options[0] : DEFAULT_THRESHOLD;
  50. const isFileComplexity = context.options.includes('metric');
  51. /** Complexity of the file */
  52. let fileComplexity = 0;
  53. /** Complexity of the current function if it is *not* considered nested to the first level function */
  54. let complexityIfNotNested = [];
  55. /** Complexity of the current function if it is considered nested to the first level function */
  56. let complexityIfNested = [];
  57. /** Current nesting level (number of enclosing control flow statements and functions) */
  58. let nesting = 0;
  59. /** Indicator if the current top level function has a structural (generated by control flow statements) complexity */
  60. let topLevelHasStructuralComplexity = false;
  61. /** Indicator if the current top level function is React functional component */
  62. const reactFunctionalComponent = {
  63. nameStartsWithCapital: false,
  64. returnsJsx: false,
  65. isConfirmed() {
  66. return this.nameStartsWithCapital && this.returnsJsx;
  67. },
  68. init(node) {
  69. this.nameStartsWithCapital = nameStartsWithCapital(node);
  70. this.returnsJsx = false;
  71. },
  72. };
  73. /** Own (not including nested functions) complexity of the current top function */
  74. let topLevelOwnComplexity = [];
  75. /** Nodes that should increase nesting level */
  76. const nestingNodes = new Set();
  77. /** Set of already considered (with already computed complexity) logical expressions */
  78. const consideredLogicalExpressions = new Set();
  79. /** Stack of enclosing functions */
  80. const enclosingFunctions = [];
  81. let secondLevelFunctions = [];
  82. return {
  83. ':function': (node) => {
  84. onEnterFunction(node);
  85. },
  86. ':function:exit'(node) {
  87. onLeaveFunction(node);
  88. },
  89. '*'(node) {
  90. if (nestingNodes.has(node)) {
  91. nesting++;
  92. }
  93. },
  94. '*:exit'(node) {
  95. if (nestingNodes.has(node)) {
  96. nesting--;
  97. nestingNodes.delete(node);
  98. }
  99. },
  100. Program() {
  101. fileComplexity = 0;
  102. },
  103. 'Program:exit'(node) {
  104. if (isFileComplexity) {
  105. // value from the message will be saved in SonarQube as file complexity metric
  106. context.report({
  107. node,
  108. messageId: 'fileComplexity',
  109. data: { complexityAmount: fileComplexity },
  110. });
  111. }
  112. },
  113. IfStatement(node) {
  114. visitIfStatement(node);
  115. },
  116. ForStatement(node) {
  117. visitLoop(node);
  118. },
  119. ForInStatement(node) {
  120. visitLoop(node);
  121. },
  122. ForOfStatement(node) {
  123. visitLoop(node);
  124. },
  125. DoWhileStatement(node) {
  126. visitLoop(node);
  127. },
  128. WhileStatement(node) {
  129. visitLoop(node);
  130. },
  131. SwitchStatement(node) {
  132. visitSwitchStatement(node);
  133. },
  134. ContinueStatement(node) {
  135. visitContinueOrBreakStatement(node);
  136. },
  137. BreakStatement(node) {
  138. visitContinueOrBreakStatement(node);
  139. },
  140. CatchClause(node) {
  141. visitCatchClause(node);
  142. },
  143. LogicalExpression(node) {
  144. visitLogicalExpression(node);
  145. },
  146. ConditionalExpression(node) {
  147. visitConditionalExpression(node);
  148. },
  149. ReturnStatement(node) {
  150. visitReturnStatement(node);
  151. },
  152. };
  153. function onEnterFunction(node) {
  154. if (enclosingFunctions.length === 0) {
  155. // top level function
  156. topLevelHasStructuralComplexity = false;
  157. reactFunctionalComponent.init(node);
  158. topLevelOwnComplexity = [];
  159. secondLevelFunctions = [];
  160. }
  161. else if (enclosingFunctions.length === 1) {
  162. // second level function
  163. complexityIfNotNested = [];
  164. complexityIfNested = [];
  165. }
  166. else {
  167. nesting++;
  168. nestingNodes.add(node);
  169. }
  170. enclosingFunctions.push(node);
  171. }
  172. function onLeaveFunction(node) {
  173. enclosingFunctions.pop();
  174. if (enclosingFunctions.length === 0) {
  175. // top level function
  176. if (topLevelHasStructuralComplexity && !reactFunctionalComponent.isConfirmed()) {
  177. let totalComplexity = topLevelOwnComplexity;
  178. secondLevelFunctions.forEach(secondLevelFunction => {
  179. totalComplexity = totalComplexity.concat(secondLevelFunction.complexityIfNested);
  180. });
  181. checkFunction(totalComplexity, (0, locations_1.getMainFunctionTokenLocation)(node, node.parent, context));
  182. }
  183. else {
  184. checkFunction(topLevelOwnComplexity, (0, locations_1.getMainFunctionTokenLocation)(node, node.parent, context));
  185. secondLevelFunctions.forEach(secondLevelFunction => {
  186. checkFunction(secondLevelFunction.complexityIfThisSecondaryIsTopLevel, (0, locations_1.getMainFunctionTokenLocation)(secondLevelFunction.node, secondLevelFunction.parent, context));
  187. });
  188. }
  189. }
  190. else if (enclosingFunctions.length === 1) {
  191. // second level function
  192. secondLevelFunctions.push({
  193. node,
  194. parent: node.parent,
  195. complexityIfNested,
  196. complexityIfThisSecondaryIsTopLevel: complexityIfNotNested,
  197. loc: (0, locations_1.getMainFunctionTokenLocation)(node, node.parent, context),
  198. });
  199. }
  200. else {
  201. // complexity of third+ level functions is computed in their parent functions
  202. // so we never raise an issue for them
  203. }
  204. }
  205. function visitIfStatement(ifStatement) {
  206. const { parent } = ifStatement;
  207. const { loc: ifLoc } = (0, locations_1.getFirstToken)(ifStatement, context);
  208. // if the current `if` statement is `else if`, do not count it in structural complexity
  209. if ((0, nodes_1.isIfStatement)(parent) && parent.alternate === ifStatement) {
  210. addComplexity(ifLoc);
  211. }
  212. else {
  213. addStructuralComplexity(ifLoc);
  214. }
  215. // always increase nesting level inside `then` statement
  216. nestingNodes.add(ifStatement.consequent);
  217. // if `else` branch is not `else if` then
  218. // - increase nesting level inside `else` statement
  219. // - add +1 complexity
  220. if (ifStatement.alternate && !(0, nodes_1.isIfStatement)(ifStatement.alternate)) {
  221. nestingNodes.add(ifStatement.alternate);
  222. const elseTokenLoc = (0, locations_1.getFirstTokenAfter)(ifStatement.consequent, context).loc;
  223. addComplexity(elseTokenLoc);
  224. }
  225. }
  226. function visitLoop(loop) {
  227. addStructuralComplexity((0, locations_1.getFirstToken)(loop, context).loc);
  228. nestingNodes.add(loop.body);
  229. }
  230. function visitSwitchStatement(switchStatement) {
  231. addStructuralComplexity((0, locations_1.getFirstToken)(switchStatement, context).loc);
  232. for (const switchCase of switchStatement.cases) {
  233. nestingNodes.add(switchCase);
  234. }
  235. }
  236. function visitContinueOrBreakStatement(statement) {
  237. if (statement.label) {
  238. addComplexity((0, locations_1.getFirstToken)(statement, context).loc);
  239. }
  240. }
  241. function visitCatchClause(catchClause) {
  242. addStructuralComplexity((0, locations_1.getFirstToken)(catchClause, context).loc);
  243. nestingNodes.add(catchClause.body);
  244. }
  245. function visitConditionalExpression(conditionalExpression) {
  246. const questionTokenLoc = (0, locations_1.getFirstTokenAfter)(conditionalExpression.test, context).loc;
  247. addStructuralComplexity(questionTokenLoc);
  248. nestingNodes.add(conditionalExpression.consequent);
  249. nestingNodes.add(conditionalExpression.alternate);
  250. }
  251. function visitReturnStatement({ argument }) {
  252. // top level function
  253. if (enclosingFunctions.length === 1 &&
  254. argument &&
  255. ['JSXElement', 'JSXFragment'].includes(argument.type)) {
  256. reactFunctionalComponent.returnsJsx = true;
  257. }
  258. }
  259. function nameStartsWithCapital(node) {
  260. const checkFirstLetter = (name) => {
  261. const firstLetter = name[0];
  262. return firstLetter === firstLetter.toUpperCase();
  263. };
  264. if (!(0, nodes_1.isArrowFunctionExpression)(node) && node.id) {
  265. return checkFirstLetter(node.id.name);
  266. }
  267. const { parent } = node;
  268. if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
  269. return checkFirstLetter(parent.id.name);
  270. }
  271. return false;
  272. }
  273. function visitLogicalExpression(logicalExpression) {
  274. if (!consideredLogicalExpressions.has(logicalExpression)) {
  275. const flattenedLogicalExpressions = flattenLogicalExpression(logicalExpression);
  276. let previous;
  277. for (const current of flattenedLogicalExpressions) {
  278. if (!previous || previous.operator !== current.operator) {
  279. const operatorTokenLoc = (0, locations_1.getFirstTokenAfter)(logicalExpression.left, context).loc;
  280. addComplexity(operatorTokenLoc);
  281. }
  282. previous = current;
  283. }
  284. }
  285. }
  286. function flattenLogicalExpression(node) {
  287. if ((0, nodes_1.isLogicalExpression)(node)) {
  288. consideredLogicalExpressions.add(node);
  289. return [
  290. ...flattenLogicalExpression(node.left),
  291. node,
  292. ...flattenLogicalExpression(node.right),
  293. ];
  294. }
  295. return [];
  296. }
  297. function addStructuralComplexity(location) {
  298. const added = nesting + 1;
  299. const complexityPoint = { complexity: added, location };
  300. if (enclosingFunctions.length === 0) {
  301. // top level scope
  302. fileComplexity += added;
  303. }
  304. else if (enclosingFunctions.length === 1) {
  305. // top level function
  306. topLevelHasStructuralComplexity = true;
  307. topLevelOwnComplexity.push(complexityPoint);
  308. }
  309. else {
  310. // second+ level function
  311. complexityIfNested.push({ complexity: added + 1, location });
  312. complexityIfNotNested.push(complexityPoint);
  313. }
  314. }
  315. function addComplexity(location) {
  316. const complexityPoint = { complexity: 1, location };
  317. if (enclosingFunctions.length === 0) {
  318. // top level scope
  319. fileComplexity += 1;
  320. }
  321. else if (enclosingFunctions.length === 1) {
  322. // top level function
  323. topLevelOwnComplexity.push(complexityPoint);
  324. }
  325. else {
  326. // second+ level function
  327. complexityIfNested.push(complexityPoint);
  328. complexityIfNotNested.push(complexityPoint);
  329. }
  330. }
  331. function checkFunction(complexity = [], loc) {
  332. const complexityAmount = complexity.reduce((acc, cur) => acc + cur.complexity, 0);
  333. fileComplexity += complexityAmount;
  334. if (isFileComplexity) {
  335. return;
  336. }
  337. if (complexityAmount > threshold) {
  338. const secondaryLocations = complexity.map(complexityPoint => {
  339. const { complexity, location } = complexityPoint;
  340. const message = complexity === 1 ? '+1' : `+${complexity} (incl. ${complexity - 1} for nesting)`;
  341. return (0, locations_1.issueLocation)(location, undefined, message);
  342. });
  343. (0, locations_1.report)(context, {
  344. messageId: 'refactorFunction',
  345. data: {
  346. complexityAmount,
  347. threshold,
  348. },
  349. loc,
  350. }, secondaryLocations, message, complexityAmount - threshold);
  351. }
  352. }
  353. },
  354. };
  355. module.exports = rule;
  356. //# sourceMappingURL=cognitive-complexity.js.map