crap-score
$
npx mdskill add microsoft/testfx/crap-scoreCalculates CRAP scores for .NET methods, classes, or files to identify risky untested code
- Identifies complex and undertested .NET code for targeted refactoring or testing
- Uses Cobertura coverage data and cyclomatic complexity metrics
- Applies CRAP formula to combine coverage and complexity into a risk score
- Returns CRAP scores with risk levels for specific code targets
SKILL.md
.github/skills/crap-scoreView on GitHub ↗
---
name: crap-score
description: >
Calculates targeted CRAP (Change Risk Anti-Patterns) scores for a named .NET
method, class, or single source file. Use when the user explicitly asks to
compute CRAP scores or assess risky untested code for a specific target,
combining Cobertura coverage data with cyclomatic complexity analysis.
DO NOT USE FOR: project-wide coverage analysis, coverage plateau or "stuck
coverage" diagnosis, what's blocking coverage, or where to add tests across
a project (use coverage-analysis); writing tests; running tests without
CRAP context.
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 `writing-mstest-tests` skill or general coding assistance)
- 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 (no Cobertura XML available), **always run `dotnet test` with coverage collection first** and mention the exact command in your response. Do not skip this step -- CRAP scores require 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 |
### 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)
- 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
- **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.