complexity.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * @fileoverview Counts the cyclomatic complexity of each function of the script. See http://en.wikipedia.org/wiki/Cyclomatic_complexity.
  3. * Counts the number of if, conditional, for, while, try, switch/case,
  4. * @author Patrick Brosset
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const astUtils = require("./utils/ast-utils");
  11. const { upperCaseFirst } = require("../shared/string-utils");
  12. //------------------------------------------------------------------------------
  13. // Rule Definition
  14. //------------------------------------------------------------------------------
  15. /** @type {import('../shared/types').Rule} */
  16. module.exports = {
  17. meta: {
  18. type: "suggestion",
  19. docs: {
  20. description: "Enforce a maximum cyclomatic complexity allowed in a program",
  21. recommended: false,
  22. url: "https://eslint.org/docs/latest/rules/complexity"
  23. },
  24. schema: [
  25. {
  26. oneOf: [
  27. {
  28. type: "integer",
  29. minimum: 0
  30. },
  31. {
  32. type: "object",
  33. properties: {
  34. maximum: {
  35. type: "integer",
  36. minimum: 0
  37. },
  38. max: {
  39. type: "integer",
  40. minimum: 0
  41. },
  42. variant: {
  43. enum: ["classic", "modified"]
  44. }
  45. },
  46. additionalProperties: false
  47. }
  48. ]
  49. }
  50. ],
  51. messages: {
  52. complex: "{{name}} has a complexity of {{complexity}}. Maximum allowed is {{max}}."
  53. }
  54. },
  55. create(context) {
  56. const option = context.options[0];
  57. let THRESHOLD = 20;
  58. let VARIANT = "classic";
  59. if (typeof option === "object") {
  60. if (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max")) {
  61. THRESHOLD = option.maximum || option.max;
  62. }
  63. if (Object.hasOwn(option, "variant")) {
  64. VARIANT = option.variant;
  65. }
  66. } else if (typeof option === "number") {
  67. THRESHOLD = option;
  68. }
  69. const IS_MODIFIED_COMPLEXITY = VARIANT === "modified";
  70. //--------------------------------------------------------------------------
  71. // Helpers
  72. //--------------------------------------------------------------------------
  73. // Using a stack to store complexity per code path
  74. const complexities = [];
  75. /**
  76. * Increase the complexity of the code path in context
  77. * @returns {void}
  78. * @private
  79. */
  80. function increaseComplexity() {
  81. complexities[complexities.length - 1]++;
  82. }
  83. //--------------------------------------------------------------------------
  84. // Public API
  85. //--------------------------------------------------------------------------
  86. return {
  87. onCodePathStart() {
  88. // The initial complexity is 1, representing one execution path in the CodePath
  89. complexities.push(1);
  90. },
  91. // Each branching in the code adds 1 to the complexity
  92. CatchClause: increaseComplexity,
  93. ConditionalExpression: increaseComplexity,
  94. LogicalExpression: increaseComplexity,
  95. ForStatement: increaseComplexity,
  96. ForInStatement: increaseComplexity,
  97. ForOfStatement: increaseComplexity,
  98. IfStatement: increaseComplexity,
  99. WhileStatement: increaseComplexity,
  100. DoWhileStatement: increaseComplexity,
  101. AssignmentPattern: increaseComplexity,
  102. // Avoid `default`
  103. "SwitchCase[test]": () => IS_MODIFIED_COMPLEXITY || increaseComplexity(),
  104. SwitchStatement: () => IS_MODIFIED_COMPLEXITY && increaseComplexity(),
  105. // Logical assignment operators have short-circuiting behavior
  106. AssignmentExpression(node) {
  107. if (astUtils.isLogicalAssignmentOperator(node.operator)) {
  108. increaseComplexity();
  109. }
  110. },
  111. MemberExpression(node) {
  112. if (node.optional === true) {
  113. increaseComplexity();
  114. }
  115. },
  116. CallExpression(node) {
  117. if (node.optional === true) {
  118. increaseComplexity();
  119. }
  120. },
  121. onCodePathEnd(codePath, node) {
  122. const complexity = complexities.pop();
  123. /*
  124. * This rule only evaluates complexity of functions, so "program" is excluded.
  125. * Class field initializers and class static blocks are implicit functions. Therefore,
  126. * they shouldn't contribute to the enclosing function's complexity, but their
  127. * own complexity should be evaluated.
  128. */
  129. if (
  130. codePath.origin !== "function" &&
  131. codePath.origin !== "class-field-initializer" &&
  132. codePath.origin !== "class-static-block"
  133. ) {
  134. return;
  135. }
  136. if (complexity > THRESHOLD) {
  137. let name;
  138. if (codePath.origin === "class-field-initializer") {
  139. name = "class field initializer";
  140. } else if (codePath.origin === "class-static-block") {
  141. name = "class static block";
  142. } else {
  143. name = astUtils.getFunctionNameWithKind(node);
  144. }
  145. context.report({
  146. node,
  147. messageId: "complex",
  148. data: {
  149. name: upperCaseFirst(name),
  150. complexity,
  151. max: THRESHOLD
  152. }
  153. });
  154. }
  155. }
  156. };
  157. }
  158. };