cargo-crap:在AI生成的Rust代码中发现未测试的复杂度

Lobsters Hottest 工具

摘要

cargo-crap 是一个 Rust 工具,它使用 CRAP 指标来识别既复杂又测试不足的函数,帮助开发者管理 AI 生成代码中的风险。

<p><a href="https://lobste.rs/s/saldxa/cargo_crap_finding_untested_complexity">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/18 22:35

# cargo-crap:发现 AI 生成 Rust 代码中的未测试复杂度 来源:https://minikin.me/blog/cargo-crap Rust 让许多 bug 变得不可能出现:内存安全、线程安全、所有权、生命周期、穷尽匹配、强类型。 但 Rust 无法回答一个关键的维护问题:这段代码修改起来安全吗? 一个函数可以完美编译,但触碰它仍然有风险。它可能有过多的分支、过多的特例、过多的隐藏路径,以及不够的测试来给你信心。 这就是我构建 `cargo-crap` (https://github.com/minikin/cargo-crap) 的原因。它是一个 Rust 工具,通过计算**变更风险反模式**(CRAP)指标,来发现那些既复杂又测试不足的函数。 覆盖率展示了代码的哪些部分被测试执行过。复杂度展示了函数中可能存在的路径数量。CRAP 结合了这两个信号,突出显示修改起来有风险的代码。 我把它构建为 AI 辅助 Rust 开发的一道小型护栏:智能体可以快速行动,但我们仍然需要对其引入的复杂度进行可衡量的检查。 ### 仅靠覆盖率的问题 代码覆盖率很有用,但可能会误导人。 一个覆盖率为 `0%` 的小辅助函数虽然不理想,但可能不是系统中最大的风险: ```rust fn is_empty(value: &str) -> bool { value.trim().is_empty() } ``` 现在,与一个解析输入、验证业务规则、处理多个边界情况、改变状态并且有多个嵌套分支的大型函数相比。如果那个函数覆盖率为 `0%`,风险就完全不同了。 仅靠覆盖率无法告诉你这一点。它只能告诉你测试期间执行了什么。它无法告诉你代码有多难理解,或者有多少路径穿过它。 ### 仅靠复杂度的问题 圈复杂度衡量一个函数中独立路径的数量。每个 `if`、`match`、`loop` 和分支都会增加可能的路径。 这是有用的信息,但仅靠复杂度也不够。 有些代码天生复杂。解析器、状态机、协议处理器、验证逻辑和兼容层经常分支,因为领域本身就需要分支。 如果这些代码经过了良好测试,风险就会降低。 **真正的问题是未测试的复杂度**,而不是复杂度本身。 ### CRAP 指标 CRAP 指标由 Alberto Savoia 和 Bob Evans 在 2007 年与 crap4j (https://www.artima.com/weblogs/viewpost.jsp?thread=215899) 一起引入,后来 Savoia 在 Google 测试博客 (https://testing.googleblog.com/2011/02/this-code-is-crap.html) 上更详细地描述了这一想法。它将圈复杂度和测试覆盖率结合成一个数字: ``` CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m) ``` 其中: - `comp(m)` 是函数的圈复杂度 - `cov(m)` 是该函数的测试覆盖率百分比 你不需要记住公式。直觉很简单: - 简单且经过良好测试的代码得分低 - 复杂但经过良好测试的代码得分中等 - 简单但未测试的代码不理想,但通常可控 - 复杂且未测试的代码得分高得多 最后一类是最重要的,因为那是代码变更变得危险的地方。 降低高 CRAP 分值有两种直接方法:减少函数的复杂度,或围绕重要路径添加有意义的测试。这个数字不是为了羞辱某个函数。它展示了在哪里进行重构或测试工作能最大程度地降低风险。 ### 为什么这对 AI 智能体来说更重要 AI 智能体正成为日常软件开发的一部分。它们可以生成代码、重构函数、添加测试、更新 API,并非常快速地在代码库中移动。 这种速度是把双刃剑。它也可能使代码比以前更复杂或测试更少。 随着每个自动生成的分支、异常或回退,AI 智能体会逐渐使函数更难理解和测试。 我经常看到的一种模式是“累积式保留”:智能体不会简化模型,而是添加另一个回退、另一个特例或另一个兼容分支,以保持当前行为正常运行。测试可能仍然通过,但函数已经变得难以推理。代码可以编译,但系统已经变得更难安全地修改。 `cargo-crap` 充当了 **AI 辅助开发的边界**,当分数超过你选择的阈值时发出警报。 > 你可以修改代码,但 `cargo-crap` 能让高风险、未测试复杂度的无声增加变得可见。 当代码不仅由人类修改,还由 **AI 智能体** 修改时,这个边界更加重要。 我期望的工作流程很简单: 1. 让智能体实现变更。 2. 运行测试和覆盖率。 3. 运行 `cargo-crap`。 4. 如果分数增加了,要求智能体要么简化函数,要么添加有意义的分支覆盖。 ### `cargo-crap` 的功能 `cargo-crap` 将此指标作为 Cargo 风格的工具引入 Rust 生态系统。 如果你使用 cargo-binstall (https://github.com/cargo-bins/cargo-binstall),你可以安装预构建的二进制文件: ``` cargo binstall cargo-crap ``` 或者从源码安装: ``` cargo install cargo-crap ``` 基本工作流程是两个命令: ``` cargo llvm-cov --lcov --output-path lcov.info cargo crap --lcov lcov.info ``` 1. 首先,生成一个 `LCOV` 覆盖率报告。 2. 然后 `cargo-crap` 分析你的 Rust 源码,计算每个函数的复杂度,将其与覆盖率数据结合,并打印一个排名报告: ``` ┌───┬───────┬────┬───────────────────┬──────────┬───────────────┐ │ │ CRAP │ CC │ Coverage │ Function │ Location │ ╞═══╪═══════╪════╪═══════════════════╪══════════╪═══════════════╡ │ ✗ │ 156.0 │ 12 │ ░░░░░░░░░░ 0.0% │ crappy │ src/lib.rs:24 │ │ ▲ │ 6.7 │ 4 │ ████░░░░░░ 44.4% │ moderate │ src/lib.rs:12 │ │ ✓ │ 1.0 │ 1 │ ██████████ 100.0% │ trivial │ src/lib.rs:8 │ └───┴───────┴────┴───────────────────┴──────────┴───────────────┘ ✗ 1/3 function(s) exceed CRAP threshold 30. ``` 无需扫描大型覆盖率报告并进行猜测,`cargo-crap` 会为你提供一个聚焦的、排名的函数列表,这些函数需要关注。 默认阈值是 `30`,遵循原始的 CRAP 指导:一旦函数超过这条线,就值得将其视为高风险变更目标,而不是普通的背景噪音。 ### 一个小例子 想象三个函数: | 函数 | 复杂度 | 覆盖率 | CRAP | |----------|--------|--------|-------| | trivial | 1 | 100% | 1.0 | | moderate | 4 | 50% | 6.0 | | risky | 12 | 0% | 156.0 | 第一个函数直接了当且经过全面测试。没什么好担心的。 第二个函数有一些分支和部分覆盖率。可能值得改进,但并非迫切需要关注。 第三个函数是有趣的那个。复杂度为 12 表明代码中有许多路径。覆盖率为 0% 时,每一次变更都变成了一个小小的信念跳跃。 这就是你可能希望在它给你带来生产环境惊喜之前检查的函数。 以下是 `cargo-crap` 旨在使其可见的函数形状的一个例子: ```rust fn classify_event(kind: &str, retry_count: u8, source: Option<&str>) -> &'static str { if kind == "payment_failed" { if retry_count > 3 { return "manual_review"; } if source == Some("partner") { return "partner_retry"; } return "retry"; } if kind == "payment_succeeded" { return "complete"; } if kind == "refund_requested" && source != Some("internal") { return "review_refund"; } "unknown" } ``` 这个函数并不算大,Rust 会很乐意编译它。但它已经拥有多个路径、早期返回以及隐藏在条件内的业务规则。如果覆盖率漏掉了大多数这些路径,风险就不是理论上的。下一个修改它的人或智能体必须猜测哪些分支是重要的。 以下是来自 PR #17 (https://github.com/minikin/cargo-crap/pull/17#issuecomment-4411828962) 的摘录,该 PR 为 `cargo-crap` 添加了 SARIF 2.1.0 输出: PR #17 那个评论改变了审查对话。不再问“PR 看起来没问题吗?”,你可以问更具体的问题: - 新的输出格式是否增加了必要的分支,或者分发逻辑是否可以更简单? - 回归是否可以接受,因为受影响的函数仍然被完全覆盖? - 这个阈值或基线是否应该吸收它,或者实现是否应该重塑? 这就是实用价值。它不是要取代审查,而是为审查提供了一个更清晰的起点。 ### 这个工具不是什么 - 它不是工程判断的替代品。 - 它不理解你的业务领域。 - 它不能证明你的测试是好的。 覆盖率可以执行一行代码而不断言正确的行为。一个函数可以完全覆盖但仍然测试得很差。 因此,CRAP 分数不应被视为绝对真理。它是一个信号——一个有用的信号。 这个工具的最佳用途是提出更好的问题: - 为什么这个函数如此复杂? - 这种复杂度是本质的还是偶然的? - 测试是否覆盖了重要的分支? - 我们能否将其拆分成更小的部分? - 这个逻辑是否应该更明确地建模? 好的工具不会取代思考。它们能让思考更容易集中。 这对于拥有不断增长的 Rust 代码库、大型重构、生成代码或 AI 辅助拉取请求的团队最有用。如果你的代码变化很快,审查时间有限,一个排名列表能给审查者一个具体的起点。 ### 在 CI 中使用它 你可以在本地使用 `cargo-crap`,但我认为它在 CI 中更有用。 对于新项目或小型项目,绝对阈值效果很好。你可以在函数超过阈值时使构建失败: ``` cargo crap --lcov lcov.info --fail-above --threshold 30 ``` 对于现有项目,我对严格阈值会保持谨慎。大多数真实代码库已经有一些遗留的复杂度。如果你立即打开一个严格的关卡,你可能会得到一长串旧的、没人准备好立即修复的问题。 对于成熟的代码库,我会从基线模式开始: ``` cargo crap --lcov lcov.info --format json --output baseline.json cargo crap --lcov lcov.info --baseline baseline.json --fail-regression ``` 这不会假装代码库今天就是完美的。它只是说: > 我们可能已经存在问题,但新的变更不应该让情况变得更糟。 这是一个健康的工程规则。它对 AI 辅助开发特别有用,因为你想要快速迭代又不失去对代码库的控制。 你也可以让输出对审查者更友好。`--format github` 会输出 GitHub Actions annotations,而 `--format pr-comment` 会产生一种在审查期间更容易扫描的拉取请求评论格式。 在 GitHub Actions 中,最简单的阈值门控看起来像这样: ```yaml name: change-risk on: pull_request: push: branches: [main] jobs: cargo-crap: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-llvm-cov - run: cargo install cargo-crap - run: cargo llvm-cov --lcov --output-path lcov.info - run: cargo crap --lcov lcov.info --fail-above --threshold 30 ``` 这故意很小。对于成熟的代码库,从基线模式开始,而不是绝对的门控,然后在最严重的问题被理解后再收紧阈值。 ### 最后的思考 软件质量不仅仅关乎代码能否编译或测试是否存在。它还关乎我们明天能否安全地修改系统。 当 AI 智能体能够比人类审查每一处细节更快地修改代码时,这一点更加重要。 `cargo-crap` 让一个问题变得可见:复杂且测试覆盖率不足的 Rust 代码。 目标不是让每个数字都完美,也不是禁止变更。关键是让有风险的变更在悄悄变得正常之前就被发现。 Rust 给了我们关于程序不能做什么的强大保证。AI 给了我们速度。`cargo-crap` 通过一个简单的规则帮助连接这两个现实: > 快速行动,但要衡量你正在增加的风险。

相似文章

我用Rust构建了一个自托管的上下文赌博机装置,并部署在一个实时的AI交易产品上。在发现运行时错误之前,先找到了自己配置中的两个错误。

Reddit r/ArtificialInteligence

宣布两个开源Rust项目:Lycan(一种用于上下文赌博机的图执行语言)和Syntra(一个自托管的Docker设备,用于服务Lycan胶囊)。作者在自己的实时AI交易产品上自用测试,发现数据管道错误(而非算法问题)主导了适配工作。