Software Metrics - Cyclomatic Complexity

· 4 min read

Recently while configuring ESLint rules, I revisited the complexity rule. I previously didn’t pay enough attention to this rule and had some blind spots in understanding, so I studied it systematically and documented it here.

Concept

Cyclomatic complexity is also known as conditional complexity or circuitous complexity. It’s a software metric proposed by Thomas J. McCabe Sr. in 1976 to represent program complexity.

Cyclomatic Complexity Calculation

Formula

M = E − N + 2P Where:

  • E is the number of edges in the graph
  • N is the number of nodes in the graph
  • P is the number of connected components [number of programs; for a single program, P is always 1], as shown in the diagram below it’s 1

As shown in the figure, E = 9, N = 8, P = 1, so the cyclomatic complexity is 9 - 8 + (2*1) = 3

The above is the standard formula, but a simple approach is actually number of decision nodes plus one

Node Decision

I personally prefer this direct calculation method as it’s simpler.

Decision Nodes

  • if statements
  • conditional statements, like ?:
  • for statements
  • while statements
  • try statements
  • switch/case statements

Notes

  • Start from 1 and go through the program
  • Encounter the following keywords or similar ones, add 1: if, while, repeat, for, and, or
  • Each case in if-else-if and switch-case statements adds 1

Cyclomatic Complexity Reference Values

Here’s a comparison table of complexity values and risk levels. You can see that 20 is already on the edge of maintainability, while 10 is optimal.

  • 01 to 10 – Minimal Risk, Easy to maintain
  • 11 to 20 – Moderate Risk, Harder to maintain
  • 21 to 50 – High Risk, Candidates for refactoring/redesign
  • Over 50 – Very High Risk

Notes

  • The default value in eslint is 20, while in checkstyle the default value is 10
  • Low cyclomatic complexity doesn’t necessarily mean good code, but if it’s too high, the code is definitely not good and lacks maintainability - this point needs to be clear

Detection Methods

In development, to ensure unified code style, we often use tools like eslint, checkStyle to detect code and report errors for non-compliant standards. These detection tools all support cyclomatic complexity.

  • eslint-rules

{ ‘complexity’: [’error’, { ‘max’: 20 }] } ```

  • checkStyle

    
    <?xml version="1.0"?>
    <!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
    <module name = "Checker">
    <property name="charset" value="UTF-8"/>
    <module name="TreeWalker">
    <module name="CyclomaticComplexity">
    <property name="max" value="10"/>
    </module>
    </module>
    </module>
    

Refactoring Techniques

When facing functions with excessively high complexity values, we need to refactor to solve the problem. How exactly should we handle this?

  • Extract functions to achieve single function responsibility, reducing the complexity of individual functions, and naturally lowering complexity values
  • When switch and if-else are used extensively, consider polymorphism and map mapping to solve multi-branch problems
  • Simplify logical judgments through continuous logical merging and consolidation, sometimes discovering duplicates

ESLint Cyclomatic Complexity Source Code Analysis

Let’s understand the implementation principle through ESLint’s related rule

eslint/lib.rules/complexity.js, link here


       /**
         * Increase the switch complexity in context
         * @param {ASTNode} node node to evaluate
         * @returns {void}
         * @private
         */
        function increaseSwitchComplexity(node) {

            // Avoiding `default`
            if (node.test) {
                increaseComplexity();
            }
        }

        return {
            FunctionDeclaration: startFunction,
            FunctionExpression: startFunction,
            ArrowFunctionExpression: startFunction,
            "FunctionDeclaration:exit": endFunction,
            "FunctionExpression:exit": endFunction,
            "ArrowFunctionExpression:exit": endFunction,

            CatchClause: increaseComplexity,
            ConditionalExpression: increaseComplexity,
            LogicalExpression: increaseComplexity,
            ForStatement: increaseComplexity,
            ForInStatement: increaseComplexity,
            ForOfStatement: increaseComplexity,
            IfStatement: increaseComplexity,
            SwitchCase: increaseSwitchComplexity,
            WhileStatement: increaseComplexity,
            DoWhileStatement: increaseComplexity
        };
        

The calculation method is to find these decision nodes based on the AST tree and perform corresponding complexity calculations. As shown above, default in switch/case doesn’t count.

Final Thoughts

  • I remember reading in a book a memorable line: code readability is for humans. For machines running code, you can theoretically write it any way, but the problem is that humans need to read and understand it, so readability and maintainability become particularly important. So, pay attention to cyclomatic complexity - it’s a good metric to urge us to write readable code.
  • The significance of cyclomatic complexity is to catch defects before they become defects.

References

Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover