mirror of
https://github.com/unclebob/crapper.git
synced 2026-11-06 18:13:50 +00:00
Score Clojure, Java, Go, TypeScript, Rust, and Python with each language's complexity rules and coverage report.
94 lines
5.2 KiB
Markdown
94 lines
5.2 KiB
Markdown
# crapper
|
||
|
||
CRAP scores for Clojure, Java, Go, TypeScript, Rust, and Python. One run detects the language of each source file, applies that language's complexity and coverage rules, and writes the snapshot [uml-viewer](https://github.com/unclebob/uml-viewer) already reads.
|
||
|
||
The formula is the one from [crap4clj](https://github.com/unclebob/crap4clj), [crap4java](https://github.com/unclebob/crap4java), and [crap4go](https://github.com/unclebob/crap4go):
|
||
|
||
```text
|
||
CRAP = CC² × (1 − coverage)³ + CC
|
||
```
|
||
|
||
`CC` is cyclomatic complexity. `coverage` is the fraction of the function exercised by tests. A score of 1–5 is low risk, 5–30 is worth a look, and 30+ is complex and under-tested.
|
||
|
||
## Run
|
||
|
||
```bash
|
||
./crapper
|
||
```
|
||
|
||
The first run creates `.venv` and installs the tool. From a project root it walks the tree, skips `test`, `spec`, `vendor`, `node_modules`, and `target`, and writes two results:
|
||
|
||
- a table on stdout, worst score first
|
||
- `.metrics/crap.edn`, replaced on every successful analysis
|
||
|
||
```bash
|
||
./crapper --no-coverage # complexity only; coverage is N/A
|
||
./crapper --use-existing-coverage # read reports already on disk
|
||
./crapper src/demo/core.clj src/ui # these files and trees
|
||
./crapper --changed # git additions and edits
|
||
./crapper combat # path fragment, same idea as crap4clj
|
||
./crapper --threshold 30 # exit 2 when the worst score is higher
|
||
```
|
||
|
||
## Snapshot
|
||
|
||
`.metrics/crap.edn` has the same shape crap4clj writes:
|
||
|
||
```clojure
|
||
{:entries [{:name "place"
|
||
:namespace "demo.Board"
|
||
:complexity 3
|
||
:coverage 75.0
|
||
:crap 3.1}]}
|
||
```
|
||
|
||
uml-viewer groups entries by `:namespace` and joins each operation on `:name`. `nil` coverage means the function was found but no coverage data applied, and CRAP is `nil` too.
|
||
|
||
| Language | `:namespace` | `:name` |
|
||
| --- | --- | --- |
|
||
| Clojure | the `ns` | the `defn` / `defn-` name |
|
||
| Java | `package.Class`, or `package.Outer.Inner` | the method name |
|
||
| Go | the package import path, or `import/path.Receiver` | the function or method name |
|
||
| TypeScript | the dotted module path, or `module.Class` | the function or method name |
|
||
| Rust | `crate::module`, or `crate::module::Type` | the function or method name |
|
||
| Python | the dotted module path, or `module.Class` | the function or method name |
|
||
|
||
Rename or move is a new entry. There is no identity matching across runs.
|
||
|
||
## What each language counts
|
||
|
||
Clojure follows crap4clj: `if` / `when` and their variants, `and`, `or`, `loop`, `catch`, and each clause of `cond`, `condp`, `case`, `cond->`, `cond->>`, `some->`, and `some->>`. Coverage prefers Cloverage's per-line form counts and falls back to LCOV.
|
||
|
||
Java follows crap4java: `if`, loops, `catch`, `?:`, each `switch` label, and `&&` / `||`. Constructors and methods inside anonymous classes are omitted. Coverage is JaCoCo's `INSTRUCTION` counter.
|
||
|
||
Go follows crap4go: `if`, `for`, `range`, each `switch` and `select` clause, and `&&` / `||`. Coverage is `go test -coverprofile`.
|
||
|
||
TypeScript and TSX use the same structural decisions as Java, including `&&` inside JSX. Top-level functions, class methods, and top-level arrow functions are entries. Nested callbacks stay inside the enclosing function. Coverage is LCOV. A `coverage` script is used as-is. A Vitest project runs `vitest --coverage` (installing `@vitest/coverage-v8` into `node_modules` when it is missing, without editing `package.json`). Other test scripts are wrapped in `c8`.
|
||
|
||
Rust counts `if`, loops, each `match` arm, `?`, and `&&` / `||`. `mod tests` is skipped. Coverage is LCOV from `cargo llvm-cov` or `cargo tarpaulin`, run in the nearest directory that contains `Cargo.toml`. When neither tool is installed, the run installs `cargo-llvm-cov`.
|
||
|
||
Python counts `if`, `elif`, `for`, `while`, `except`, each `match` case, a comprehension filter, a conditional expression, and each `and` / `or`. Nested functions stay inside the enclosing function. Coverage is LCOV from `coverage.py`, running pytest when the project uses it and `unittest` otherwise. `coverage` and `pytest` are installed into the project's interpreter when they are missing.
|
||
|
||
## Coverage commands
|
||
|
||
By default a run deletes the previous report for each language it is about to measure and regenerates it:
|
||
|
||
| Language | Command | Report |
|
||
| --- | --- | --- |
|
||
| Clojure | `clj -M:cov --lcov` (needs `deps.edn` or `bb.edn`) | `target/coverage/` |
|
||
| Java | JaCoCo Maven plugin `0.8.12` in each module with `pom.xml` | `target/site/jacoco/jacoco.xml` |
|
||
| Go | `go test ./... -coverprofile=...` | `target/coverage/go/coverage.out` |
|
||
| TypeScript | `npm run coverage`, or Vitest `--coverage`, or `npx c8 ... npm test` | `coverage/lcov.info` or `target/coverage/typescript/lcov.info` |
|
||
| Rust | `cargo llvm-cov` or `cargo tarpaulin`, per Cargo package | `target/coverage/rust/lcov.info` |
|
||
| Python | `coverage run` with pytest or unittest, then `coverage lcov` | `target/coverage/python/lcov.info` |
|
||
|
||
A missing tool or a failed test run leaves that language at N/A and still writes the snapshot. Pass `--coverage-command` to replace those defaults with one command of your own.
|
||
|
||
## Development
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -e '.[dev]'
|
||
.venv/bin/pytest
|
||
```
|