crap-score
>
Works with
Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: crap-score
description: >
license: MIT
---
# CRAP Score Analysis
Calculate CRAP (Change Risk Anti-Patterns) scores for .NET methods to identify code that is both complex and undertested.
## Background
The CRAP score combines **cyclomatic complexity** and **code coverage** into a single metric:
$$\text{CRAP}(m) = \text{comp}(m)^2 \times (1 - \text{cov}(m))^3 + \text{comp}(m)$$
Where:
- $\text{comp}(m)$ = cyclomatic complexity of method $m$
- $\text{cov}(m)$ = code coverage ratio (0.0 to 1.0) of method $m$
| CRAP Score | Risk Level | Interpretation |
|------------|------------|----------------|
| < 5 | Low | Simple and well-tested |
| 5-15 | Moderate | Acceptable for most code |
| 15-30 | High | Needs more tests or simplification |
| > 30 | Critical | Refactor and add coverage urgently |
A method with 100% coverage has CRAP = complexity (the minimum). A method with 0% coverage has CRAP = complexity^2 + complexity.
## When to Use
- User wants to assess which methods are risky due to low coverage and high complexity
- User asks for CRAP score of specific methods, classes, or files
- User wants to prioritize which code to test next
- User wants to evaluate test quality beyond simple coverage percentages
## When Not to Use
- User just wants to run tests (use `run-tests` skill)
- User wants to write new tests (use `code-testing-agent`)
- User only wants a coverage percentage without complexity analysis
## Inputs
| Input | Required | Description |
|-------|----------|-------------|
| Target scope | Yes | Method name, class name, or file path to analyze |
| Test project path | No | Path to the test project. Defaults to discovering test projects in the solution. |
| Source project path | No | Path to the source project under analysis |
## Workflow
### Step 1: Collect code coverage data
If no coverage data exists yet, classify the test project first. For SDK-style
projects, run `dotnet test` with coverage collection. For classic non-SDK
projects (`ToolsVersion`, explicit compile items, or `packages.config`), use only
a repository-provided coverage command that emits Cobertura. If none exists,
ask for Cobertura XML and stop; do not migrate the project or inject an SDK-style
coverage package. CRAP scores always require real coverage data.
Check the test project's `.csproj` for the coverage package, then run the appropriate command:
| Coverage Package | Command | Output Location |
|---|---|---|
| `coverlet.collector` | `dotnet test --collect:"XPlat Code Coverage" --results-directory ./TestResults` | Typically under `TestResults/<guid>/coverage.cobertura.xml`. Search recursively under the results directory (for example, `TestResults/**/coverage.cobertura.xml`) or use any explicit coverage path the user provides. |
| `Microsoft.Testing.Extensions.CodeCoverage` (.NET 9) | `dotnet test -- --coverage --coverage-output-format cobertura --coverage-output ./TestResults` | `--coverage-output` path |
| `Microsoft.Testing.Extensions.CodeCoverage` (.NET 10+) | `dotnet test --coverage --coverage-output-format cobertura --coverage-output ./TestResults` | `--coverage-output` path |
#### Never estimate coverage
**Guessed coverage produces wrong CRAP scores, which is worse than no answer.**
For a classic project with no repository coverage command or existing report,
stop here and request Cobertura; do not use any collection fallback below.
For SDK-style projects, if the first command yields no Cobertura XML, work down
this collection list before giving up:
1. For SDK-style projects only, add a provider if none is referenced:
`dotnet add <test.csproj> package coverlet.collector`, then re-run. Never use
this fallback for `packages.config` or classic non-SDK projects.
2. Use the standalone collector, which works even when the test host or a shared assembly blocks the in-proc collector:
`dotnet tool install --global dotnet-coverage` then
`dotnet-coverage collect -f cobertura -o coverage.cobertura.xml "dotnet test <test.csproj>"`.
For any project type, if a real binary `.coverage` report already exists, convert
or summarize that existing data with ReportGenerator:
3. Convert the existing report:
`dotnet tool install --global dotnet-reportgenerator-globaltool` then
`reportgenerator -reports:<file> -targetdir:cov -reporttypes:Cobertura`.
4. Tests fail but still run? Coverage is collected from the tests that executed — continue with that data and note the failures.
If every path fails, **report that coverage could not be collected, show the commands you tried and their errors, and stop.** Report complexity on its own if useful, but never publish a CRAP number derived from an assumed coverage percentage.
### Step 2: Compute cyclomatic complexity
Analyze the target source files to determine cyclomatic complexity per method. Count the following decision points (each adds 1 to the base complexity of 1):
| Construct | Example |
|-----------|---------|
| `if` | `if (x > 0)` |
| `else if` | `else if (y < 0)` |
| `case` (each) | `case 1:` |
| `for` | `for (int i = 0; ...)` |
| `foreach` | `foreach (var item in list)` |
| `while` | `while (running)` |
| `do...while` | `do { } while (cond)` |
| `catch` (each) | `catch (Exception ex)` |
| `&&` | `if (a && b)` |
| `\|\|` (OR) | `if (a \|\| b)` |
| `??` | `value ?? fallback` |
| `?.` | `obj?.Method()` |
| `? :` (ternary) | `x > 0 ? a : b` |
| Pattern match arm | `x is > 0 and < 10` |
Base complexity is 1 for every method. Each decision point adds 1.
When analyzing, read the source file and count these constructs per method. Report the breakdown.
### Step 3: Extract per-method coverage from Cobertura XML
Parse the Cobertura XML to find each method's `line-rate` attribute under the target `<class>` element. If `line-rate` is not available at method level, compute it from the `<lines>` elements:
$$\text{cov}(m) = \frac{\text{lines with hits} > 0}{\text{total lines}}$$
Method names in Cobertura may differ from source (async methods, lambdas). Match by line ranges when names don't align.
### Step 4: Calculate CRAP scores
For each method in scope, apply the formula:
$$\text{CRAP}(m) = \text{comp}(m)^2 \times (1 - \text{cov}(m))^3 + \text{comp}(m)$$
### Step 5: Present results
Present a sorted table (highest CRAP first):
```text
| Method | Complexity | Coverage | CRAP Score | Risk |
|---------------------------------|------------|----------|------------|----------|
| OrderService.ProcessOrder | 12 | 45% | 28.4 | High |
| OrderService.ValidateItems | 8 | 90% | 8.1 | Moderate |
| OrderService.CalculateTotal | 3 | 100% | 3.0 | Low |
```
Include:
- **Summary**: total methods analyzed, how many in each risk category
- **Top offenders**: methods with CRAP > 30, with specific recommendations
- **Quick wins**: methods with high complexity but where small coverage improvements would drop the score significantly
### Step 6: Provide actionable recommendations
For high-CRAP methods, suggest one or both:
1. **Add tests** -- identify uncovered branches and suggest specific test cases
2. **Reduce complexity** -- suggest extract-method refactoring for deeply nested logic
Calculate the **coverage needed** to bring a method below a CRAP threshold of 15:
$$\text{cov}_{\text{needed}} = 1 - \left(\frac{15 - \text{comp}}{\text{comp}^2}\right)^{1/3}$$
This formula only applies when comp < 15. When comp >= 15, the minimum possible CRAP score (at 100% coverage) is comp itself, which already meets or exceeds the threshold. In that case, **coverage alone cannot bring the CRAP score below the threshold** -- the method must be refactored to reduce its cyclomatic complexity first.
Report this as: "To bring `ProcessOrder` (complexity 12) below CRAP 15, increase coverage from 45% to at least 72%." For methods where complexity alone exceeds the threshold, report: "`ComplexMethod` (complexity 18) cannot reach CRAP < 15 through testing alone -- reduce complexity by extracting sub-methods."
## Validation
- Verify that coverage data was collected successfully (Cobertura XML exists and contains data)
- Confirm every coverage figure came from that XML — no estimated, assumed, or source-comment-derived values
- Cross-check that method names in coverage data match the source code
- Confirm CRAP scores by spot-checking the formula on one method manually
- Ensure a 100%-covered method's CRAP equals its complexity exactly
## Common Pitfalls
- **Estimating coverage when collection fails**: never do it — the resulting CRAP scores are wrong in the direction that matters. Work through the fallbacks in Step 1, then report the blocker instead.
- **Trusting a stale complexity comment in the source**: compute cyclomatic complexity from the current code; a `// complexity: 7` comment left by a previous author is not evidence.
- **Giving up on a shared-assembly or test-host collector error**: `dotnet-coverage collect` runs out of process and usually succeeds where the in-proc collector fails.
- **Stale coverage data**: Always regenerate coverage before computing CRAP scores. Old coverage files will produce misleading results.
- **Method name mismatches**: Cobertura XML may use mangled/compiler-generated names for async methods, lambdas, or local functions. Match by line ranges when names don't align.
- **Generated code**: Exclude auto-generated files (e.g., `*.Designer.cs`, `*.g.cs`) from analysis unless explicitly requested.More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
1.5M
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
972.7k
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
828.8k

