Crap Analysis

aaronontheweb/dotnet-skills/skills/crap-analysis

by aarononthewebe426ed93a9f3No license1.2K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 3 weeks ago

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

AI-generated overview

Analyzes .NET code coverage and CRAP scores to find high-risk, complex, under-tested code.

What it does
This skill explains how to compute CRAP (Change Risk Anti-Patterns) scores, which combine cyclomatic complexity with test coverage to rank risky code. It provides a coverage.runsettings configuration, ReportGenerator setup, dotnet test and reportgenerator commands, and guidance on reading Risk Hotspots reports. It also covers coverage thresholds, CI/CD integration examples, and exclusion patterns.
When to use it
Use it when evaluating code quality and test coverage before changes, identifying code that needs refactoring or tests, setting up coverage collection for a .NET project, or establishing coverage thresholds for CI/CD pipelines.
Requirements
Requires a .NET project with the dotnet CLI, the XPlat Code Coverage collector, and the ReportGenerator global tool; OpenCover-format coverage output is needed for complexity metrics. No scripts ship with the skill; it is instructions and configuration only.

CRAP Score Analysis

When to Use This Skill

Use this skill when:

  • Evaluating code quality and test coverage before changes
  • Identifying high-risk code that needs refactoring or testing
  • Setting up coverage collection for a .NET project
  • Prioritizing which code to test based on risk
  • Establishing coverage thresholds for CI/CD pipelines

What is CRAP?

CRAP Score = Complexity x (1 - Coverage)^2

The CRAP (Change Risk Anti-Patterns) score combines cyclomatic complexity with test coverage to identify risky code.

CRAP ScoreRisk LevelAction Required
< 5LowWell-tested, maintainable code
5-30MediumAcceptable but watch complexity
> 30HighNeeds tests or refactoring

Why CRAP Matters

  • High complexity + low coverage = danger: Code that's hard to understand AND untested is risky to modify
  • Complexity alone isn't enough: A complex method with 100% coverage is safer than a simple method with 0%
  • Focuses effort: Prioritize testing on complex code, not simple getters/setters

CRAP Score Examples

MethodComplexityCoverageCalculationCRAP
GetUserId()10%1 x (1 - 0)^21
ParseToken()5452%54 x (1 - 0.52)^212.4
ValidateForm()200%20 x (1 - 0)^220
ProcessOrder()4520%45 x (1 - 0.20)^228.8
ImportData()8010%80 x (1 - 0.10)^264.8

Coverage Collection Setup

coverage.runsettings

Create a coverage.runsettings file in your repository root. The OpenCover format is required for CRAP score calculation because it includes cyclomatic complexity metrics.

xml
<?xml version="1.0" encoding="utf-8" ?><RunSettings>  <DataCollectionRunSettings>    <DataCollectors>      <DataCollector friendlyName="XPlat code coverage">        <Configuration>          <!-- OpenCover format includes cyclomatic complexity for CRAP scores -->          <Format>cobertura,opencover</Format>
          <!-- Exclude test and benchmark assemblies -->          <Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>
          <!-- Exclude generated code, obsolete members, and explicit exclusions -->          <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
          <!-- Exclude source-generated files, Blazor generated code, and migrations -->          <ExcludeByFile>**/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/*</ExcludeByFile>
          <!-- Exclude test projects -->          <IncludeTestAssembly>false</IncludeTestAssembly>
          <!-- Optimization flags -->          <SingleHit>false</SingleHit>          <UseSourceLink>true</UseSourceLink>          <SkipAutoProps>true</SkipAutoProps>        </Configuration>      </DataCollector>    </DataCollectors>  </DataCollectionRunSettings></RunSettings>

Key Configuration Options

OptionPurpose
FormatMust include opencover for complexity metrics
ExcludeExclude test/benchmark assemblies by pattern
ExcludeByAttributeSkip generated, obsolete, and explicitly excluded code (includes ExcludeFromCodeCoverageAttribute)
ExcludeByFileSkip source-generated files, Blazor components, and migrations
SkipAutoPropsDon't count auto-properties as branches

ReportGenerator Installation

Install ReportGenerator as a local tool for generating HTML reports with Risk Hotspots.

Add to .config/dotnet-tools.json

json
{  "version": 1,  "isRoot": true,  "tools": {    "dotnet-reportgenerator-globaltool": {      "version": "5.4.5",      "commands": ["reportgenerator"],      "rollForward": false    }  }}

Then restore:

bash
dotnet tool restore

Or Install Globally

bash
dotnet tool install --global dotnet-reportgenerator-globaltool

Collecting Coverage

Run Tests with Coverage Collection

bash
# Clean previous resultsrm -rf coverage/ TestResults/
# Run unit tests with coveragedotnet test tests/MyApp.Tests.Unit \  --settings coverage.runsettings \  --collect:"XPlat Code Coverage" \  --results-directory ./TestResults
# Run integration tests (optional, adds to coverage)dotnet test tests/MyApp.Tests.Integration \  --settings coverage.runsettings \  --collect:"XPlat Code Coverage" \  --results-directory ./TestResults

Generate HTML Report

bash
dotnet reportgenerator \  -reports:"TestResults/**/coverage.opencover.xml" \  -targetdir:"coverage" \  -reporttypes:"Html;TextSummary;MarkdownSummaryGithub"

Report Types

TypeDescriptionOutput
HtmlFull interactive reportcoverage/index.html
TextSummaryPlain text summarycoverage/Summary.txt
MarkdownSummaryGithubGitHub-compatible markdowncoverage/SummaryGithub.md
BadgesSVG badges for READMEcoverage/badge_*.svg
CoberturaMerged Cobertura XMLcoverage/Cobertura.xml

