Rust 质量控制
此技能为 Rust 代码设定了一套标准,并以三种模式应用。规则来源于 Rust 风格指南、Rust API 指南、Microsoft 的实用 Rust 指南(2026.6)、Google 的 Rust 风格指南,以及 memchr、jiff、ripgrep、fd 和 dua-cli 中可见的实践。当这些来源有冲突时,references/precedence.md 记录了裁决,这样你就不必重新辩论。
第 1 步:建立上下文(所有模式)
在编写或评判任何代码之前,请执行此步骤。如果前置条件失败,请停止并报告;不要猜测缺少工具链或清单。
- 找到 crate 根目录(最近的
Cargo.toml)。注意它是一个工作区、库、二进制文件(或同时是两者),以及是否发布(publish = false或仅在内部使用,或通过 FFI 表示内部使用)。 - 首先阅读项目自己的规则,因为它们会覆盖此技能的默认值:
rustfmt.toml或.rustfmt.toml,clippy.toml,Cargo.toml中的[lints]和[workspace.lints],edition,rust-version(MSRV),rust-toolchain.toml,CONTRIBUTING.md,AGENTS.md或CLAUDE.md,以及任何AI_POLICY.md。如果AI_POLICY.md或CONTRIBUTING.md限制了 AI 或代理的贡献,请在生成针对该仓库的更改之前告知用户。 - 将代码分类为库或应用程序。API 设计规则对于库是强制性的,对于应用程序是强烈推荐,应用程序应更倾向于直接性而非通用性。
- 注意平台。如果
rust-toolchain.toml列出了目标或代码大量使用#[cfg(windows)]风格的门控来支持不同于当前主机的平台,则也使用--target <triple>运行门控(它会针对该目标进行 lint 和文档化;测试会被跳过)。由于平台门控仅出现在主机上的警告(如死代码)是主机伪影,不是发现。 - 运行一次
scripts/check.sh <crate-root>以记录基线。你需要它来区分自己的回归与已存在的技术债务。当 crate 具有互斥特性时,使用--features "<list>"而不是默认的--all-features。
除非用户要求,否则不要安装工具链、组件或工具(nightly、Miri、cargo-hack),也不要使用网络(除了获取 crate)。当检查需要缺失的东西时,请告知并推荐。
第 2 步:选择模式
创建 当用户想要新的函数、方法、类型、模块或 crate 时。遵循下面的“创建代码”。
修改 当用户想要更改现有代码时:功能改进、优化、缺陷修复、重构或审计发现的问题修复。遵循下面的“修改代码”。
审计 当用户想要审查、评估或检查现有代码,识别问题并提出修复建议时。遵循下面的“审计代码”。审计不会更改代码,除非用户要求;它会生成发现问题列表。
一个请求可以跨模式(例如“审计此模块并修复你能找到的问题”)。首先运行审计并报告,然后切换到修改模式进行修复。
不可谈判的规则
这些规则适用于每种模式。完整的规则集(带有 ID)列在末尾的参考文件中。
- 格式化是 rustfmt 的工作。 决不手动格式化以对抗 rustfmt。除非为了刻意对齐(表格、矩阵)或生成的代码,决不添加
#[rustfmt::skip]。 - 代码是内存安全的。 安全代码必须不能导致未定义行为。每个
unsafe块的正上方都有一个// SAFETY:注释,解释每个前置条件为何成立。每个unsafe fn和unsafe trait都有一个# Safety文档章节。仅在 FFI、经基准测试验证的性能需求或新颖抽象中使用unsafe,并且决不要用它来绕过Send、Sync或生命周期。参见references/unsafe.md。 - 错误与 panic。 由输入或环境引起的可恢复故障返回
Result(或Option,当原因明显时)。检测到的编程错误会 panic。库代码决不会因可能合理地无效的调用者输入而 panic。参见references/errors.md。 - 库错误让调用者无需解析文本即可分支。 具体的错误类型,带有返回公共
#[non_exhaustive]枚举的kind()方法,或is_*()谓词。私有字段没问题;没有访问器的私有 kind 是一个缺陷。消息指向有问题的 token 和位置,并且决不复制整个输入。 - 生产代码中没有裸的
unwrap()。 使用?、模式匹配或expect("..."),其消息说明为何不变量成立。测试中可以使用unwrap()。文档示例使用?,并带隐藏的# Ok::<(), Box<dyn std::error::Error>>(())行。 - 每个新依赖都是一项成本,包括开发依赖。 仅当它消除了真正的复杂性或提供你无法合理编写的正确性时才添加依赖(Unicode 大小写折叠是一个好理由;为一个 100 行的解析器使用属性测试框架则不是)。在你的报告中说明原因。
- 结构匹配规模。 不要为一个小函数及其错误类型创建模块;模块需要证明其文件存在,即一个独特的概念和多个项或几百行代码。
- Lint 覆盖使用
#[expect(lint, reason = "...")],而不是#[allow],以便过时的覆盖会显现。 - 公共项都有文档,第一行大约 15 个词,函数包含
# Errors、# Panics和# Safety章节(如果适用)。参见references/documentation.md。 - 公共类型实现
Debug。 旨在供人阅读的类型也实现Display。 - 注释解释为什么,而不是什么。 如果注释重述了代码,则删除它。ripgrep 在
crates/core/main.rs中的分配器注释是典范:决策、原因以及适用的条件。 - 注释或文档中不使用 ASCII 艺术图或制表符绘制字符。 用句子描述结构;绘图在屏幕阅读器中不可读,并且在代码更改时会腐烂。(团队约定,非上游。)
- 文档中没有设计日志内容。 文档描述当前行为,而不是代码如何产生或遵循了哪些指南。
- 测试验证行为。 没有重述常量或镜像实现的测试。
创建代码
从外到内工作:先类型和签名,然后文档,然后实现,然后测试。在函数体之前编写文档注释是发现令人困惑的 API 的最快方式。
- 放置它。 将项放在拥有其概念的模块中;在小 crate 中通常是
lib.rs。新模块以其主要类型命名,并且仅在代码需要时才创建(组织规则 19)。参见references/organization.md。 - 用类型建模。 对容易混淆或携带不变量的值使用新类型,并附带强制该不变量的构造函数。使用
enum而不是含义在调用点不明显的bool参数。使用命名字段而不是元组。使无效状态不可表示。 - 塑造签名。 借用你仅读取的内容(
&str、&[T]、&Path、Option<&T>),仅在你存储或消费时才获取所有权。在库中,接受impl AsRef<Path>或impl IntoIterator以减少摩擦;在应用程序中,保留具体类型。对于具有多个可选设置的类型使用构建器,在build()中验证。参见references/api-design.md。 - 命名它。 RFC 430 命名规范;
as_/to_/into_转换;new、with_*、try_new构造函数;foo()获取器。没有含糊词(Manager、Service、Factory、Helper、Util),并且没有与同一接口中已有含义冲突的名称(命名规则 9)。 - 按
references/documentation.md文档化它,包括公共项的可运行示例,以及返回的任何位置或长度的单位。 - 实现它。 单一用途函数。当组合器链需要条件或外部状态时,使用普通的
for循环;当组合器可以替换手写逻辑时使用组合器(any、position、sum)。在推断会让读者猜测的地方添加类型注解。人类文本使用 Unicode 感知处理:不区分大小写的匹配使用 Unicode 大小写折叠,或记录其不足之处(API 规则 27)。 - 按
references/errors.md处理错误:库中使用具体的错误类型,带有公共 kind 或谓词;应用程序中使用anyhow和.context()。 - 按
references/testing.md测试它:内联单元测试、tests/下的集成测试,以及不需要新依赖的广泛不变量检查(枚举短输入,或以种子循环对抗一个简单的参考实现)。 - 使用下面的门控验证。
修改代码
目标是做出最小的正确更改,使被触及的代码至少和你发现时一样清晰。
- 编辑前先阅读。 阅读整个函数、其调用者、其测试以及相邻的 API。识别本地约定(导入风格、错误类型、文档标题风格如
# Example与# Examples、行宽、密度、参数和键名)并匹配它们,即使与此技能的默认值不同。 - 缺陷修复从一个覆盖该缺陷类的失败测试开始。 重现报告的输入,然后添加相同的根本原因会破坏的相邻案例(更多元素、其他顺序、组合)。观察它们失败,修复根本原因,观察它们通过。如果项目将回归测试放在一处(如 ripgrep 的
tests/regression.rs),则把你的测试添加在那里。 - 修复原因,而不是症状。 一个新的
unwrap_or_default()、clone()或使失败沉默的早期返回不是修复,除非你能解释为什么原始路径是错误的。在修复处添加一行注释,如果原因从代码中不明显。 - 保持手术刀式精确。 不要重命名、重新排序或重新格式化你没有被要求触及的代码。列出无关问题给用户,而不是默默地修复它们。
- 保持公共 API 和 ABI 稳定,除非用户批准了破坏性变更。向带有公共字段的公共结构添加字段、向穷尽公共枚举添加变体、添加必需的 trait 方法或更改 FFI 契约都是破坏性的。
- 随代码更新文档和测试。 更改签名或行为意味着在同一更改中更改
# Errors、# Panics、示例和测试。 - 格式化: 如果基线是 rustfmt 清理过的,则运行
cargo fmt。如果不是,则仅格式化你更改的部分并告知用户。如果rustfmt.toml使用了仅 nightly 可用的选项(稳定版 rustfmt 会警告它们“只在 nightly 频道可用”),则项目的标准是cargo +nightly fmt;如果已安装则使用它,否则不自动格式化任何内容并告知用户,因为稳定版 rustfmt 会按照项目的设置重写代码。 - 审计的修复: 按严重性顺序修复发现问题,每个逻辑更改修复一个发现,并在你的摘要中命名该发现。
- 使用下面的门控验证,并确认你没有相对于基线引入新的警告,无论你在第 1 步中检查了哪个目标。
审计代码
-
运行
scripts/check.sh --audit <crate-root>(加上第 1 步中的--target)。--audit遍报告,包括公共 API 可达性、未文档化的unsafe、缺失文档和# Errors/# Panics章节、测试之外的unwrap、没有原因的#[allow]、dbg!/todo!、全局导入以及没有消息的断言。简洁地报告这些卫生结果:每个类别(文档、Debug、SAFETY 注释、lint 配置)一个次要发现,每个发现带一个计数和最重要的少数实例。仅因为此处的工具链比项目 CI 或rust-toolchain.toml使用的更新而导致的门控步骤失败是工具链漂移:一个次要发现,不是主要发现。 -
运行
python3 -I scripts/audit_scan.py <crate-root>以检测 clippy 无法看到的问题:全局重导出、含糊词类型名、ASCII 艺术注释、设计历史文档、参数表、裸 TODO、空的expect消息以及库代码中的打印宏。通过阅读代码确认每个命中;丢弃误报。 -
像用户那样探测行为。 这是在实际 crate 中发现大多数用户可见的错误的地方,所以把审计的大部分时间花在这里和第 5 步。阅读 crate 的文档、规范和测试,列出其声称处理的输入特性。对于每个特性,构建现有测试未覆盖的一个小的真实输入,通过工作区外的一个一次性 crate 的公共 API 运行它,并将输出与用户应得到的结果进行比较。覆盖格式允许但示例跳过的问题:
- 继承、间接或默认值
- 重复项
- 可选或隐藏部分
- 其他工具能容忍的轻微损坏
- 非 ASCII 文本和遗留编码
- 大但合理的尺寸
对每个确认的问题,附带显示它的输入报告。
-
处理不受信任的输入。 对于每个解析不受信任的字节或文本的公共函数,在相关字母表上提供每个短输入,或几百万个种子随机变异,并捕获 panic。还要尝试使工作量增长快于输入大小的构造(共享或递归引用、深层嵌套),并设置超时。
-
阅读代码以找出工具无法判断的问题:每个
unsafe块的内存安全性、错误设计、API 形状、模块边界、命名、测试质量以及注释是否解释了原因。在与操作系统对话的代码中,还要检查:- 在系统调用、I/O 或环境失败时 panic(
assert!或unwrap于返回值、非 UTF-8 环境变量或 passwd 字段) - 在
fork和exec之间的代码(pre_exec闭包)或信号处理程序中分配、锁定、格式化、日志或退出的代码,这些都非异步信号安全 - 泄漏的文件描述符或句柄,以及发送到可能已被回收并重用的进程 ID 的信号
- 对尺寸、计数和坐标进行窄化转换和未检查的算术
使用
references/audit-checklist.md。 - 在系统调用、I/O 或环境失败时 panic(
-
使用下面的格式报告。为每个发现提出具体修复。
第 1 步和第 2 步只是一个快速基准,不是审计:在这些步骤上花费不超过大约五分之一的时间。一个主要列出 lint、文档的审计是半个审计——在第 3 步和第 5 步中缺失了最需要人类判断的问题。
验证门控
在完成更改后,根据模式运行验证:
对于 创建 和 修改:
scripts/check.sh <crate-root>
对于 审计:
scripts/check.sh --audit <crate-root>
还要确保 scripts/ 目录中的任何自定义脚本在环境中可用。
报告格式
对于审计,使用以下格式报告发现:
## 审计报告:<模块或 crate 名称>
### 严重性:<主要/次要/建议>
**发现问题 1:** <问题标题>
- 位置:`<文件路径>:<行号>`
- 描述:<问题解释>
- 建议修复:<具体代码更改或说明>
对于修改和创建,提供一个总结:
## 更改总结
- 修改了什么:<文件和更改>
- 原因:<理由>
- 影响:<对行为的影响>
- 验证:<如何测试>
参考文件
此技能依赖 references/ 目录中的文件,这些文件提供了基础规则和背景:
references/precedence.md- 冲突源的决策references/unsafe.md-unsafe代码的最佳实践references/errors.md- 错误设计指南references/documentation.md- 文档标准references/organization.md- 模块结构指南references/api-design.md- API 设计原则references/testing.md- 测试策略references/audit-checklist.md- 审计检查清单