Programmation Rust
Utilise cette skill pour maintenir les changements Rust idiomatiques, maintenables et faciles à vérifier.
Règles de fonctionnement
- Lis le module environnant avant de modifier la propriété, les trait bounds, les types publics ou le flux async.
- Préfère les limites de crate et les types d'erreur existants du repository plutôt que d'introduire une nouvelle abstraction.
- Rends les états invalides non représentables avec des types, énums et constructeurs ciblés où le domaine a de véritables invariants.
- Retourne
Resultpour les défaillances récupérables et réservepanic!,unwrapetexpectaux tests, états impossibles avec un message clair, ou à la validation au démarrage fatale au processus. - Maintiens les allocations et les clones intentionnels. Emprunte d'abord, clone aux limites de propriété, et documente les compromis de durée de vie ou d'allocation non évidents.
- Favorise le contrôle de flux explicite plutôt que des chaînes astucieuses quand la gestion des erreurs, le verrouillage, l'annulation ou la mutation d'état sont impliqués.
- Garde les API publiques petites, nommées de façon cohérente et suffisamment documentées pour que les appelants connaissent la propriété, les erreurs et les effets secondaires.
- Exécute
cargo fmt,cargo testciblé etcargo clippy --workspace --all-targetsquand le changement le justifie.
Vérifications de conception
- Types : Le type exprime-t-il le domaine, ou est-ce un ensemble de champs peu liés ?
- Propriété : L'appelant peut-il passer une référence ? Si la propriété est nécessaire, le transfert est-il visible dans l'API ?
- Erreurs : Chaque erreur est-elle exploitable à la couche qui la reçoit ?
- Concurrence : Les verrous sont-ils maintenus pour le plus court champ pratique, jamais traverser des awaits sauf s'il est délibérément requis ?
- Traits : Les trait bounds sont-ils sur la fonction qui en a besoin plutôt que de se propager à travers des structs plus grandes ?
- Tests : Les tests couvrent-ils le comportement et les cas limites, pas seulement la compilation ou les snapshots ?
Patterns de bonnes pratiques
Accepter des entrées empruntées
// À éviter : force les appelants à allouer ou à abandonner la propriété.
fn load_config(path: PathBuf) -> Result<Config, ConfigError> {
std::fs::read_to_string(path)?.parse()
}
// Préférer : accepte Path, PathBuf et les wrappers ressemblant à des chemins.
fn load_config(path: impl AsRef<std::path::Path>) -> Result<Config, ConfigError> {
std::fs::read_to_string(path)?.parse()
}
Garder les types d'erreur exploitables
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
#[error("failed to read import file {path}")]
Read {
path: std::path::PathBuf,
#[source]
source: std::io::Error,
},
#[error("invalid import payload")]
InvalidPayload(#[from] serde_json::Error),
}
Utilise les variantes typées quand les appelants peuvent récupérer différemment. Utilise des messages riches en contexte aux limites du processus ou de l'interface utilisateur où le diagnostic compte plus que l'appariement.
Rendre les états invalides non représentables
pub struct NonEmptyName(String);
impl NonEmptyName {
pub fn parse(value: String) -> Result<Self, NameError> {
if value.trim().is_empty() {
return Err(NameError::Empty);
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
Fais la validation une fois à la construction plutôt que de vérifier à répétition les chaînes brutes partout dans le codebase.
Ne pas maintenir les verrous traversant les awaits
// À éviter : le verrou reste maintenu pendant que l'écriture async attend.
async fn save_bad(state: &tokio::sync::Mutex<State>, store: &Store) -> anyhow::Result<()> {
let guard = state.lock().await;
store.write(&guard.snapshot()).await
}
// Préférer : copie les données nécessaires, puis relâche le verrou avant await.
async fn save_good(state: &tokio::sync::Mutex<State>, store: &Store) -> anyhow::Result<()> {
let snapshot = {
let guard = state.lock().await;
guard.snapshot()
};
store.write(&snapshot).await
}
Cloner aux limites de propriété
let workspace_id = workspace.id.clone();
tokio::spawn(async move {
refresh_workspace(workspace_id).await
});
Cloner pour une tâche spawned, un message de canal, une entrée de cache ou une valeur stockée est souvent correct. Cloner pour satisfaire le borrow checker à l'intérieur d'une seule fonction synchrone est un signal pour raccourcir d'abord les emprunts.
Tester le comportement, pas la forme d'implémentation
#[test]
fn rejects_empty_workspace_name() {
let err = NonEmptyName::parse(" ".to_string()).unwrap_err();
assert!(matches!(err, NameError::Empty));
}
Préfère les tests qui épinglent le comportement public, les variantes d'erreur, les transitions d'état et les cas de régression.
Références
Lis references/idiomatic-rust.md pour les patterns concrets et les heuristiques de révision.