Reading the Report

Risk Hotspots Section

The HTML report includes a Risk Hotspots section showing methods sorted by complexity:

  • Cyclomatic Complexity: Number of independent paths through code (if/else, switch cases, loops)
  • NPath Complexity: Number of acyclic execution paths (exponential growth with nesting)
  • Crap Score: Calculated from complexity and coverage

Interpreting Results

Risk Hotspots─────────────Method                          Complexity  Coverage  Crap Score──────────────────────────────────────────────────────────────────DataImporter.ParseRecord()      54          52%       12.4AuthService.ValidateToken()     32          0%        32.0   ← HIGH RISKOrderProcessor.Calculate()      28          85%       1.3UserService.CreateUser()        15          100%      0.0

Action items:

  • ValidateToken() has CRAP > 30 with 0% coverage - test immediately or refactor
  • ParseRecord() is complex but has decent coverage - acceptable
  • CreateUser() and Calculate() are well-tested - safe to modify

Coverage Thresholds

Recommended Standards

Coverage TypeTargetAction
Line Coverage> 80%Good for most projects
Branch Coverage> 60%Catches conditional logic
CRAP Score< 30Maximum for new code

Configuring Thresholds

Create coverage.props in your repository:

xml
<Project>  <PropertyGroup>    <!-- Coverage thresholds for CI enforcement -->    <CoverageThresholdLine>80</CoverageThresholdLine>    <CoverageThresholdBranch>60</CoverageThresholdBranch>  </PropertyGroup></Project>

CI/CD Integration

GitHub Actions

yaml
name: Coverage
on:  pull_request:    branches: [main, dev]
jobs:  coverage:    runs-on: ubuntu-latest
    steps:      - uses: actions/checkout@v4
      - name: Setup .NET        uses: actions/setup-dotnet@v4        with:          dotnet-version: '9.0.x'
      - name: Restore tools        run: dotnet tool restore
      - name: Run tests with coverage        run: |          dotnet test \            --settings coverage.runsettings \            --collect:"XPlat Code Coverage" \            --results-directory ./TestResults
      - name: Generate report        run: |          dotnet reportgenerator \            -reports:"TestResults/**/coverage.opencover.xml" \            -targetdir:"coverage" \            -reporttypes:"Html;MarkdownSummaryGithub;Cobertura"
      - name: Upload coverage report        uses: actions/upload-artifact@v4        with:          name: coverage-report          path: coverage/
      - name: Add coverage to PR        uses: marocchino/sticky-pull-request-comment@v2        with:          path: coverage/SummaryGithub.md

Azure Pipelines

yaml
- task: DotNetCoreCLI@2  displayName: 'Run tests with coverage'  inputs:    command: 'test'    arguments: '--settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory $(Build.SourcesDirectory)/TestResults'
- task: DotNetCoreCLI@2  displayName: 'Generate coverage report'  inputs:    command: 'custom'    custom: 'reportgenerator'    arguments: '-reports:"$(Build.SourcesDirectory)/TestResults/**/coverage.opencover.xml" -targetdir:"$(Build.SourcesDirectory)/coverage" -reporttypes:"HtmlInline_AzurePipelines;Cobertura"'
- task: PublishCodeCoverageResults@2  displayName: 'Publish coverage'  inputs:    codeCoverageTool: 'Cobertura'    summaryFileLocation: '$(Build.SourcesDirectory)/coverage/Cobertura.xml'

Quick Reference

One-Liner Commands

bash
# Full analysis workflowrm -rf coverage/ TestResults/ && \dotnet test --settings coverage.runsettings \  --collect:"XPlat Code Coverage" \  --results-directory ./TestResults && \dotnet reportgenerator \  -reports:"TestResults/**/coverage.opencover.xml" \  -targetdir:"coverage" \  -reporttypes:"Html;TextSummary"
# View summarycat coverage/Summary.txt
# Open HTML report (Linux)xdg-open coverage/index.html
# Open HTML report (macOS)open coverage/index.html
# Open HTML report (Windows)start coverage/index.html

Project Standards

MetricNew CodeLegacy Code
Line Coverage80%+60%+ (improve gradually)
Branch Coverage60%+40%+ (improve gradually)
Maximum CRAP30Document exceptions
High-risk methodsMust have testsAdd tests before modifying

What Gets Excluded

The recommended coverage.runsettings excludes:

PatternReason
[*.Tests]*Test assemblies aren't production code
[*.Benchmark]*Benchmark projects
[*.Migrations]*Database migrations (generated)
GeneratedCodeAttributeSource generators
CompilerGeneratedAttributeCompiler-generated code
ExcludeFromCodeCoverageAttributeExplicit developer opt-out
*.g.cs, *.designer.csGenerated files
*.razor.g.csBlazor component generated code
*.razor.css.g.csBlazor CSS isolation generated code
**/Migrations/**/*EF Core migrations (auto-generated)
SkipAutoPropsAuto-properties (trivial branches)

When to Update Thresholds

Lower thresholds temporarily for:

  • Legacy codebases being modernized (document in README)
  • Generated code that can't be modified
  • Third-party wrapper code

Never lower thresholds for:

  • "It's too hard to test" - refactor instead
  • "We'll add tests later" - add them now
  • New features - should meet standards from the start

Additional Resources

Source and attribution

Source:aaronontheweb/dotnet-skillsinskills/crap-analysisat commite426ed9

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal