mcpField
C
crap-score

crap-score

github.com/dotnet测试与质量5.8k3.7k 次安装1 天前更新
代码仓库

关于 crap-score

用于分析 .NET 方法 CRAP 分数的技能,帮助开发者结合圈复杂度与代码覆盖率识别高风险、测试不足的方法。它要求使用真实 Cobertura 覆盖率数据,不会估算覆盖率。

主要功能

  1. 01按方法计算 CRAP 分数
  2. 02结合复杂度与覆盖率评估风险
  3. 03从 Cobertura 提取方法覆盖率
  4. 04手动统计圈复杂度
  5. 05按风险排序并给出改进建议

适用场景

  1. 01评估指定方法的测试风险
  2. 02比较类或文件中的高风险方法
  3. 03根据覆盖率与复杂度安排测试优先级
  4. 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 与 VSTestdotnet test <test.csproj> --collect:"XPlat Code Coverage" --results-directory <results-dir>
Microsoft.Testing.Extensions.CodeCoverage 与 .NET 9 桥接 MTPdotnet test <test.csproj> -- --coverage --coverage-output-format cobertura --coverage-output <output-path>
Microsoft.Testing.Extensions.CodeCoverage 与 .NET 10+ 原生 MTPdotnet 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):

结构示例
ifif (x > 0)
else ifelse if (y < 0)
case(每个)case 1:
forfor (int i = 0; ...)
foreachforeach (var item in list)
whilewhile (running)
do...whiledo { } 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 值较高的方法,提出以下一项或两项建议:

  1. 添加测试——找出未覆盖的分支,并建议具体的测试用例
  2. 降低复杂度——建议对深度嵌套的逻辑进行提取方法重构

计算将方法的 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)。

评论

支持 Markdown