rust-programming

Par mkurman · zorai

À utiliser lors de l'écriture, de la revue ou du refactoring de code Rust, en particulier pour les crates de workspace, les API publiques, la gestion des erreurs, le code async, les choix de propriété ou les chemins sensibles aux performances.

npx skills add https://github.com/mkurman/zorai --skill rust-programming

Programmation Rust

Utilise cette skill pour maintenir les changements Rust idiomatiques, maintenables et faciles à vérifier.

Règles de fonctionnement

  1. Lis le module environnant avant de modifier la propriété, les trait bounds, les types publics ou le flux async.
  2. Préfère les limites de crate et les types d'erreur existants du repository plutôt que d'introduire une nouvelle abstraction.
  3. Rends les états invalides non représentables avec des types, énums et constructeurs ciblés où le domaine a de véritables invariants.
  4. Retourne Result pour les défaillances récupérables et réserve panic!, unwrap et expect aux tests, états impossibles avec un message clair, ou à la validation au démarrage fatale au processus.
  5. 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.
  6. 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.
  7. 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.
  8. Exécute cargo fmt, cargo test ciblé et cargo clippy --workspace --all-targets quand 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.

Skills similaires