Skip to content

Quickstart

CLI

Here is demo.py, four lines, one branch:

def classify(value, limit):
    if value > limit and value < 100:
        return "high"
    return "low"

codecaliper demo.py measures it and prints a JSON report. The default report carries every metric family plus the 25-feature readability vector, so it runs to a few hundred lines. Restricting it to two metrics makes it small enough to print whole, and nothing below is elided:

codecaliper demo.py --metrics cyclomatic,cognitive --no-readability
{
  "path": "demo.py",
  "parse_ok": true,
  "file_metrics": [
    {
      "metric": "cyclomatic",
      "value": 3,
      "rulings": [
        "CC-ALL-0001",
        "CC-PY-0001",
        "CC-PY-0002",
        "CC-PY-0003",
        "CC-PY-0004",
        "CC-PY-0005",
        "CC-PY-0006",
        "CC-PY-0007",
        "CC-PY-0008"
      ],
      "derived_from": [],
      "trace": [],
      "diagnostics": []
    },
    {
      "metric": "cognitive",
      "value": 2,
      "rulings": [
        "COG-ALL-0001",
        "COG-ALL-0002",
        "COG-ALL-0003",
        "COG-ALL-0004",
        "COG-ALL-0005",
        "COG-PY-0001"
      ],
      "derived_from": [],
      "trace": [],
      "diagnostics": []
    }
  ],
  "functions": [
    {
      "name": "classify",
      "qualified_name": "classify",
      "span": {
        "start_line": 1,
        "start_col": 0,
        "end_line": 4,
        "end_col": 16
      },
      "metrics": [
        {
          "metric": "cyclomatic",
          "value": 3,
          "rulings": [
            "CC-ALL-0001",
            "CC-PY-0001",
            "CC-PY-0002",
            "CC-PY-0003",
            "CC-PY-0004",
            "CC-PY-0005",
            "CC-PY-0006",
            "CC-PY-0007",
            "CC-PY-0008"
          ],
          "derived_from": [],
          "trace": [],
          "diagnostics": []
        },
        {
          "metric": "cognitive",
          "value": 2,
          "rulings": [
            "COG-ALL-0001",
            "COG-ALL-0002",
            "COG-ALL-0003",
            "COG-ALL-0004",
            "COG-ALL-0005",
            "COG-PY-0001"
          ],
          "derived_from": [],
          "trace": [],
          "diagnostics": []
        }
      ]
    }
  ],
  "readability": [],
  "diagnostics": [],
  "provenance": {
    "tool_version": "0.2.1",
    "spec_version": "1.2.1",
    "language": "python",
    "grammar": {
      "language": "python",
      "package": "tree-sitter-python",
      "version": "0.25.0",
      "abi_version": 15,
      "validated": true
    },
    "modes": {
      "cognitive": "whitepaper"
    },
    "rulings_applied": [
      "CC-ALL-0001",
      "CC-PY-0001",
      "CC-PY-0003",
      "COG-ALL-0001",
      "COG-ALL-0003"
    ]
  }
}

Cyclomatic is 3 on those four lines, and rulings_applied says why: the base value (CC-ALL-0001), the if (CC-PY-0001), and the and (CC-PY-0003, "Boolean short-circuit operators count per operator"). Cognitive is 2, because it treats the same and differently: a boolean sequence increments once, not once per operator (COG-ALL-0003).

The two lists differ on purpose. A metric's rulings are the decisions that govern that metric, whether or not this input triggered them; provenance.rulings_applied is the union of what actually fired here. Keys come out in a fixed canonical field order, not alphabetically: path, parse_ok, file_metrics, functions, readability, diagnostics, provenance. There are no timestamps and no hash-order dependence, so the same input under the same versions gives byte-identical output on a given platform.

Add --explain and each metric's trace fills with the individual increments, every one carrying the ruling that made it and the span it fired on. The cyclomatic trace on demo.py holds exactly three:

ruling span (line:col) delta
CC-ALL-0001 1:0 to 4:0 +1
CC-PY-0001 2:4 to 3:21 +1
CC-PY-0003 2:7 to 2:36 +1

Useful flags:

