Wrote some code and want to know if it's good? If it's testable, maintainable, and "clean"?
How to measure code complexity? 🤔
Evaluating code quality can be subjective, as it often depends on individual contexts, patterns, and rules.
Time complexity and space complexity are two possible ways, but writing a compiler to measure this is a challenging task. Also these are metrics more focused in performance than code complexity itself. So what about cyclomatic complexity?
What is Cyclomatic Complexity?
Cyclomatic complexity is a software metric used to indicate the complexity of a program. It is a quantitative measure of the number of linearly independent paths through a program's source code. It was developed by Thomas J. McCabe, Sr. in 1976.
Cyclomatic complexity is computed using the control-flow graph of the program: the nodes of the graph correspond to indivisible groups of commands of a program, and a directed edge connects two nodes if the second command might be executed immediately after the first command. Cyclomatic complexity may also be applied to individual functions, modules, methods or classes within a program.
Simplifying the definition
Cyclomatic complexity is a way to measure how complex a program is by counting its independent paths.
Independent paths include loops, conditional structures, and other "branches" in your code. If a code segment can lead to a different route or deviation, it's an independent path. Examples include if, else, else if, for, and while.
Fewer independent paths make your code more readable, maintainable, and testable, while also simplifying testing and understanding.
Writing a TypeScript cyclomatic complexity analyzer
Let's use the TypeScript API to write a code to analyze the cyclomatic complexity of a TypeScript function.
import ts from 'typescript';
import fs from 'fs';
const args = process.argv.slice(2);
const fileToRead = args[0];
const fileContent = fs.readFileSync(fileToRead, 'utf8');
const tmpSourceFile = ts.createSourceFile(
'tmp.ts',
fileContent,
ts.ScriptTarget.Latest,
true,
);
let complexity = 1;
/**
* Function to visit each node in the AST recursively
* @param {ts.Node} node - The node to visit
*/
const visitNode = (node: ts.Node) => {
switch (node.kind) {
case ts.SyntaxKind.IfStatement:
case ts.SyntaxKind.ForInStatement:
case ts.SyntaxKind.ForOfStatement:
case ts.SyntaxKind.ForStatement:
case ts.SyntaxKind.WhileStatement:
case ts.SyntaxKind.TryStatement:
case ts.SyntaxKind.CatchClause:
case ts.SyntaxKind.ConditionalExpression:
complexity += 1;
break;
case ts.SyntaxKind.SwitchStatement:
const switchStmt = node as ts.SwitchStatement;
switchStmt.caseBlock.clauses.forEach((clause) => {
if (ts.isCaseClause(clause)) {
// handle only case clausa, because it is not allowed to have a switch inside another
complexity += 1;
}
});
break;
case ts.SyntaxKind.BinaryExpression:
const binaryExpr = node as ts.BinaryExpression;
if (
binaryExpr.operatorToken.kind ==
ts.SyntaxKind.AmpersandAmpersandToken ||
binaryExpr.operatorToken.kind == ts.SyntaxKind.BarBarToken
) {
// if the binary expression token is AND or OR, it is an assertion branch
// so it increases the complexity in +1
complexity += 1;
}
break;
}
ts.forEachChild(node, visitNode);
};
visitNode(tmpSourceFile);
console.log(complexity);
This code basically reads a TypeScript file, and measure the cyclomatic complexity by visiting the AST nodes and adding +1 for each independent path it finds.
Interpreting the results
The code will print the cyclomatic complexity result, so let's interpret the results:
- 1 - 10 Simple procedure, little risk
- 11 - 20 More complex, moderate risk
- 21 - 50 Complex, high risk
- > 50 Untestable code, very high risk
Usage
To run this code you just need to build the TypeScript file and run it using node. For example:
tsc && node dist/main.js
Or just
yarn start
if you are following my configuration
Current limitations
The cyclomatic complexity analyzer presented above has some limitations:
Single function support: The code measures cyclomatic complexity accurately only when the provided file contains a single function. It won't work correctly for classes or files with multiple functions.
TypeScript-specific: The analyzer is designed for TypeScript. If you use another programming language, you will need to reimplement the analyzer using the appropriate language-specific tools and techniques.
Top comments (6)
nice, will be good a vs code extension for it ;)
There is CodeMetrics which resembles cyclomatic copmlexity.
Thanks for this!
Are there any industry standards or guidelines to help developers aim for an optimal level of complexity?
I'd say no. There are no standards. The lower the better.
BUT Never aim for better numbers - aim for better readability.
Your code might have a low (cyclomatic) complexity but still be hard to read.
You dont write code for machines, you write code for (other) humans - this includes your future self.
agreed
I don't know if I understood your question correctly, what do you mean by industry standards? I guess that applying some code patterns to avoid ifs and nested loops helps a lot to achieving low level of code complexity. If you follow SOLID, Object Calisthenics, declarative programming and clean architecture your code will probably have a low complexity