|
| 1 | +--- |
| 2 | +description: Rust Coding Style |
| 3 | +globs: "**/*.rs" |
| 4 | +alwaysApply: false |
| 5 | +--- |
| 6 | +# Rust Coding Style |
| 7 | + |
| 8 | +## Project-Specific Patterns |
| 9 | + |
| 10 | +- Use the 2024 edition of Rust |
| 11 | +- Prefer `derive_more` over manual trait implementations |
| 12 | +- Feature flags in this codebase use the `#[cfg(feature = "...")]` pattern |
| 13 | +- Invoke `cargo clippy` with `--all-features`, `--all-targets`, and `--no-deps` from the root |
| 14 | +- Use `cargo doc --no-deps --all-features` for checking documentation |
| 15 | +- Use `rustfmt` to format the code |
| 16 | +- Use `#[expect(lint, reason = "...")]` over `#[allow(lint)]` |
| 17 | + |
| 18 | +## Type System |
| 19 | + |
| 20 | +- Create strong types with newtype patterns for domain entities |
| 21 | +- Implement `hash_graph_types` traits for custom domain types |
| 22 | +- Consider visibility carefully (avoid unnecessary `pub`) |
| 23 | + |
| 24 | +```rust |
| 25 | +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, derive_more::Display)] |
| 26 | +pub struct UserId(Uuid); |
| 27 | +``` |
| 28 | + |
| 29 | +## Async Patterns |
| 30 | + |
| 31 | +- Use `impl Future<Output = T> + Send` in trait definitions: |
| 32 | + |
| 33 | +```rust |
| 34 | +fn get_data( |
| 35 | + &self, |
| 36 | + id: String, |
| 37 | +) -> impl Future<Output = Result<Data, Report<DataError>>> + Send { |
| 38 | + async move { |
| 39 | + // Implementation |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +## Function Arguments |
| 45 | + |
| 46 | +- Functions should **never** take more than 7 arguments. If a function requires more than 7 arguments, encapsulate related parameters in a struct. |
| 47 | +- Functions that use data immutably should take a reference to the data, while functions that modify data should take a mutable reference. Never take ownership of data unless the function explicitly consumes it. |
| 48 | +- Make functions `const` whenever possible. |
| 49 | +- Prefer the following argument types when applicable, but only if this does not reduce performance: |
| 50 | + - `impl AsRef<str>` instead of `&str` or `&String` |
| 51 | + - `impl AsRef<Path>` instead of `&Path` or `&PathBuf` |
| 52 | + - `impl IntoIterator<Item = &T>` when only iterating over the data |
| 53 | + - `&[T]` instead of `&Vec<T>` |
| 54 | + - `&mut [T]` instead of `&mut Vec<T>` when the function doesn't need to resize the vector |
| 55 | + - `impl Into<Cow<T>>` instead of `Cow<T>` |
| 56 | + - `impl Into<Arc<T>>` instead of `Arc<T>` |
| 57 | + - `impl Into<Rc<T>>` instead of `Rc<T>` |
| 58 | + - `impl Into<Box<T>>` instead of `Box<T>` |
| 59 | +- Never use `impl Into<Option<_>>` as from reading the caller site, it's not visible that `None` could potentially be passed |
| 60 | + |
| 61 | +## `From` and `Into` |
| 62 | + |
| 63 | +- Generally prefer `From` implementations over `Into` implementations. The Rust compiler will automatically derive `Into` from `From`, but not vice versa. |
| 64 | +- When converting between types, prefer using the `from` method over `into` for clarity. The `from` method makes the target type explicit in the code, while `into` requires type inference. |
| 65 | +- For wrapper types like `Cow`, `Arc`, `Rc`, `Report`, and `Box`, prefer using explicit constructors (e.g., `Cow::from`, `Arc::new`) instead of `.into()`. This improves readability by clearly indicating the target type. |
| 66 | + |
| 67 | +## Smart Pointers |
| 68 | + |
| 69 | +- When cloning smart pointers such as `Arc` and `Rc`, **always** use `Arc::clone(&pointer)` and `Rc::clone(&pointer)` instead of `pointer.clone()`. This explicitly indicates you're cloning the reference, not the underlying data. |
| 70 | + |
| 71 | +## Instrumentation |
| 72 | + |
| 73 | +- Annotate functions that perform significant work with `#[tracing::instrument]` |
| 74 | +- Use `tracing` macros (e.g., `trace!`, `debug!`, `info!`, `warn!`, `error!`) instead of `println!` or `eprintln!` for logging |
| 75 | + |
| 76 | +## Allocations |
| 77 | + |
| 78 | +- Minimize allocations when possible. For example, reuse a `Vec` in a loop instead of creating a new one in each iteration. |
| 79 | +- Prefer borrowed data over owned data where appropriate. |
| 80 | +- Balance performance and readability—if an allocation makes code significantly more readable or maintainable, the trade-off may be worthwhile. |
| 81 | + |
| 82 | +## Types |
| 83 | + |
| 84 | +- Use newtypes when a value should carry specific semantics beyond its underlying type. This improves type safety and code clarity. |
| 85 | + |
| 86 | +For example: |
| 87 | + |
| 88 | +```rust |
| 89 | +struct UserId(u64); // instead of `type UserId = u64;` or `u64` |
| 90 | +``` |
| 91 | + |
| 92 | +## Naming Conventions |
| 93 | + |
| 94 | +When suggesting names for variables, functions, or types: |
| 95 | + |
| 96 | +- Do not prefix test-function names with `test_`, this would otherwise result in `test::test_<name>` names. |
| 97 | +- Provide a concise list of naming options with brief explanations of why each fits the context |
| 98 | +- Choose names of appropriate length—avoid names that are too long or too short |
| 99 | +- Avoid abbreviations unless they are widely recognized in the domain (e.g., `Http` or `Json` is acceptable, but `Ctx` instead of `Context` is not) |
| 100 | +- Do not suffix names with their types (e.g., use `users` instead of `usersList`) |
| 101 | +- Do not repeat the type name in variable names (e.g., use `user` instead of `userUser`) |
| 102 | + |
| 103 | +## Crate Preferences |
| 104 | + |
| 105 | +- Use `similar_asserts` for test assertions |
| 106 | +- Use `insta` for snapshot tests |
| 107 | +- Use `test_log` for better test output (`#[test_log::test]`) |
| 108 | +- Use `tracing` macros, not `log` macros |
| 109 | +- Prefer `tracing::instrument` for function instrumentation |
| 110 | + |
| 111 | +## Import Style |
| 112 | + |
| 113 | +- Don't use local imports within functions, or blocks |
| 114 | +- Avoid wildcard imports like `use super::*;`, or `use crate::module::*;` |
| 115 | +- Never use a prelude `use crate::prelude::*` |
| 116 | +- Prefer explicit imports to make dependencies clear and improve code readability |
| 117 | +- We prefer `core` over `alloc` over `std` for imports to minimize dependencies |
| 118 | + - Use `core` for functionality that doesn't require allocation |
| 119 | + - Use `alloc` when you need allocation but not OS-specific features |
| 120 | + - Only use `std` when necessary for OS interactions or when using `core`/`alloc` would be unnecessarily complex |
| 121 | +- Prefer qualified imports (`use foo::Bar; let x = Bar::new()`) over fully qualified paths (`let x = foo::Bar::new()`) for frequently used types |
| 122 | +- Use `pub use` re-exports in module roots to create a clean public API |
| 123 | +- Avoid importing items with the same name from different modules; use qualified imports |
| 124 | +- Import traits using `use module::Trait as _;` when you only need the trait's methods and not the trait name itself |
| 125 | + - This pattern brings trait methods into scope without name conflicts |
| 126 | + - Use this especially for extension traits or when implementing foreign traits on local types |
| 127 | + |
| 128 | +```rust |
| 129 | +// Good - Importing a trait just for its methods: |
| 130 | +use std::io::Read as _; |
| 131 | + |
| 132 | +// Example with trait methods: |
| 133 | +fn read_file(file: &mut File) -> Result<String, std::io::Error> { |
| 134 | + // Read methods available without importing the Read trait name |
| 135 | + let mut content = String::new(); |
| 136 | + file.read_to_string(&mut content)?; |
| 137 | + Ok(content) |
| 138 | +} |
| 139 | + |
| 140 | +// Bad - Directly importing trait when only methods are needed: |
| 141 | +use std::io::Read; |
| 142 | + |
| 143 | +// Good - Importing trait for implementing it: |
| 144 | +use std::io::Write; |
| 145 | +impl Write for MyWriter { /* implementation */ } |
| 146 | + |
| 147 | +// Bad - Wildcard import: |
| 148 | +mod tests { |
| 149 | + use super::*; // Wildcard import |
| 150 | + |
| 151 | + #[test] |
| 152 | + fn test_something() { |
| 153 | + // Test implementation |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +// Good - Explicit imports: |
| 158 | +mod tests { |
| 159 | + use crate::MyStruct; |
| 160 | + use crate::my_function; |
| 161 | + |
| 162 | + #[test] |
| 163 | + fn test_something() { |
| 164 | + // Test implementation |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +// Bad - Local import: |
| 169 | +fn process_data() { |
| 170 | + use std::collections::HashMap; // Local import |
| 171 | + let map = HashMap::new(); |
| 172 | + // Implementation |
| 173 | +} |
| 174 | + |
| 175 | +// Good - Module-level import: |
| 176 | +use std::collections::HashMap; |
| 177 | + |
| 178 | +fn process_data() { |
| 179 | + let map = HashMap::new(); |
| 180 | + // Implementation |
| 181 | +} |
| 182 | + |
| 183 | +// Bad - Using std when core would suffice: |
| 184 | +use std::fmt::Display; |
| 185 | + |
| 186 | +// Good - Using core for non-allocating functionality: |
| 187 | +use core::fmt::Display; |
| 188 | + |
| 189 | +// Bad - Using std when alloc would suffice: |
| 190 | +use std::collections::BTreeSet; |
| 191 | + |
| 192 | +// Good - Using alloc for allocation without full std dependency: |
| 193 | +use alloc::vec::Vec; |
| 194 | + |
| 195 | +// Appropriate - Using std when needed: |
| 196 | +use std::fs::File; // OS-specific functionality requires std |
| 197 | +``` |
| 198 | + |
| 199 | +## Libraries and Components |
| 200 | + |
| 201 | +- Abstract integrations with third-party systems behind traits to maintain clean separation of concerns |
| 202 | + |
| 203 | +## Comments and Assertions |
| 204 | + |
| 205 | +- Do not add comments after a line of code; place comments on separate lines above the code they describe |
| 206 | +- When using assertions, include descriptive messages using the optional description parameter rather than adding a comment |
| 207 | +- All `expect()` messages should follow the format "should ..." to clearly indicate the expected behavior |
| 208 | + |
| 209 | +For example: |
| 210 | + |
| 211 | +```rust |
| 212 | +// Bad: |
| 213 | +assert_eq!(result, expected); // This should match the expected value |
| 214 | + |
| 215 | +// Good: |
| 216 | +assert_eq!(result, expected, "Values should match expected output"); |
| 217 | + |
| 218 | +// Bad: |
| 219 | +some_value.expect("The value is not None"); // This should never happen |
| 220 | + |
| 221 | +// Good: |
| 222 | +some_value.expect("should contain a valid value"); |
| 223 | +``` |
0 commit comments