关于 crap-score
用于分析 .NET 方法 CRAP 分数的技能,帮助开发者结合圈复杂度与代码覆盖率识别高风险、测试不足的方法。它要求使用真实 Cobertura 覆盖率数据,不会估算覆盖率。
主要功能
- 01按方法计算 CRAP 分数
- 02结合复杂度与覆盖率评估风险
- 03从 Cobertura 提取方法覆盖率
- 04手动统计圈复杂度
- 05按风险排序并给出改进建议
适用场景
- 01评估指定方法的测试风险
- 02比较类或文件中的高风险方法
- 03根据覆盖率与复杂度安排测试优先级
- 04检查覆盖率不足是否推高风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill crap-score安装后在 Claude Code 中输入/crap-score确认已加载。
详细介绍
CRAP 分数分析
计算 .NET 方法的 CRAP(变更风险反模式)分数,以识别复杂且测试不足的代码。
背景
CRAP 分数将圈复杂度和代码覆盖率结合为一个指标:
$$\text{CRAP}(m) = \text{comp}(m)^2 \times (1 - \text{cov}(m))^3 + \text{comp}(m)$$
其中:
- $\text{comp}(m)$ = 方法 $m$ 的圈复杂度
- $\text{cov}(m)$ = 方法 $m$ 的代码覆盖率(0.0 到 1.0)
| CRAP 分数 | 风险级别 | 说明 |
|---|---|---|
| < 5 | 低 | 简单且测试充分 |
| 5 到 < 15 | 中等 | 对大多数代码而言可以接受 |
| 15 到 30 | 高 | 需要增加测试或简化代码 |
| > 30 | 严重 | 应立即重构并提高覆盖率 |
覆盖率为 100% 的方法,其 CRAP = 复杂度(最低值)。覆盖率为 0% 的方法,其 CRAP = 复杂度^2 + 复杂度。
适用场景
- 用户想评估哪些方法因覆盖率低且复杂度高而存在风险
- 用户要求计算特定方法、类或文件的 CRAP 分数
- 用户想根据覆盖率和复杂度风险,确定指定方法、类或文件中接下来应优先测试的内容
- 用户想在简单的覆盖率百分比之外,评估测试质量
不适用场景
- 用户只想运行测试(使用
run-tests技能) - 用户想编写新测试(使用
code-testing) - 用户只想查看覆盖率百分比,不需要复杂度分析
- 用户想进行项目范围的覆盖率/CRAP 分析或确定优先级(使用
coverage-analysis)
输入
| 输入 | 必需 | 说明 |
|---|---|---|
| 目标范围 | 是 | 要分析的方法名、类名或文件路径 |
| 测试项目路径 | 否 | 测试项目的路径。默认自动发现解决方案中的测试项目。 |
| 源项目路径 | 否 | 要分析的源项目路径 |
工作流
步骤 1:收集代码覆盖率数据
如果用户提供了有效的 Cobertura 报告,且其中包含所请求的目标,则直接使用该报告,不要重新运行测试。如果提供的报告格式错误、内容为空、内部矛盾或缺少目标,则将其视为无效输入:在可能的情况下,使用与仓库兼容的命令重新生成报告;如果无法收集,则请求有效报告。否则,调用 run-tests,判断仓库的测试平台并确认兼容的命令形式,然后要求使用能够生成 Cobertura 报告的命令:
| 覆盖率提供程序 | Cobertura 命令 |
|---|---|
coverlet.collector 与 VSTest | dotnet test <test.csproj> --collect:"XPlat Code Coverage" --results-directory <results-dir> |
Microsoft.Testing.Extensions.CodeCoverage 与 .NET 9 桥接 MTP | dotnet test <test.csproj> -- --coverage --coverage-output-format cobertura --coverage-output <output-path> |
Microsoft.Testing.Extensions.CodeCoverage 与 .NET 10+ 原生 MTP | dotnet test --project <test.csproj> --coverage --coverage-output-format cobertura --coverage-output <output-path> |
如果项目定义了由仓库提供的等效命令,则使用该命令。如果收集器会创建 GUID 子目录,则递归搜索结果目录。没有可用转换器时,不要用通用二进制 .coverage 命令替代。
不要在首次遇到还原、编译、测试或收集器失败时就停止。判断失败发生在哪一层,检查命令生成的所有报告,并在请求输入前尝试完所有不持久的重试。安全的重试方式包括:使用不会更改源文件和清单的命令行 MSBuild 属性,以及使用已安装或由仓库提供的替代收集器。如果通过可逆的命令行设置即可编译相同源代码,那么简单的源代码错误属于收集阻碍,而不是最终分析结果。绝不要将空的 Cobertura 文件称为成功的备用方案。
对于经典的非 SDK 项目(ToolsVersion、显式编译项或 packages.config),只使用仓库提供且能生成 Cobertura 的覆盖率命令。如果没有此类命令,则请求 Cobertura XML 并停止;不要迁移项目,也不要注入 SDK 风格的提供程序。CRAP 分数始终需要真实覆盖率数据。
绝不估算覆盖率
猜测覆盖率会导致 CRAP 分数错误,这比不给出结果更糟。 对于没有仓库覆盖率命令或现有报告的经典项目,请在此处停止并请求 Cobertura。
除非用户明确授权更改依赖项或工具,否则不要添加覆盖率包、修改项目清单或安装全局工具。如果仓库没有可用的提供程序或转换器,则报告确切的前置条件,以及通过 run-tests 确认的兼容命令形式,然后停止。如果存在现有二进制 .coverage 报告,则只能使用已安装或由仓库提供的转换器进行转换;否则,请求授权或 Cobertura 导出。如果测试执行失败,但仍生成了有效覆盖率数据,则使用这些数据继续分析,并注明失败情况。如有帮助,可以单独报告复杂度,但绝不要发布基于假定覆盖率得出的 CRAP 分数。
使用报告前,请确认报告可以解析,至少包含一个类和一个方法,并且包含所请求的目标。空报告或缺少目标的报告表示收集或筛选失败,不代表覆盖率为 0%。如果可能,重新生成覆盖率;否则停止,不要发布 CRAP 分数。
如果用户提供了现有报告,请说明该报告未重新生成。除非在本次分析中运行仓库的覆盖率命令,确认报告来源,否则不要称其数据为当前数据。
步骤 2:计算圈复杂度
优先使用仓库提供的代码度量报告中的机器生成的逐方法复杂度,或使用 Cobertura 方法的 complexity 属性(前提是该报告与当前源代码对应)。Microsoft.CodeAnalysis.Metrics 可以通过 msbuild /t:Metrics 生成方法级别的 CyclomaticComplexity 数据,但未经用户批准,不要添加该包或修改项目。
如果不存在机器生成的度量数据,请分析当前目标源代码,并将结果标记为手动复杂度计数。计算以下决策点(每个决策点在基础复杂度 1 上加 1):
| 结构 | 示例 |
|---|---|
if | if (x > 0) |
else if | else if (y < 0) |
case(每个) | case 1: |
for | for (int i = 0; ...) |
foreach | foreach (var item in list) |
while | while (running) |
do...while | do { } while (cond) |
catch(每个) | catch (Exception ex) |
&& | if (a && b) |
||(或) | if (a || b) |
?? | value ?? fallback |
?. | obj?.Method() |
? :(三元运算符) | x > 0 ? a : b |
| 模式匹配分支 | x is > 0 and < 10 |
每个方法的基础复杂度为 1。每个决策点加 1。
手动计数时,请阅读源文件,报告逐结构的计数明细,不要以源代码注释作为证据。计算每一处出现,包括嵌套在参数或返回表达式中的运算符;在宣布存在冲突前,专门重新检查 &&、||、??、?.、三元运算符以及 switch/模式分支。如果提供的报告与当前方法对应,且仔细重新计数后结果一致,请明确采用其度量值。如果仍存在实质性分歧,请标明两种来源;如果用户明确要求使用该报告,请根据其机器生成的度量值计算主要 CRAP 结果,并将手动计数作为注意事项,而不要因此拒绝给出所请求的结果。
步骤 3:从 Cobertura XML 提取逐方法覆盖率
解析 Cobertura XML,在目标 <class> 元素下查找每个方法的 line-rate 属性。如果方法级别没有 line-rate,请根据 <lines> 元素计算:
$$\text{cov}(m) = \frac{\text{命中数} > 0 的行数}{\text{总行数}}$$
Cobertura 中的方法名可能与源代码中的名称不同(异步方法、lambda)。名称无法匹配时,请根据行号范围进行匹配。
如果 line-rate 和 <lines> 同时存在,请重新计算命中率并进行比较。仅允许正常的报告舍入误差(一个百分点);差异更大则说明报告自相矛盾。请重新生成报告,或报告冲突并停止,不要计算 CRAP。绝不能默默选择更符合预期分数的值。
步骤 4:计算 CRAP 分数
对范围内的每个方法应用以下公式:
$$\text{CRAP}(m) = \text{comp}(m)^2 \times (1 - \text{cov}(m))^3 + \text{comp}(m)$$
使用计算器或脚本进行运算,并展示代入的复杂度和覆盖率。不要心算公式。对于指定的方法,直接分析该方法;只有当所请求的范围是类或文件时,才分析无关方法。确定所请求的结果后,不要附加假设性的重构分数或覆盖率目标,除非用户提出要求。任何数值示例也必须由计算器或脚本得出。
步骤 5:呈现结果
按 CRAP 从高到低排序并以表格呈现:
| Method | Complexity | Coverage | CRAP Score | Risk |
|---------------------------------|------------|----------|------------|----------|
| OrderService.ProcessOrder | 10 | 45% | 26.6 | High |
| OrderService.ValidateItems | 8 | 90% | 8.1 | Moderate |
| OrderService.CalculateTotal | 3 | 100% | 3.0 | Low |
包含以下内容:
- 摘要:分析的方法总数,以及各风险类别的方法数量
- 主要问题项:CRAP > 30 的方法,并提供具体建议
- 快速改进项:复杂度较高,但少量提高覆盖率即可显著降低分数的方法
第 6 步:提供可执行的建议
对于 CRAP 值较高的方法,提出以下一项或两项建议:
- 添加测试——找出未覆盖的分支,并建议具体的测试用例
- 降低复杂度——建议对深度嵌套的逻辑进行提取方法重构
计算将方法的 CRAP 值降至 15 以下所需的覆盖率:
$$\text{cov}_{\text{needed}} = 1 - \left(\frac{15 - \text{comp}}{\text{comp}^2}\right)^{1/3}$$
此公式仅适用于 comp < 15 的情况。当 comp >= 15 时,即使覆盖率达到 100%,CRAP 值也只能达到 comp 本身,仍然达到或超过阈值。因此,仅提高覆盖率无法将 CRAP 值降至阈值以下——必须先重构方法以降低其圈复杂度。
报告时请使用以下格式:"要将 ProcessOrder(复杂度为 10)的 CRAP 值降至 15 以下,需将覆盖率从 45% 提高到 63.2% 以上(以整数百分比报告时至少为 64%)。" 对于仅凭复杂度就超过阈值的方法,请报告:"ComplexMethod(复杂度为 18)无法仅通过测试达到 CRAP < 15——请通过提取子方法降低复杂度。"
验证
- 验证覆盖率数据已成功收集(Cobertura XML 文件存在且包含数据)
- 确认目标方法存在;找不到该方法并不能证明其覆盖率为 0%
- 确认每个覆盖率数据都来自该 XML——不得使用估算值、假设值或根据源码注释推导的数值
- 如果方法同时具有
line-rate和行命中率,请交叉核对 - 交叉核对覆盖率数据中的方法名称与源代码中的名称
- 使用计算器或脚本输出确认 CRAP 值
- 确保覆盖率为 100% 的方法,其 CRAP 值恰好等于其复杂度
常见陷阱
- 收集失败时估算覆盖率:绝不要这样做——由此得出的 CRAP 值会在关键方向上出错。请使用通过
run-tests确认的、与仓库兼容的 Cobertura 路径,然后报告阻碍因素。 - 将空报告或缺失的方法视为覆盖率为 0%:这是收集、筛选或方法映射失败;不要凭空编造分数。
- 相信相互矛盾的 Cobertura 字段:比较
line-rate与行命中率;如果两者差异超出舍入误差,请停止。 - 相信源码中过时的复杂度注释:根据当前代码计算圈复杂度;前一位作者留下的
// complexity: 7注释并非证据。 - 靠心算计算 CRAP 值:请使用计算器或脚本,并展示代入的数值。
- 更改工具以绕过收集器错误:报告仓库兼容命令执行失败以及缺少的前置条件;未经明确授权,不要安装全局收集器或编辑清单文件。
- 覆盖率数据过时:当用户要求当前结果或源代码/二进制文件已更改时,重新生成数据;否则,请说明所提供的报告未重新生成。
- 方法名称不匹配:Cobertura XML 可能会为异步方法、lambda 或局部函数使用改编后的名称或编译器生成的名称。名称不匹配时,请根据行范围进行匹配。
- 生成代码:除非另有明确要求,否则请从分析中排除自动生成的文件(例如
*.Designer.cs、*.g.cs)。