| name | rust-best-practices |
| description | Idiomatic Rust conventions for the PADE Tauri backend (src-tauri) — naming, error handling, type safety, ownership, control flow, dependency discipline, and testing. Use BEFORE writing or reviewing any Rust in this repo, and when a clippy/pedantic warning needs an idiomatic fix rather than an `#[allow]`. |
Rust best practices (PADE src-tauri)
Synthesized from the Rust API Guidelines,
the Rust Book on error handling,
the idiomatic-rust collection, and this
repo's own CLAUDE.md. These override defaults; where CLAUDE.md is more
specific (full-word names, no magic strings), it wins.
0. The gate is non-negotiable
Every Rust change must pass pnpm lint:rust before it is committed:
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
cargo fmt --manifest-path src-tauri/Cargo.toml
Clippy runs at -D warnings (pedantic-leaning). A warning is an error. Fix
it idiomatically; reach for #[allow(...)] only with a one-line comment
justifying why the lint is wrong here (e.g. a dependency the lint would pull in
violates minimize-dependencies). Prefer restructuring:
single_match_else → use if let … else, not match with one arm + _.
naive_bytecount → don't filter(|b| *b == x).count() a big buffer; if you
truly need a count, restructure (e.g. position to short-circuit) rather than
add the bytecount crate.
needless_return, redundant_clone, manual_map, map_or → take the
suggestion; it is almost always cleaner.
Run the tests too: cargo test --manifest-path src-tauri/Cargo.toml.
1. Naming (RFC 430 + this repo's full-word rule)
- Types/traits/enums
UpperCamelCase; functions/vars/modules snake_case;
consts SCREAMING_SNAKE_CASE.
- Full, spelled-out words —
extension not ext, previous not prev,
command not cmd, index not idx. Only universal short forms (id,
url, ok) and a bare loop i are allowed.
- Conversions follow the cost convention:
as_* (cheap borrow), to_*
(expensive/owned), into_* (consuming). Iterators: iter / iter_mut /
into_iter.
- A function with a clear receiver is a method; constructors are inherent static
methods (
Foo::new).
2. Error handling
3. Type safety — make illegal states unrepresentable
- Newtypes for domain distinctions — a
ProjectPath(String) beats a bare
String; the compiler then stops you mixing it with any other string.
- Enums, never magic values. Model a closed set once as an
enum and match
its variants — mirrors CLAUDE.md's "enums over magic strings". Give the enum
the one authoritative as_str()/from_* mapping (see watcher::ChangeKind).
- Arguments convey meaning through types, not
bool/Option flags. Two
bool params at a call site are unreadable; use an enum or a small struct.
- No
as casts. as silently truncates/wraps. Use From/Into for
widening, TryFrom/try_into() (handle the error) for narrowing, or
u128::from(x) explicitly. (Enforced by clippy in this repo.)
- Structs keep private fields; expose behavior, not representation.
4. Ownership, borrowing, immutability
- Borrow, don't clone. Take
&str/&Path/&[T] in function params; clone
only when you must own. A .clone() in a hot path is a smell — check whether a
borrow works first.
- Immutable by default.
let over let mut; reach for mut only with a
reason. Prefer building a value with an iterator chain over mutating an
accumulator in a loop.
- Accept the most general borrow:
impl AsRef<Path>, R: Read by value, &[T]
over &Vec<T>.
- Iterators over index loops:
for entry in entries.flatten(), .filter_map(),
.take(n) for bounds — expressive and bounds-checked.
5. Control flow
- Early returns / guard clauses so the happy path reads top-to-bottom; don't
nest
if/else pyramids.
if let … else for a single-pattern branch; let … else for the "bail if not
this shape" guard.
matches!(value, Pattern) for a boolean shape test.
6. Interoperability & derives
- Types eagerly derive the common traits when it makes sense:
Debug
(always, on every public type — clippy wants it), Clone, Copy (small POD),
PartialEq/Eq, Hash, Default.
- Wire/persisted types derive serde
Serialize/Deserialize with
#[serde(rename_all = "camelCase")] so the TS side sees camelCase (the zod
schema is the source of truth — see CLAUDE.md).
- Conversions use
From/TryFrom/AsRef, not ad-hoc constructor methods.
7. Dependencies — minimize (supply-chain surface)
- std first. Before adding a crate, check whether std or a ~20-line helper
does it. This repo hand-rolls percent-encoding, PATH search, registry reads
(
reg query), and home-dir lookup rather than pulling crates.
- Shell out through
crate::util::command(...), never Command::new
directly — it suppresses the console window on Windows. Use it for git,
reg, editor launchers, task runners. git ls-files, git diff, etc. go
through the CLI, not a git library.
- A new dependency needs a justification in the commit message and must be small,
well-audited, and already load-bearing.
8. Modules & SoC
- One concern per module (
pty, watcher, vcs, ide, usage, workspace,
os, util); lib.rs only wires them and registers Tauri commands.
- Shared helpers live in
util and are reused (DRY) — extend is_on_path,
command, resolve, percent_encode rather than re-deriving.
- Keep a
#[tauri::command] thin: validate inputs, call into the module's real
logic, map the error to String.
9. Testing
10. Documentation
/// doc comments on public items, explaining the why (the non-obvious
intent), not restating the signature. This repo's Rust is heavily commented on
rationale — match that density.
//! module docs at the top of each file state the module's one concern.
- Reserve inline
// comments for a non-obvious why a name can't carry; name
the thing well first.
Sources: Rust API Guidelines checklist ·
The Rust Book — Error Handling ·
idiomatic-rust ·
Rust By Example — Error handling ·
Rust Design Patterns · this repo's CLAUDE.md.