Skip to content

Concepts

go-crap computes the CRAP score by combining two independent analyses and merging them:

Pipeline Overview

go-crap scan
│
├── scan.Scan()                  — orchestrator (internal/scan)
│       ├── build exclude regex from --exclude flags
│       ├── coverage.Scan()      — discover Go modules, run go test -cover
│       │       │
│       │       └── coverage data per (file, func)
│       │
│       ├── complexity.Analyze() — walk AST, compute cyclomatic complexity
│       │       │
│       │       └── complexity data per (file, func)
│       │
│       ├── merge.Merge()        — join by (filepath, funcname)
│       │       │
│       │       └── merged entries with CC + Coverage
│       │
│       ├── score.Score()        — apply CRAP formula + missing policy
│       │       │
│       │       └── CRAP entries
│       │
│       └── applyFilters()       — sort descending, apply --min and --top
│
└── report.Format()              — table / json / github / sarif / pr-comment
        |
        └── output

The Modules

Each module is independently testable:

Module Purpose
internal/scan Orchestrates the pipeline: coverage, complexity, merge, score, filters
internal/complexity AST walking to compute cyclomatic complexity (adapted from gocyclo, BSD-3-Clause)
internal/coverage Module discovery + go test -cover profiling (adapted from test-finder, MIT)
internal/merge Join coverage and complexity by (filepath, funcname) using a double index
internal/score CRAP formula + missing coverage policy
internal/report Output formatters (table, JSON, GitHub, SARIF, PR comment)
pkg/logger Logger interface and configuration types
pkg/slogger slog-backed Logger implementation

Important: Path Matching

Coverage and complexity produce paths in different formats. The merge layer uses a double index to match them — never canonicalize relative paths against CWD. See internal/merge/index.go and the test TestMerge_RelativePathsNotResolvedAgainstCWD.

Function Name Matching

Coverage and complexity also produce function names in different formats. Coverage includes the receiver in the name (e.g. Level.String, (*JSONFormatter).Format), while complexity stores the receiver separately. The merge layer normalizes both sides to bare method names before matching, supporting pointer receivers (*Type), value receivers (Type), and plain functions.