Understanding & Linting for Cyclomatic Complexity
Background
There are many ways to measure the complexity of a piece of software: number of lines, Big O,
and so on. Cyclomatic Complexity attempts to do this by enumerating all linearly
independent paths through a segment of code. Linearly independent means that each path has at
least one edge that is not in another path (if viewing the code as a graph).
McCabe defined CC using graph theory as:
M = E - N + 2P
Where:
E
= edges in the graphN
= nodes in the graphP
= number of connected components (think modules) of a program
If a program had no control flow statements (if, switch, goto, etc.), then the CC would be 1. If
a program has 1 if statement, then its CC is 2.
A program that has 50 lines consisting of 25 consecutive "if/then" constructs will have over 33
million (2^{25}
) distinct paths through the program. There's no way all those paths are
getting tested!
Let's analyze the following snippet of Python code:
def cc_example(b, c):
a = 0
if b > c:
a = b
else:
a = c
print(a, b, c)
What is the cyclomatic complexity of this method?
We can draw the graph of this method:
The graph has seven nodes and seven edges, so its cyclomatic complexity is:
7 - 7 + 2 = 2
Suggested Levels of CC
There doesn't seem to be an agreed-upon threshold for what amount of CC is too much, though
generally the higher your code's CC, the harder it is to maintain. This naturally grows with the
number of lines in your software.
According to a random StackExchange post, the following is a rubric for CC scores and their
meaning:
- < 10: Easy to maintain
- 11-20: Harder to maintain
- 21+: Candidates for refactoring/redesign
Other websites have slightly different tolerances, but I like the StackExchange metric for
methods.
Pros of Using CC in Your Project
- Helps write code that is easier to read & reason about
- Easier to write unit tests (fewer paths that need to be explored)
- Easy to calculate with plugins to popular linters
ESLint Settings for Cyclomatic Complexity
If you're using JavaScript or TypeScript in your project, you've already got a Cyclomatic
Complexity checker at hand. You just need to configure it to run when you're linting.
The following additions to your .eslintrc
file will activate the complexity rules and several
other related checks. The complexity
rule below measures Cyclomatic Complexity specifically.
Here, it is set to raise a linting error if the complexity of a method is above 5.
Many of the other rules measure things like clause depth and the number of lines in a method.
{
"rules": {
"complexity": ["error", 5],
"array-callback-return": "error",
"max-statements": ["error", 20],
"max-statements-per-line": ["error", { "max": 1 }],
"max-nested-callbacks": ["error", 3],
"max-depth": ["error", { "max": 3 }],
"max-lines": ["error", 200],
"no-return-assign": "error",
"no-param-reassign": "error"
}
}