Measuring your code complexity
Tags: typescript, programming, computerscience, tooling
Published: 2023-04-05 14:42:24

Wrote some code and want to know if it's good? If it's testable, maintainable, and "clean"?

## How to measure code complexity? :thinking:

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**?

&nbsp;

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

&nbsp;

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

```ts
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.

&nbsp;

## 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

&nbsp;

## 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](https://github.com/josethz00/ts-cyclomatic-complexity/blob/main/package.json)

&nbsp;

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

&nbsp;

## Github repository

[TS Cyclomatic Complexity](https://github.com/josethz00/ts-cyclomatic-complexity)