crap-analysis by aaronontheweb/dotnet-skills
npx skills add https://github.com/aaronontheweb/dotnet-skills --skill crap-analysis在以下情况下使用此技能:
CRAP 分数 = 复杂度 × (1 - 覆盖率)^2
CRAP(变更风险反模式)分数结合了圈复杂度和测试覆盖率,以识别高风险代码。
| CRAP 分数 | 风险等级 | 所需操作 |
|---|---|---|
| < 5 | 低 | 经过良好测试、可维护的代码 |
| 5-30 | 中 | 可接受,但需关注复杂度 |
| > 30 | 高 | 需要测试或重构 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 方法 | 复杂度 | 覆盖率 | 计算 | CRAP |
|---|---|---|---|---|
GetUserId() | 1 | 0% | 1 × (1 - 0)^2 | 1 |
ParseToken() | 54 | 52% | 54 × (1 - 0.52)^2 | 12.4 |
ValidateForm() | 20 | 0% | 20 × (1 - 0)^2 | 20 |
ProcessOrder() | 45 | 20% | 45 × (1 - 0.20)^2 | 28.8 |
ImportData() | 80 | 10% | 80 × (1 - 0.10)^2 | 64.8 |
在仓库根目录创建一个 coverage.runsettings 文件。CRAP 分数计算需要 OpenCover 格式,因为它包含圈复杂度指标。
<?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>
| 选项 | 用途 |
|---|---|
Format | 必须包含 opencover 以获取复杂度指标 |
Exclude | 按模式排除测试/基准测试程序集 |
ExcludeByAttribute | 跳过生成的、过时的和显式排除的代码(包括 ExcludeFromCodeCoverageAttribute) |
ExcludeByFile | 跳过源生成文件、Blazor 组件和迁移文件 |
SkipAutoProps | 不将自动属性计为分支 |
将 ReportGenerator 安装为本地工具,用于生成包含风险热点的 HTML 报告。
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}
然后恢复:
dotnet tool restore
dotnet tool install --global dotnet-reportgenerator-globaltool
# Clean previous results
rm -rf coverage/ TestResults/
# Run unit tests with coverage
dotnet 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
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"
| 类型 | 描述 | 输出 |
|---|---|---|
Html | 完整的交互式报告 | coverage/index.html |
TextSummary | 纯文本摘要 | coverage/Summary.txt |
MarkdownSummaryGithub | GitHub 兼容的 Markdown | coverage/SummaryGithub.md |
Badges | 用于 README 的 SVG 徽章 | coverage/badge_*.svg |
Cobertura | 合并的 Cobertura XML | coverage/Cobertura.xml |
HTML 报告包含一个风险热点部分,按复杂度排序显示方法:
Risk Hotspots
─────────────
Method Complexity Coverage Crap Score
──────────────────────────────────────────────────────────────────
DataImporter.ParseRecord() 54 52% 12.4
AuthService.ValidateToken() 32 0% 32.0 ← HIGH RISK
OrderProcessor.Calculate() 28 85% 1.3
UserService.CreateUser() 15 100% 0.0
待办事项:
ValidateToken() 的 CRAP > 30 且覆盖率为 0% - 立即测试或重构ParseRecord() 很复杂但覆盖率尚可 - 可接受CreateUser() 和 Calculate() 经过良好测试 - 修改安全| 覆盖率类型 | 目标 | 操作 |
|---|---|---|
| 行覆盖率 | > 80% | 对大多数项目来说良好 |
| 分支覆盖率 | > 60% | 捕获条件逻辑 |
| CRAP 分数 | < 30 | 新代码的最大值 |
在仓库中创建 coverage.props:
<Project>
<PropertyGroup>
<!-- Coverage thresholds for CI enforcement -->
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>
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
- 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'
# Full analysis workflow
rm -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 summary
cat 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
| 指标 | 新代码 | 遗留代码 |
|---|---|---|
| 行覆盖率 | 80%+ | 60%+(逐步改进) |
| 分支覆盖率 | 60%+ | 40%+(逐步改进) |
| 最大 CRAP | 30 | 记录例外情况 |
| 高风险方法 | 必须有测试 | 修改前添加测试 |
推荐的 coverage.runsettings 排除了以下内容:
| 模式 | 原因 |
|---|---|
[*.Tests]* | 测试程序集不是生产代码 |
[*.Benchmark]* | 基准测试项目 |
[*.Migrations]* | 数据库迁移(生成的) |
GeneratedCodeAttribute | 源生成器 |
CompilerGeneratedAttribute | 编译器生成的代码 |
ExcludeFromCodeCoverageAttribute | 开发者显式选择退出 |
*.g.cs, *.designer.cs | 生成的文件 |
*.razor.g.cs | Blazor 组件生成的代码 |
*.razor.css.g.cs | Blazor CSS 隔离生成的代码 |
**/Migrations/**/* | EF Core 迁移(自动生成) |
SkipAutoProps | 自动属性(简单分支) |
临时降低阈值的情况:
绝不降低阈值的情况:
每周安装数
96
仓库
GitHub 星标数
495
首次出现
2026年1月29日
安全审计
安装于
claude-code77
codex60
github-copilot57
opencode57
gemini-cli56
kimi-cli53
Use this skill when:
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 Score | Risk Level | Action Required |
|---|---|---|
| < 5 | Low | Well-tested, maintainable code |
| 5-30 | Medium | Acceptable but watch complexity |
| > 30 | High | Needs tests or refactoring |
| Method | Complexity | Coverage | Calculation | CRAP |
|---|---|---|---|---|
GetUserId() | 1 | 0% | 1 x (1 - 0)^2 | 1 |
ParseToken() | 54 | 52% | 54 x (1 - 0.52)^2 | 12.4 |
ValidateForm() | 20 | 0% | 20 x (1 - 0)^2 | 20 |
ProcessOrder() |
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 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>
| Option | Purpose |
|---|---|
Format | Must include opencover for complexity metrics |
Exclude | Exclude test/benchmark assemblies by pattern |
ExcludeByAttribute | Skip generated, obsolete, and explicitly excluded code (includes ExcludeFromCodeCoverageAttribute) |
ExcludeByFile | Skip source-generated files, Blazor components, and migrations |
SkipAutoProps |
Install ReportGenerator as a local tool for generating HTML reports with Risk Hotspots.
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}
Then restore:
dotnet tool restore
dotnet tool install --global dotnet-reportgenerator-globaltool
# Clean previous results
rm -rf coverage/ TestResults/
# Run unit tests with coverage
dotnet 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
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"
| Type | Description | Output |
|---|---|---|
Html | Full interactive report | coverage/index.html |
TextSummary | Plain text summary | coverage/Summary.txt |
MarkdownSummaryGithub | GitHub-compatible markdown | coverage/SummaryGithub.md |
Badges |
The HTML report includes a Risk Hotspots section showing methods sorted by complexity:
Risk Hotspots
─────────────
Method Complexity Coverage Crap Score
──────────────────────────────────────────────────────────────────
DataImporter.ParseRecord() 54 52% 12.4
AuthService.ValidateToken() 32 0% 32.0 ← HIGH RISK
OrderProcessor.Calculate() 28 85% 1.3
UserService.CreateUser() 15 100% 0.0
Action items:
ValidateToken() has CRAP > 30 with 0% coverage - test immediately or refactorParseRecord() is complex but has decent coverage - acceptableCreateUser() and Calculate() are well-tested - safe to modify| Coverage Type | Target | Action |
|---|---|---|
| Line Coverage | > 80% | Good for most projects |
| Branch Coverage | > 60% | Catches conditional logic |
| CRAP Score | < 30 | Maximum for new code |
Create coverage.props in your repository:
<Project>
<PropertyGroup>
<!-- Coverage thresholds for CI enforcement -->
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>
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
- 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'
# Full analysis workflow
rm -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 summary
cat 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
| Metric | New Code | Legacy Code |
|---|---|---|
| Line Coverage | 80%+ | 60%+ (improve gradually) |
| Branch Coverage | 60%+ | 40%+ (improve gradually) |
| Maximum CRAP | 30 | Document exceptions |
| High-risk methods | Must have tests | Add tests before modifying |
The recommended coverage.runsettings excludes:
| Pattern | Reason |
|---|---|
[*.Tests]* | Test assemblies aren't production code |
[*.Benchmark]* | Benchmark projects |
[*.Migrations]* | Database migrations (generated) |
GeneratedCodeAttribute | Source generators |
CompilerGeneratedAttribute | Compiler-generated code |
ExcludeFromCodeCoverageAttribute |
Lower thresholds temporarily for:
Never lower thresholds for:
Weekly Installs
96
Repository
GitHub Stars
495
First Seen
Jan 29, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code77
codex60
github-copilot57
opencode57
gemini-cli56
kimi-cli53
后端测试指南:API端点、业务逻辑与数据库测试最佳实践
11,800 周安装
| 45 |
| 20% |
| 45 x (1 - 0.20)^2 |
| 28.8 |
ImportData() | 80 | 10% | 80 x (1 - 0.10)^2 | 64.8 |
| Don't count auto-properties as branches |
| SVG badges for README |
coverage/badge_*.svg |
Cobertura | Merged Cobertura XML | coverage/Cobertura.xml |
| Explicit developer opt-out |
*.g.cs, *.designer.cs | Generated files |
*.razor.g.cs | Blazor component generated code |
*.razor.css.g.cs | Blazor CSS isolation generated code |
**/Migrations/**/* | EF Core migrations (auto-generated) |
SkipAutoProps | Auto-properties (trivial branches) |