DEV Community

José Thomaz
José Thomaz

Posted on • Edited on

Measuring your code complexity

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);
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.

  2. 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.

 

Github repository

TS Cyclomatic Complexity

Top comments (6)

Collapse
 
tgmarinhodev profile image
Thiago Marinho • Edited

nice, will be good a vs code extension for it ;)

Collapse
 
htho profile image
Hauke T.

There is CodeMetrics which resembles cyclomatic copmlexity.

Collapse
 
sherrydays profile image
Sherry Day

Thanks for this!

Are there any industry standards or guidelines to help developers aim for an optimal level of complexity?

Collapse
 
htho profile image
Hauke T.

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.

Collapse
 
josethz00 profile image
José Thomaz

agreed

Collapse
 
josethz00 profile image
José Thomaz

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