Why Code Review in Rust is More Than Just Checking Brackets
Rust is a language that promises safe memory management and high performance. But even the strictest compiler won't protect against logical errors, non-idiomatic code, or unjustified use of unsafe. Code review in Rust is an art. You don't just check if the curly braces are closed. You check how the developer circumvented (or didn't circumvent) the borrow checker's limitations, how efficiently they use iterators, and whether they're trying to "outsmart" the system with raw pointers.
In this article, I'll talk about which aspects to focus on when reviewing Rust code. No unnecessary theory — only practical advice, examples, and a checklist you can hang on the wall (or save to bookmarks). Let's go!
1. Ownership, Borrowing, and Lifetimes: The Main Source of Pain
The first thing to check is whether the ownership system works correctly. Even if the code compiles, it doesn't mean it's correct. Typical problems:
- Excessive cloning. When a reference could have been used, the developer copies data. This kills performance.
- Mutable and immutable borrowing. If there is both
&mut Tand&Tin the same scope, it will be an error. But sometimes you can reorder the code to avoid this. - Incorrect lifetimes. Especially in complex structures with multiple references.
Example: Bad Code
fn process(data: &Vec<i32>) -> i32 { let mut sum = 0; for &x in data.iter() { sum += x; } // It's fine, but why pass &Vec when you can pass &[i32]? sum}Good Code
fn process(data: &[i32]) -> i32 { data.iter().sum()}What to check during review: Is &Vec, &String, or &HashMap being passed where a slice would suffice? Are clone() calls used unnecessarily? Are lifetime annotations placed correctly?
2. Unsafe: The Red Zone
unsafe blocks are like an open hatch into safe Rust. They must be justified, minimal, and thoroughly documented. During a code review, pay attention to:
- Why is unsafe used? Often, safe abstractions are sufficient.
- Correctness of raw pointers. Is a null pointer being dereferenced? Is a reference dangling after memory is freed?
- Invariants. Code with
unsafeshould explicitly describe what invariants it maintains.
Example: Dangerous unsafe
unsafe fn dangerous() { let ptr = 0x12345 as *mut i32; *ptr = 42; // What if there's nothing at this address?}Such code should not pass review. Even if "it works" — that's not an argument.
Good unsafe
unsafe fn init_buffer(buf: &mut [u8]) { // We are sure that buf points to valid memory, // because the calling party guarantees it. std::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len());}What to check: Is there a comment justifying each unsafe? Are there tests that check edge cases? Are safety guarantees being violated?
3. Closures and Functional Style
Rust encourages a functional approach: iterators, closures, map, filter, fold. But this can lead to overcomplication. During review, look at:
- Are closures overused? If a closure captures many variables, it might be worth extracting the logic into a separate f