flag effect
--bw-granularity {snippet,function,file} BW measurement unit. Vectors outside the native snippet granularity are labelled extrapolated: true
--sonar-compat cognitive complexity in SonarSource-compatible mode (the mode differences are the COG-* ruling pairs in the spec)
--explain per-increment attribution traces on cyclomatic, cognitive and lloc
--strict error exit on parse errors instead of measure-and-label
--metrics, --no-readability restrict what is computed
--csv, -o FILE wide-format CSV; write to a file

Every counting decision can be interrogated. codecaliper spec show TOK-ALL-0006 prints that ruling's title, its metric family and language, its status, the spec version it entered in, the corpus cases that pin it, and the prose that decides the case:

Indentation features count leading whitespace with a tab as 8 characters and a space as 1. Supersedes TOK-ALL-0004 (tab = 1), whose PROVISIONAL marker was resolved by the pre-registered 32-cell BW faithfulness arbitration [...]

The whole set is browsable under Rulings.

codecaliper env prints the installed grammar versions against the calibrated ones, and codecaliper cite prints a methods-section template as a single unwrapped line, ready to paste (scroll it, or pipe it to fmt):

$ codecaliper cite
Metrics were computed with codecaliper 0.2.1 under mapping specification 1.2.1 (grammars: tree-sitter-python 0.25.0, tree-sitter-java 0.23.5, tree-sitter-go 0.25.0); cognitive complexity in whitepaper mode unless stated; readability vectors are the raw Buse-Weimer 2010 feature set, with granularity and extrapolation as labelled in the output.

Python API

from codecaliper import measure

report = measure(source_text, language="python")

report.parse_ok                     # False => measured anyway, and labelled
report.provenance.spec_version      # "1.2.1", on every report
for m in report.file_metrics:
    print(m.metric, m.value, m.rulings)

vec = report.readability[0]         # bw2010: the raw 25-feature vector
dict(zip(vec.names, vec.values))    # canonical order, sha-stamped
vec.extrapolated                    # True at file granularity: BW's native
                                    # unit is the snippet, and that is labelled

Per-function vectors and metrics:

report = measure(source_text, language="python", granularity="function")
for fn in report.functions:
    print(fn.qualified_name, fn.span, fn.metrics)

Java fragments are usually measured exactly as you pass them:

report = measure(java_fragment, language="java", granularity="snippet")

tree-sitter-java's program rule already accepts a bare statement sequence or a bare method declaration, so a snippet in that shape parses on its own and no wrapper is involved. CORE-JAVA-0001 is a narrow fallback for the fragments that do not: at snippet granularity, an error-bearing bare parse is retried inside two candidate class scaffolds, and a scaffold is adopted only when it strictly beats the bare parse on error count. In practice it rescues constructors and modifier-bearing members that cannot stand alone as a program. On the Buse-Weimer dataset's 100 snippets, 10 end up scaffolded.

When a scaffold is adopted, the emitted numbers are computed over the original lines only, spans are rebased to the snippet's own coordinates, and the vector carries a snippet-scaffolded diagnostic. Scaffolding lowers the error count; it does not have to reach zero, and it cannot repair a fragment that was truncated mid-construct. Most of the Buse-Weimer snippets are truncated that way. Of the 100: 27 parse cleanly as they stand, 2 more do once scaffolded, 8 are scaffolded and still error-bearing, and 63 are neither. The 71 that keep parse_ok: false are measured under BW-ALL-0007, described below. Measure the same corpus at file granularity and 73 come back recovered, because the scaffold never runs there; Validation reconciles the two counts.

All result types are frozen dataclasses. See the API reference.

Reading a report honestly

parse_ok. On a parse error codecaliper still measures, sets parse_ok: false and attaches parse-error-recovered. When the recovered ERROR region actually adds tokens, the BW token-family features fall back to the full lexical stream under ruling BW-ALL-0007 and say so (bw-lexical-fallback). A MISSING-only recovery adds no tokens, so it keeps the opaque stream and carries no fallback label.

provenance. Quote spec_version and the grammar versions in anything you publish. They are what make the number reproducible by someone else.

Per-value diagnostics. Every MI value carries mi-contains-cc; every Halstead value carries halstead-approximation. Neither is a warning you can clear by fixing your code: both are permanent properties of the metric. Honesty invariants says what each one is protecting you from.