Skip to content

Metrics

codaviz computes four base metrics. Two are per-function, two are per-module. Packages get transitive aggregates of both.

Metric Scope Meaning Direction
Cyclomatic (CC) function McCabe complexity: decision points + 1. higher = worse
Cognitive (Cog) function SonarSource cognitive complexity. higher = worse
Maintainability index (MI) module radon's 0–100 composite. lower = worse
SLOC module Source lines of code. n/a (sets tile size)

Cyclomatic complexity

The classic McCabe number counts how many independent paths run through a function; one branch or loop adds one.

For Python it comes from the mccabe library, so the numbers match Ruff's C901 rule and python -m mccabe exactly.

For every other language it is computed from the tree-sitter syntax tree: each function-like node starts at 1. Each decision node beneath it (if, loops, case clauses, catch, ternaries, and short-circuiting boolean operators) adds 1. Nested functions are counted separately from their parent; each owns its own decisions.

This is McCabe-family. Numbers are internally consistent within a language, but codaviz does not claim parity with gocyclo, PMD, or ESLint.

Cognitive complexity

SonarSource's cognitive complexity asks a different question than McCabe: how hard is this to follow, against how many paths. It penalises nesting (a condition three levels deep costs more than one at the top) and ignores shorthand that humans read easily, like a chain of else if or a switch.

It is the better signal for "should I refactor this", so codaviz shows both.

Python only. It comes from the cognitive_complexity library. Tree-sitter analyzers leave it unset; a shared SonarSource walker for those languages is planned. In the report, cognitive data that is absent renders as "no data", never as zero.

Maintainability index

radon computes this 0–100 composite of Halstead volume, cyclomatic complexity, and SLOC per module with multi=True and rounds it to two decimals. It is kept because people already recognise the number.

MI radon grade Reading
≥ 20 A Good
10–19 B Getting heavy
< 10 C Hard to maintain

Python only. There is no principled way to compute Halstead volume across arbitrary grammars, so codaviz declines to approximate one. A project with no MI data anywhere (say, a pure TypeScript repo) opens the report on the cyclomatic lens, so the treemap does not open all-grey.

SLOC

SLOC counts source lines of code, excluding blanks.

For Python, codaviz takes it from radon's raw analyzer, which also excludes comments and docstrings.

For tree-sitter languages, non-blank lines, including comment lines. The simplification is marked as such in the code. Refining it means walking comment-node spans per grammar.

SLOC sizes the treemap tiles, so a big tile means a lot of code, though bad code is a separate question.

Derived metrics in the report

The report does not offer raw per-function CC as a ranking axis. It aggregates per module first:

Key Label Definition
mi Maintainability index The module's MI.
cc_max Max cyclomatic Highest CC among the module's functions.
cc_sum Total cyclomatic Sum of CC over the module's functions.
cog_max Max cognitive Highest cognitive score in the module.
cog_sum Total cognitive Sum of cognitive scores in the module.
sloc Lines of code The module's SLOC.

cc_max finds the one function that needs attention. cc_sum finds the module that is heavy overall: a file of thirty moderately branchy functions has no alarming maximum, and plenty of total weight.

A module with no measurable functions gets None for the complexity keys, so the report shows it as "no data" (grey, sorted last) instead of as the best possible score.

Package aggregates

Packages carry transitive aggregates over every descendant module, direct children included:

  • sloc, cc_sum, cog_sum, and the function count are sums.
  • cc_max and cog_max are maxima.
  • mi is a SLOC-weighted mean, since MI has no clean aggregate.

Sums and maxima survive any split of a package into files, which is exactly what makes them meaningful at package level: a complex package stands out even when it has been divided into twenty small modules that each look innocuous.

Circular imports

codaviz detects cycles statically from the AST; no code is imported or executed.

codaviz parses module-level import statements into a directed graph of internal modules, then reports strongly connected components larger than one node using Tarjan's algorithm. Cycles are sorted largest first.

Import resolution follows some statements and ignores others:

  • Followed: top-level import and from ... import, including inside module-level if/try/with blocks, and relative imports (resolved against the module's own package).
  • Ignored: imports inside functions or classes. They do not cause import-time cycles, so moving an import into a function is the standard fix.
  • Ignored: anything under if TYPE_CHECKING:, for the same reason.
  • Ignored: dynamic imports (importlib, __import__).

Resolution favours false negatives over false positives: an import that cannot be matched to a known internal module is dropped, never guessed at. Python only.