Software Metrics - Cyclomatic Complexity
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 asconditional complexity
orcircuitous complexity
. It’s a software metric proposed by Thomas J. McCabe Sr. in1976
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.