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_maxandcog_maxare maxima.miis 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
importandfrom ... import, including inside module-levelif/try/withblocks, 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.