关于 rust-code-review
用于 Rust 代码审查的 Skill,引导审查者了解 crate 环境、变更代码及所有权、错误处理、并发和 unsafe 等问题类别。分阶段关卡要求提交发现前提供证据并校准严重性。
主要功能
- 01Edition 与依赖环境检查
- 02所有权与错误处理清单
- 03并发与接口审查
- 04unsafe 代码审查清单
- 05分阶段发现验证关卡
适用场景
- 01跨 Rust edition 审查代码变更
- 02检查所有权与异步行为
- 03提交前验证 unsafe 代码问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ existential-birds/ beagle --skill rust-code-review安装后在 Claude Code 中输入/rust-code-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/async-concurrency.mdcommon-mistakes.mdconcurrency-models.mdconcurrency-primitives.mderror-handling.mdinterface-design.mdlifetime-variance.mdlock-free-patterns.mdmemory-ordering.mdownership-borrowing.mdpatterns-in-the-wild.mdtypes-layout.mdunsafe-deep.md详细介绍
Rust 代码审查
审查工作流
按以下顺序操作,避免误报并发现特定版本的问题:
- 检查
Cargo.toml— 记录 Rust 版本(2018、2021、2024)以及是否设置了 MSRV。Rust 2024 版对 unsafe 语义、RPIT 生命周期捕获、临时值作用域和!类型回退引入了破坏性变更。这决定了哪些模式适用。如果存在工作区结构,也要检查。 - 检查依赖项 — 记录关键 crate(thiserror 与 anyhow、tokio 功能、serde 功能)。这些信息有助于判断预期使用哪些模式。
- 扫描变更文件 — 阅读完整函数,而不仅仅是差异。许多 Rust 漏洞都隐藏在函数的所有权流转中。
- 逐项检查类别 — 按照下面的检查清单逐项检查,并在需要时加载参考资料。
- 提交前验证 — 完成下面的 关卡,包括验证协议关卡,再提交发现。
关卡
这些步骤有先后顺序:不要跳步进行“脑内验证”。每一步都有客观的通过条件,可通过检查磁盘上的文件和自己的阅读路径来满足。
- Crate 上下文 — 在参考特定版本的检查清单条目(Rust 2024、与 MSRV 相关的 API)或依赖项假设之前。通过条件: 打开相关的
Cargo.toml(包或工作区清单),并能用一行说明edition和rust-version(如果已设置)。 - 扩展阅读 — 在报告 Major 或 Critical 发现之前。通过条件: 阅读包含所引用代码行的完整函数、
unsafe代码块或impl/trait 项(而不只是差异片段)。 - 严重性匹配 — 在报告中的每条发现之前。通过条件: Severity 标签符合该问题类别对应的 Severity Calibration,或者使用 Informational 并给出一行理由。
- 验证协议 — 在完成报告之前。通过条件: 已加载 review-verification-protocol 技能,并完成其中适用于本次审查的每个步骤(不要用含糊的“我检查过了”代替)。
输出格式
按以下格式报告发现:
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
快速参考
| 问题类型 | 参考 |
|---|---|
| 所有权转移、借用、生命周期、克隆陷阱、迭代器 | references/ownership-borrowing.md |
| 生命周期变型、协变/不变、内存区域 | references/lifetime-variance.md |
Result/Option 处理、thiserror、anyhow、不透明错误与枚举错误、使用 ? 延迟清理 | references/error-handling.md |
| 异步陷阱、Send/Sync 限制、poll 契约、Pin 机制、取消安全性 | references/async-concurrency.md |
| Send/Sync 语义、原子操作、内存顺序、锁模式 | references/concurrency-primitives.md |
| 内存顺序决策树、栅栏、ABA、凭空出现 | references/memory-ordering.md |
| 手写自旋锁、通道、Arc、seqlock、CAS 重试模式 | references/lock-free-patterns.md |
| 共享内存与工作线程池及 Actor 设计、异步与线程、竞态条件与数据竞争 | references/concurrency-models.md |
| 类型布局、对齐、repr、PhantomData、泛型与 dyn Trait、宽指针、自动 Trait 泄漏 | references/types-layout.md |
| 对象安全、符合人体工学的 Trait 实现、Deref 规范、可失败的析构函数、隐藏契约、is_normal | references/interface-design.md |
| 索引指针、drop 守卫、扩展 Trait、crate 前置模块 | references/patterns-in-the-wild.md |
| 不安全代码、API 设计、derive 模式、clippy 模式 | references/common-mistakes.md |
| 有效性与安全性、drop 检查、may_dangle、来源信息、不安全代码中的 panic 安全、MaybeUninit、Miri | references/unsafe-deep.md |
如需有关性能、指针类型、类型状态、clippy 配置、迭代器、泛型和文档的开发指导,请加载 rust-best-practices 技能。
审查清单
所有权与借用
- 没有为绕过借用检查器而进行不必要的
.clone()(以此掩盖设计问题) - 循环内没有
.clone()——优先在迭代器上使用.cloned()或.copied() - 不通过克隆来避免生命周期注解(显式取得所有权或重构代码)
- 引用具有恰当的生命周期(较短的生命周期可行时,不使用过宽的
'static) - Edition 2024:RPIT(
-> impl Trait)默认捕获作用域内的所有生命周期;使用+ use<'a>精确控制捕获范围 - 函数参数优先使用
&str而非String,使用&[T]而非Vec<T> - 灵活的 API 参数使用
impl AsRef<T>或Into<T> - 没有悬垂引用或移动后使用
- 仅在确实需要共享可变性时使用内部可变性(
Cell、RefCell、Mutex) - 小型类型(≤24 字节)派生
Copy并按值传递 - 所有权不明确时使用
Cow<'_, T> - 集合转换优先使用迭代器链,而非基于索引的循环
- 不要过早调用
.collect()——消费者接受迭代器时,直接传入迭代器 - 求和优先使用
.sum()而非.fold()(编译器优化效果更好) - 回退逻辑涉及分配时,使用
_or_else变体 - Edition 2024:
if let临时值会在if let结束时析构——若代码依赖临时值持续到 else 分支,需重构 - Edition 2024:
Box<[T]>实现了IntoIterator——优先直接迭代,而非先调用into_vec()
错误处理
- 对可恢复错误使用
Result<T, E>,而非panic!/unwrap/expect - 错误类型提供上下文(使用带有
#[error("...")]的 thiserror 或手动实现Display) - 使用
?运算符时,提供适当的From实现或使用.map_err() - 仅在测试、示例或可证明安全的上下文中使用
unwrap()/expect() - 错误变体足够具体,便于调用方采取行动
- 在应用中使用
anyhow,在库中使用thiserror(或明确说明采用其他方案的理由) - 当回退操作涉及分配时,使用
_or_else变体(ok_or_else、unwrap_or_else) - 使用
let-else在失败时提前返回(let Ok(x) = expr else { return ... }) - 使用
inspect_err记录错误,使用map_err转换错误
Trait 和类型
- Trait 保持精简且内聚(单一职责)
- 为类型选择恰当的
derive宏(正确使用Clone、Debug、PartialEq) - 使用 newtype 避免原始类型痴迷(例如,使用
struct UserId(Uuid),而非直接使用Uuid) -
From/Into实现应无损且不可失败;可能失败的转换使用TryFrom - 当不应允许外部实现时,使用封闭 Trait
- 在适用时提供默认实现
- 验证跨线程共享的类型满足
Send + Sync约束 - 在公共 Trait 上使用
#[diagnostic::on_unimplemented],以便用户忘记实现时提供清晰的错误消息
接口设计
- 面向
dyn的 Trait 中的方法不使用按值传递的Self、泛型参数或关联常量(或通过where Self: Sized限定) - 新 Trait 应为
&T、&mut T和Box<T>提供 blanket impl,以支持引用和智能指针参数 - 可迭代类型应为
&Self和&mut Self实现IntoIterator,而不仅是为Self实现 -
Deref仅用于透明转发,绝不用于“继承”——固有方法歧义是一类真实的 Bug - 可能失败的清理操作应使用返回
Result的显式close()/shutdown();Drop仅作为尽力而为的后备方案 - 不要在
Drop中调用block_on(...)或启动新的运行时(这会在异步运行时中导致死锁) - 公共类型应提供编译时测试
fn is_normal<T: Sized + Send + Sync + Unpin>() {},以便在构建时发现自动 Trait 回归 - 标记公共 API 中重新导出的外部类型——下游的主版本升级会成为此 crate 的破坏性变更
- Getter 方法遵循
fn name(&self)这一约定,而非fn get_name(&self)(将get_*保留给返回Option或执行特殊查找的方法) -
as_*表示低开销的引用到引用转换,to_*可能分配,into_*会消耗自身——确认成本与前缀相符 - 为每个公共类型考虑标准派生(
Debug、Clone、Default、PartialEq、Eq、Hash);仅在类型确实轻量且具有值语义时使用Copy(之后移除它会造成破坏性变更)
实际应用中的模式
- 索引指针图使用生成索引(
slotmap::DefaultKey、petgraph::NodeIndex)——Vec::swap_remove后,裸usize会变成孤儿索引 - Drop 守卫绑定到
let _guard = ...,绝不使用let _ = ...(后者会立即销毁,而非在作用域结束时销毁) - 不要在
panic = "abort"下依赖 Drop 守卫进行清理(不会运行析构函数) - 仅当类型来自外部时才使用扩展 Trait;对于自有类型,直接使用固有
impl - 扩展 Trait 的方法不得遮蔽同一类型上已有的常用方法(会造成调用位置歧义)
- 将 crate 的
prelude模块新增内容视为 semver 次版本变更(RFC 1105);尽可能留到主版本发布时添加
并发设计(模型)
- 在设计中明确说明并发模型(共享内存、工作池或 Actor);所用原语与模型相符
- Mutex 临界区简短、可测量,且不包含 I/O、网络调用或
.await点 - 工作池队列有界;明确说明背压策略
- Actor 邮箱通道的容量与预期负载相符;检查跨 Actor 循环是否会导致死锁
- 不将
async与并行混为一谈——join!会在单个线程上交错执行;只有tokio::spawn(或多线程运行时中的等效机制)才会并行执行 - 考虑将热点原子变量上的 CAS 重试循环替换为
fetch_add/分片(CAS 在竞争下的复杂度为 O(N²)) - 不使用
println!/dbg!调试竞态条件(Stdout mutex 会改变竞态状况)
不安全代码
-
unsafe块附有安全性注释,说明相关不变量 -
unsafe用量最少——块内只包含真正不安全的操作 - 安全性不变量有文档说明,且由周边安全代码维护
- 不存在未定义行为(空指针解引用、数据竞争、无效内存访问)
-
unsafetrait 实现说明为何满足其契约 - Edition 2024:
unsafe fn函数体在不安全操作周围使用显式的unsafe {}块(unsafe_op_in_unsafe_fn为 deny) - Edition 2024:
extern "C" {}块写作unsafe extern "C" {} - Edition 2024:
#[no_mangle]和#[export_name]写作#[unsafe(no_mangle)]和#[unsafe(export_name)]
并发(内存排序与无锁模式)
详细指南:references/memory-ordering.md、references/lock-free-patterns.md
- 跨线程共享的类型具有正确的
Send/Sync约束;unsafe impl Send/Sync附有注释,说明相关不变量 - 每个原子操作都对应一个明确命名的先行发生关系(spawn/join、同一原子变量上的
Release/Acquire,或内存栅栏);Release发布数据,Acquire观察数据 - 默认不使用
SeqCst——只有在两个或更多独立原子变量需要单一全局全序时才使用,并通过注释说明这一要求 - 不使用
store(.., Acquire)/load(.., Release)/load(.., AcqRel)(这些排序与其所在位置的类型不匹配) - 不使用
Relaxed发布或观察非原子数据(使用Release/Acquire) - 在重试循环中使用
compare_exchange_weak;compare_exchange仅用于一次性更新;获取临界区时,成功排序至少为Acquire - 手写自旋锁的忙等循环中包含
std::hint::spin_loop(),采用指数退避,并最终调用thread::yield_now();除非有文档说明不适合使用Mutex的原因,否则不要在普通用户态二进制程序中使用 - 手写
Arc克隆时使用Relaxed,递减时使用Release,且在最后一次递减时使用fence(Acquire),并包含溢出保护 - 使用
Weak打破Arc<Mutex<...>>循环;检查是否可将Arc<Mutex<Copy>>替换为Arc<AtomicT> - 手写无锁原语包含
#[cfg(loom)]测试模块和可由 Miri 运行的测试(不使用一概而论的cfg_attr(miri, ignore)) - 新代码优先使用
OnceLock/LazyLock,而非once_cell/lazy_static(MSRV ≥ 1.80) - 不在
.await期间持有MutexGuard(使用tokio::sync::Mutex或先释放 guard) - 对竞争缓存行上的热点原子变量使用
CachePadded或#[repr(align(64))]包装,以避免伪共享 - 共享可变状态通过
UnsafeCell<T>实现(不使用裸*mut T,也不将&转换为&mut) - 基于指针的 CAS(
AtomicPtr<Node>)使用 epoch/hazard pointer/tagged pointer 回收机制;考虑 ABA 风险
命名与风格
- 类型使用
PascalCase,函数/方法使用snake_case,常量使用SCREAMING_SNAKE_CASE - 模块使用
snake_case - 布尔值返回方法使用
is_、has_、can_前缀 - 构建器模式的方法接收并返回
self(而非&mut self),以支持链式调用 - 公共项包含文档注释(
///) - 对于忽略返回值很可能导致错误的函数,使用
#[must_use] - 导入顺序:std → 外部 crate → 工作区 → crate/super
- 抑制 lint 时,优先使用
#[expect(clippy::...)]而非#[allow(...)]
性能
详细指南: rust-best-practices 技能(
references/performance.md)
- 热路径中没有不必要的分配(优先使用
&str而非String,使用&[T]而非Vec<T>) -
collect()的类型已指定或可推断 - 转换集合时优先使用迭代器,而非带索引的循环
- 已知大小时使用
Vec::with_capacity() - 没有多余的
.to_string()/.to_owned()链式调用 - 直接传递迭代器可行时,不使用中间的
.collect() - 求和时优先使用
.sum(),而非.fold() - 除非需要灵活性,否则使用静态分发(
impl Trait),而非动态分发(dyn Trait)
Clippy 配置
详细指南: rust-best-practices 技能(
references/clippy-config.md)
- 在工作区级别的
Cargo.toml中配置 lint([workspace.lints.clippy]或[lints.clippy]) - 使用
#[expect(clippy::lint)],而非#[allow(...)]——当抑制规则不再适用时会发出警告 - 抑制任何 lint 时均附有说明注释
- 强制启用关键 lint:
redundant_clone、large_enum_variant、needless_collect、perf组 -
cargo clippy --all-targets --all-features -- -D warnings通过 - 为库 crate 启用文档 lint(
missing_docs、broken_intra_doc_links)
类型状态模式
详细指南: rust-best-practices 技能(
references/type-state-pattern.md)
- 使用
PhantomData<State>实现零成本的编译时状态机(而非运行时枚举/布尔值) - 状态转换消耗
self并返回新的状态类型(防止重用旧状态) - 每种状态仅提供适用的方法(无效操作会导致编译错误)
- 在能够提升安全性的场景使用该模式(包含必需字段的构建器、连接状态、工作流)
- 不要过度用于简单状态(需要运行时灵活性时,简单枚举即可)
严重性分级
严重(阻止合并)
unsafe代码存在不健全的不变量或未定义行为- 使用后释放或悬垂引用模式
- 生产代码中对用户输入或外部数据调用
unwrap() - 数据竞争(并发修改时未进行同步)
- 控制其他共享数据的原子操作使用了错误的内存顺序(数据竞争)
- 通过没有弱引用的循环
Arc<Mutex<...>>导致内存泄漏
重大(应修复)
- 返回错误时缺少上下文(相当于直接
return err) - 热路径中的
.clone()掩盖了所有权设计问题 - 跨线程使用的类型缺少
Send/Sync约束 - 库代码对可恢复的错误调用
panic! - 过于宽泛的
'static生命周期掩盖了 API 设计问题
轻微(可考虑修复)
- 公共项缺少文档注释
- 参数使用
String,而&str或impl AsRef<str>也适用 - 应使用派生宏的类型缺少相应宏
Cargo.toml中存在未使用的功能标志- 迭代器链不够理想(多次分配,而一次即可满足需求)
参考信息(仅作提示)
- 建议为领域建模引入 newtype
- trait 设计重构思路
- 未经测量验证的性能优化
- 建议添加
#[must_use]或#[non_exhaustive]
何时加载参考资料
- 审查所有权、借用、生命周期、克隆陷阱 → ownership-borrowing.md
- 审查生命周期方差、协变/不变、多个生命周期参数 → lifetime-variance.md
- 审查 Result/Option 处理、错误类型、不透明类型与枚举类型、延迟清理、
Errortrait 实现 → error-handling.md - 审查异步代码、poll 契约、Pin 机制、取消安全性、跨运行时 → async-concurrency.md
- 审查并发设计决策(共享内存、工作池还是 actor,异步还是线程)、数据竞争与竞态条件 → concurrency-models.md
- 审查 Send/Sync、原子操作、互斥锁、锁模式 → concurrency-primitives.md
- 审查内存排序决策、栅栏、ABA、凭空出现问题 → memory-ordering.md
- 审查手写自旋锁、通道、Arc、seqlock、CAS 重试模式 → lock-free-patterns.md
- 审查类型布局、对齐、repr、PhantomData、宽指针、自动 trait 泄漏、
Sized/?Sized→ types-layout.md - 审查接口设计——对象安全、符合人体工程学的 blanket impl、
Deref使用规范、可能失败/阻塞的析构函数、隐藏契约、命名 → interface-design.md - 审查索引指针图、drop guard、扩展 trait、crate 前导模块 → patterns-in-the-wild.md
- 审查不安全代码、API 设计、derive 宏、clippy 模式 → common-mistakes.md
- 审查有效性与安全性、drop check + may_dangle、来源信息、不安全代码中的 panic 安全性、MaybeUninit → unsafe-deep.md
- 审查性能、指针类型、类型状态、泛型、迭代器、文档 → rust-best-practices 技能
有效模式(不要标记)
以下是可接受的 Rust 模式——报告这些问题会浪费开发者的时间:
- 测试中的
.clone()—— 测试代码中清晰度优先于性能 - 测试和示例中的
unwrap()—— 有意在失败时 panic 的场景下可以接受 - 简单二进制程序中的
Box<dyn Error>—— 并非每个应用都需要自定义错误类型 - 结构体中的
String字段 —— 结构体中使用拥有所有权的数据是正确的;&str字段需要生命周期参数 - 开发期间的
#[allow(dead_code)]—— 迭代开发期间很常见 - 新代码中的
todo!()/unimplemented!()—— 开发期间有效的占位符 - 附带清晰说明的
.expect("reason")—— 可自行说明用途,适用于不变量 - 测试模块中的
use super::*——#[cfg(test)]模块的标准模式 - 复杂类型的类型别名 ——
type Result<T> = std::result::Result<T, MyError>是惯用写法 - 返回位置上的
impl Trait—— 零开销抽象,是标准模式 - Turbofish 语法 —— 类型推断需要帮助时,
collect::<Vec<_>>()是惯用写法 - 为有意不使用的变量添加
_前缀 —— 符合编译器约定 - 附有理由的
#[expect(clippy::...)]—— 可自动清理的 lint 抑制 Arc::clone(&arc)—— 显式克隆 Arc 是惯用且推荐的写法- 异步代码中用于短临界区的
std::sync::Mutex—— Tokio 文档推荐这样使用 - 遍历迭代器的
for循环 —— 需要提前退出或产生副作用时使用 - trait 定义中的
async fn—— 自 1.75 起稳定;只有在需要dyn Trait或 MSRV 低于 1.75 时才需要async-traitcrate - 来自 std 的
LazyCell/LazyLock—— 自 1.80 起稳定;新代码中可替代once_cell和lazy_static + use<'a, T>精确捕获语法 —— Edition 2024 语法,用于控制 RPIT 生命周期捕获
上下文相关规则
仅在满足特定条件时标记以下问题:
| 问题 | 仅在以下情况下标记 |
|---|---|
| 缺少错误上下文 | 错误跨越模块边界时未提供上下文 |
不必要的 .clone() | 出现在热点路径或重复调用中,而非测试/设置代码中 |
| 缺少文档注释 | 条目为 pub,且不在 #[cfg(test)] 模块中 |
使用 unwrap() | 出现在生产代码路径中,而非测试/示例/可证明安全的代码中 |
缺少 Send + Sync | 类型确实会跨线程/任务边界共享 |
| 生命周期范围过宽 | 较短的生命周期可行,且 API 为公开 API |
缺少 #[must_use] | 函数返回的值通常会被调用者忽略 |
过时的 #[allow] 抑制 | 为实现可自动清理的 lint 管理,应使用 #[expect] |
缺少 Copy 派生 | 类型不超过 24 字节,所有字段均实现 Copy,且经常使用 |
Edition 2024:! 类型回退 | 对 Result<T, !> 进行匹配,或在假定回退为 () 的发散表达式中使用——现在 ! 会回退为 !,而非 () |
Edition 2024:r#gen 标识符 | 代码将 gen 用作标识符——在 Edition 2024 中必须写作 r#gen(保留关键字) |
提交问题前
请满足 Gates § 验证流程(步骤 4)的要求。报告任何问题前,请加载并遵循 review-verification-protocol 技能。
评论
支持 Markdown