Metadata-Version: 2.1
Name: CrapIndex
Version: 1.0.0
Summary: Reports the CRAP index (Change Risk Anti-Patterns) per function, combining cyclomatic complexity with test coverage
Author-email: Eduardo Almeida <eduardo.almeida@e-deploy.com.br>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: radon>=6.0
Requires-Dist: coverage>=7.6

# CrapIndex

Reports the **CRAP index** (Change Risk Anti-Patterns) per function, so you can
see which untested code is actually dangerous.

Coverage on its own does not tell you that. A 0%-covered one-line getter is
harmless; a 0%-covered function with cyclomatic complexity 14 is a landmine.
CRAP folds both signals into one number:

```
CRAP(f) = comp(f)^2 * (1 - cov(f))^3 + comp(f)
```

`comp(f)` is the function's cyclomatic complexity and `cov(f)` its statement
coverage as a ratio in `[0, 1]`. The cubic term forgives complexity as coverage
approaches full and punishes it hard when it does not. The conventional
acceptance limit is **30** — which is exactly complexity 5 with no coverage, so
the tool treats 30 as acceptable and only flags what is strictly above it.

| Complexity | Coverage | CRAP |
|---|---|---|
| 5 | 100% | 5 |
| 5 | 50% | 8.1 |
| 5 | 0% | 30 |
| 10 | 80% | 10.8 |
| 10 | 0% | 110 |

## Install

```bash
poetry install --with dev
```

## Use

The tool does **not** run your tests. Produce coverage data first, then point it
at the source:

```bash
# in the component you want to measure
python -m coverage run -m pytest

# then
poetry run crap-index path/to/src --coverage-file path/to/.coverage
```

With no `PATH` it analyzes `./src`. With no `--coverage-file` it walks up from
the first target looking for a `.coverage`, and also probes
`<component>/test/testenv/.coverage`, which is where the older components in
this monorepo keep theirs.

```
crap-index [PATH ...]
  --coverage-file PATH   coverage data file (default: auto-discovered)
  --rcfile PATH          coverage config (default: a .coveragerc beside the data file)
  --threshold N          CRAP considered acceptable (default 30)
  --fail-over N          exit 1 if any function is strictly above N
  --min-crap N           only list functions at or above N
  --top N                only list the N worst
  --sort {crap,complexity,coverage,name}
  --format {text,json}
  --output PATH          write to a file instead of stdout
  --json-output PATH     also write the JSON report to a file
  --exclude GLOB         repeatable, adds to the default exclusions
  --include-tests        also analyze test/ and tests/
  --on-unmeasured {zero,skip,fail}
```

Exit codes: `0` fine, `1` `--fail-over` was breached, `2` something was wrong
with the run (no coverage data, nothing to analyze, or none of the target files
appear in the coverage data).

### JSON output

`--format json` emits a `schema: 1` document that identifies each function by
file plus qualified name. It carries no timestamp, so two runs over unchanged
code produce byte-identical output and can be diffed directly.

`--json-output PATH` writes that same document to a file *in addition to*
whatever `--format` is printing, so you can read the report and keep the
machine-readable copy in one run. That is how `make crap` works in
`pos-structure`.

## Python version

Runs on Python **3.8+**, so that it works under the vendored interpreter the POS
components use. Note that it parses target source with the *running*
interpreter: under 3.8 it cannot parse 3.12-only syntax such as `match`, and
those files are reported as skipped. Analyze modern code with a modern
interpreter.

## How coverage is attributed to a function

A function's coverage is measured over its **body** — from the first statement
through the last line — so the `def` line and any decorators above it, which
execute at import time whether or not the function is ever called, cannot
inflate the score.

Two consequences are deliberate rather than accidental:

- **One-liners** (`def f(): return 1`) put the body on the `def` line itself, so
  they read as fully covered. They are flagged `is_single_line` in the JSON.
- **Nested definitions** inside a body are statements that run whenever the
  parent runs, so they mildly inflate the parent's coverage. This is consistent
  with radon folding a closure's complexity into its parent, which is why
  closures are reported as part of the enclosing function rather than separately.

A function with **nothing measurable** — an abstract stub, a docstring-only
body, or a body removed entirely by the coverage config's `exclude_lines` — is
treated as fully covered, so its CRAP is just its complexity. Its coverage
renders as `n/a` in text and `null` in JSON, so it can never be mistaken for a
measured 100%. Scoring those as 0% instead would turn every stub into a false
alarm.

Files **absent from the coverage data** are a different case, and a common one:
the older `.coveragerc` files here scope `source` to a single directory, so
anything outside it was never measured. By default such files count as 0%
covered and the run prints a warning with the count; `--on-unmeasured skip`
drops them instead and `--on-unmeasured fail` refuses to continue. If *none* of
the target files appear in the data the tool exits 2 rather than printing a
confident, entirely red report.

Only the `[report]` section of an rcfile affects the numbers — `exclude_lines`,
`exclude_also`, `partial_branches`. The `source` and `omit` settings in the
legacy `.coveragerc` files are relative to the directory they are run from and
are ignored here.

## Files that cannot be parsed

A file that fails to parse is skipped and listed in the summary, never fatal.
That includes the handful of Python 2 modules still in this monorepo — note that
they cannot be identified from `requires-python`, since some sit in packages
that declare Python 3.

## Manual smoke test

`PosCfgTools` has coverage data committed in the repository:

```bash
poetry run crap-index ../PosCfgTools/src \
  --coverage-file ../PosCfgTools/test/testenv/.coverage \
  --rcfile ../PosCfgTools/test/testenv/.coveragerc
```

This is a manual check rather than a test: the data file records absolute paths
and only resolves on a checkout at the same location. It is also old enough that
the staleness warning will fire, which is itself worth seeing.

## Development

```bash
poetry run pytest
poetry run ruff check .
poetry run ruff format --check .
```
