Documentation en Rust : commentaires de doc et doc tests
Chaque page de docs.rs et chaque page de la documentation de la bibliothèque standard a été écrite par un programmeur, directement dans le code source, avec des commentaires spéciaux. Rust transforme ces commentaires en site web avec une seule commande. Vous pouvez faire exactement la même chose pour votre propre code.
Il y a une deuxième bonne raison de s’y intéresser : les exemples de code de votre documentation sont de vrais tests. Quand vous lancez cargo test, Rust les compile et les exécute. Si vous modifiez votre fonction en oubliant de mettre l’exemple à jour, le test échoue. Votre documentation ne peut donc pas devenir obsolète en silence. C’est rare dans les langages de programmation, et c’est l’un des plus beaux atouts de Rust.
Les commentaires de documentation : /// et //!
Section intitulée « Les commentaires de documentation : /// et //! »Vous avez vu les commentaires normaux sur la page Les commentaires. Les commentaires de documentation (doc comments) leur ressemblent, mais avec trois caractères :
///documente l’élément situé en dessous : une fonction, une struct, un enum, un champ ou un module.//!documente l’élément dans lequel il se trouve. Tout en haut desrc/lib.rsou desrc/main.rs, il décrit la crate entière.
//! Un tout petit programme de recettes.
/// Partage `total` grammes en `portions` parts égales.fn split_evenly(total: u32, portions: u32) -> u32 { total / portions}La première ligne d’un commentaire de documentation doit être une courte phrase de résumé. Les outils affichent cette ligne dans les listes et les résultats de recherche : soignez-la.
Les commentaires de documentation ne changent rien à ce que fait votre programme. Voici une fonction documentée, appelée depuis main :
/// Partage `total` grammes en `portions` parts égales.////// Les grammes en trop sont ignorés : 10 grammes en 3 parts donnent 3.////// # Panics////// Panique si `portions` vaut zéro.fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions doit être supérieur à zéro"); } total / portions}
fn main() { println!("{}", split_evenly(600, 3)); println!("{}", split_evenly(10, 3));}Sortie :
2003Dans la plupart des éditeurs, si vous survolez split_evenly dans main, ce texte s’affiche maintenant dans une bulle. Rien que pour ça, l’effort en vaut la peine.
Du Markdown dans les commentaires de documentation
Section intitulée « Du Markdown dans les commentaires de documentation »Les commentaires de documentation s’écrivent en Markdown, le format utilisé sur GitHub et dans beaucoup d’applications de discussion. Vous pouvez utiliser `code`, **gras**, *italique*, des listes, des liens et des titres :
/// Renvoie la température du four pour un plat, en **degrés Celsius**.////// Plats connus :////// - `"bread"` : 220 degrés/// - `"cake"` : 180 degrés/// - tout le reste : 200 degrés////// Vérifiez *toujours* avec votre propre four, car chaque four est différent./// Voir [Degré Celsius sur Wikipédia](https://fr.wikipedia.org/wiki/Degr%C3%A9_Celsius).pub fn oven_temperature(dish: &str) -> u32 { match dish { "bread" => 220, "cake" => 180, _ => 200, }}Laissez une ligne /// vide entre deux paragraphes, comme vous laisseriez une ligne vide dans un fichier Markdown normal. Entourez les noms de fonctions, de types et de valeurs de backticks (accents graves) pour qu’ils s’affichent comme du code.
Les sections habituelles
Section intitulée « Les sections habituelles »Les programmeurs Rust utilisent toujours les mêmes quelques titres, pour que les lecteurs sachent où chercher. Vous en avez déjà vu un. Voici les plus courants :
# Examples: comment utiliser l’élément. Presque chaque élément public devrait en avoir un.# Panics: quand la fonction peut déclencher une panic, pour que l’appelant puisse l’éviter.# Errors: quand une fonction qui renvoie unResultrenvoie unErr, et pourquoi.# Safety: pour uneunsafe fn, les règles que l’appelant doit respecter (voir Unsafe en Rust).
Ces titres restent en anglais, même dans une documentation écrite en français : c’est la convention de tout l’écosystème Rust, et certains outils (comme le linter Clippy) les cherchent sous ce nom exact.
Voici # Errors et # Safety dans un fichier de bibliothèque, src/lib.rs :
/// Lit une quantité comme `"250g"` et renvoie le nombre de grammes.////// # Errors////// Renvoie une erreur si le texte ne se termine pas par `g`,/// ou si ce qui précède n'est pas un nombre entier.pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' devrait se terminer par g"))?; number .parse() .map_err(|_| format!("'{number}' n'est pas un nombre entier"))}
/// Renvoie le premier ingrédient sans vérifier que la liste n'est pas vide.////// # Safety////// L'appelant doit s'assurer que `ingredients` n'est pas vide./// L'appeler sur une slice vide est un comportement indéfini.pub unsafe fn first_unchecked(ingredients: &[String]) -> &String { unsafe { ingredients.get_unchecked(0) }}Remarquez que les titres utilisent un seul #. Dans un commentaire de documentation, # est un titre Markdown, pas un attribut Rust.
Générer la documentation avec cargo doc
Section intitulée « Générer la documentation avec cargo doc »Dans la suite de cette page, nous allons travailler sur une petite bibliothèque. Créez-la avec cargo new --lib kitchen. Puis, à l’intérieur du projet, lancez :
cargo doc --open Documenting kitchen v0.1.0 (/path/to/kitchen) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.91s Generated /path/to/kitchen/target/doc/kitchen/index.htmlCargo construit un site HTML dans target/doc/, et --open l’ouvre dans votre navigateur. Il ressemble exactement aux pages de docs.rs : une barre latérale, un champ de recherche et une page par fonction et par type. Il documente aussi chaque crate dont vous dépendez, ce qui est bien pratique quand vous êtes hors ligne.
Par défaut, seuls les éléments publics (marqués pub) apparaissent, car la documentation s’adresse aux personnes qui utilisent votre bibliothèque. Ajoutez --document-private-items pour tout inclure.
Les doc tests : des exemples qui s’exécutent
Section intitulée « Les doc tests : des exemples qui s’exécutent »Voici le début de src/lib.rs dans la bibliothèque kitchen. La fonction a une section # Examples avec un bloc de code à l’intérieur du commentaire de documentation :
//! Petits outils pour la cuisine.//!//! `kitchen` convertit les températures du four, partage les aliments en portions//! et lit des quantités comme `"250g"`.
/// Convertit une température de degrés Celsius en degrés Fahrenheit.////// # Examples////// ```/// let f = kitchen::celsius_to_fahrenheit(180.0);/// assert_eq!(f, 356.0);/// ```pub fn celsius_to_fahrenheit(celsius: f64) -> f64 { celsius * 9.0 / 5.0 + 32.0}Lancez maintenant cargo test :
Doc-tests kitchen
running 1 testtest src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sCargo a trouvé le bloc de code à la ligne 10, l’a transformé en tout petit programme, l’a compilé et l’a exécuté. Le assert_eq! a vérifié le résultat. Quelques points à noter :
- L’exemple appelle
kitchen::celsius_to_fahrenheit, avec le nom de la crate. Un doc test est compilé comme s’il s’agissait du programme de quelqu’un d’autre qui utilise votre bibliothèque : il ne voit donc que les élémentspub, et vous avez besoin du chemin complet ou d’une ligneuse kitchen::...;. - Vous n’écrivez pas de
fn main. Rustdoc enveloppe votre exemple dans une fonctionmainà votre place. - Utilisez
assert!etassert_eq!(vus sur la page Les tests en Rust) pour montrer et vérifier le résultat en même temps.
Et si l’exemple est faux ? Remplacez 356.0 par 350.0 et relancez les tests :
running 1 testtest src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----Test executable failed (exit status: 101).
stderr:assertion `left == right` failed left: 356.0 right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sC’est tout l’intérêt : un exemple faux est repéré tout de suite, au lieu de dérouter un lecteur des mois plus tard. Le message assertion `left == right` failed signifie que les deux valeurs comparées sont différentes : à gauche (left) la valeur calculée, à droite (right) celle que vous attendiez. Pour lancer uniquement les doc tests, utilisez cargo test --doc.
Cacher des lignes de préparation avec #
Section intitulée « Cacher des lignes de préparation avec # »Parfois, un exemple a besoin de lignes qui distrairaient le lecteur. Dans un doc test, une ligne qui commence par # (un dièse puis une espace) est compilée mais pas affichée dans la documentation générée.
L’usage le plus courant : vous voulez montrer l’opérateur ?, mais ? ne fonctionne que dans une fonction qui renvoie un Result. Vous cachez donc votre propre main :
/// Lit une quantité comme `"250g"` et renvoie le nombre de grammes.////// # Examples////// ```/// use kitchen::parse_grams;////// # fn main() -> Result<(), String> {/// let flour = parse_grams("250g")?;/// assert_eq!(flour, 250);////// assert!(parse_grams("beaucoup").is_err());/// # Ok(())/// # }/// ```pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' devrait se terminer par g"))?; number .parse() .map_err(|_| format!("'{number}' n'est pas un nombre entier"))}Dans le navigateur, le lecteur ne voit que les quatre lignes intéressantes : le use, l’appel avec ? et les deux assertions. Le test, lui, compile tout.
ignore, no_run et should_panic
Section intitulée « ignore, no_run et should_panic »Tous les exemples ne peuvent pas simplement s’exécuter. Vous pouvez placer un mot après les backticks d’ouverture pour dire à rustdoc quoi faire :
```no_run: compiler l’exemple, mais ne pas l’exécuter. Idéal pour du code qui lit des fichiers, ouvre des connexions réseau ou tourne en boucle à l’infini.```should_panic: l’exécuter et s’attendre à une panic. Le test échoue s’il ne panique pas.```ignore: ne pas le compiler du tout. À utiliser pour des ébauches et du pseudo-code. Essayez d’abordno_run, car un exemple ignoré peut devenir obsolète sans que personne ne s’en aperçoive.```text: ce n’est pas du code Rust, juste du texte (par exemple, une sortie d’exemple).
/// Partage `total` grammes en `portions` parts égales.////// # Examples////// ```/// assert_eq!(kitchen::split_evenly(600, 3), 200);/// ```////// Zéro portion n'a aucun sens, donc ceci panique :////// ```should_panic/// kitchen::split_evenly(600, 0);/// ```pub fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions doit être supérieur à zéro"); } total / portions}
/// Charge une liste de courses depuis un fichier, un article par ligne.////// ```no_run/// let items = kitchen::load_list("courses.txt").unwrap();/// println!("{} articles à acheter", items.len());/// ```pub fn load_list(path: &str) -> std::io::Result<Vec<String>> { let text = std::fs::read_to_string(path)?; Ok(text.lines().map(String::from).collect())}Avec tous ces exemples dans kitchen, plus une ébauche ignore dans le commentaire //! de la crate, cargo test affiche :
running 7 teststest src/lib.rs - (line 8) ... ignoredtest src/lib.rs - load_list (line 87) - compile ... oktest src/lib.rs - celsius_to_fahrenheit (line 16) ... oktest src/lib.rs - Recipe (line 104) ... oktest src/lib.rs - parse_grams (line 59) ... oktest src/lib.rs - split_evenly (line 34) ... oktest src/lib.rs - split_evenly (line 40) - should panic ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.11sLe test no_run indique - compile : il a seulement été compilé. L’ordre des lignes peut changer d’une exécution à l’autre, car les tests tournent en parallèle.
Faire des liens vers d’autres éléments
Section intitulée « Faire des liens vers d’autres éléments »Dans un commentaire de documentation, écrivez un nom entre backticks et entre crochets, comme [`Vec`], et rustdoc le transforme en lien vers la page de cet élément. On appelle ça un lien intra-doc (intra-doc link). Cela fonctionne pour vos propres éléments comme pour tout ce qui se trouve dans la bibliothèque standard :
//! Un tout petit programme de recettes.
/// Une recette : un nom et une liste d'ingrédients avec leur poids en grammes.////// Créez-en une avec [`Recipe::new`] et remplissez-la avec [`Recipe::add`]./// Les ingrédients sont gardés dans un [`Vec`], dans l'ordre où vous les ajoutez.struct Recipe { /// Le nom affiché en haut de la recette. name: String, /// Chaque ingrédient et son poids en grammes. ingredients: Vec<(String, u32)>,}
impl Recipe { /// Crée une recette vide avec le nom donné. fn new(name: &str) -> Recipe { Recipe { name: name.to_string(), ingredients: Vec::new() } }
/// Ajoute un ingrédient à la fin de la liste. fn add(&mut self, ingredient: &str, grams: u32) { self.ingredients.push((ingredient.to_string(), grams)); }
/// Additionne le poids de tous les ingrédients. fn total_grams(&self) -> u32 { self.ingredients.iter().map(|(_, grams)| grams).sum() }}
fn main() { let mut pancakes = Recipe::new("Crêpes"); pancakes.add("farine", 200); pancakes.add("lait", 300); pancakes.add("œuf", 50);
println!("{}", pancakes.name); for (ingredient, grams) in &pancakes.ingredients { println!("- {ingredient} : {grams} g"); } println!("Total : {} g", pancakes.total_grams());}Sortie :
Crêpes- farine : 200 g- lait : 300 g- œuf : 50 gTotal : 550 gLes champs et les méthodes peuvent aussi avoir leurs propres commentaires ///, et chacun apparaît sur la page de la struct. Si un lien pointe vers quelque chose qui n’existe pas, cargo doc vous prévient. Ici, Recipe::push était une faute de frappe pour Recipe::add :
warning: unresolved link to `Recipe::push` --> src/lib.rs:100:63 |100 | /// Créez-en une avec [`Recipe::new`] et remplissez-la avec [`Recipe::push`]. | ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push` | = note: `#[warn(rustdoc::broken_intra_doc_links)]` on by defaultL’avertissement dit : « lien vers Recipe::push impossible à résoudre ; la struct Recipe n’a ni champ ni élément associé nommé push ».
Exiger de la documentation avec missing_docs
Section intitulée « Exiger de la documentation avec missing_docs »Pour une bibliothèque que d’autres personnes vont utiliser, vous pouvez demander au compilateur de vous signaler chaque élément public sans documentation. Placez ceci tout en haut de src/lib.rs :
#![warn(missing_docs)]Désormais, un commentaire de documentation oublié provoque un avertissement :
warning: missing documentation for a function --> src/lib.rs:138:1 |138 | pub fn grams_to_ounces(grams: f64) -> f64 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |note: the lint level is defined here --> src/lib.rs:1:9 | 1 | #![warn(missing_docs)] | ^^^^^^^^^^^^Le message signifie « documentation manquante pour une fonction », et la note indique la ligne qui a activé cette vérification. Le #! (avec un point d’exclamation) signifie que l’attribut s’applique à toute la crate. Il réclame aussi un commentaire //! pour la crate elle-même. Les éléments privés ne sont jamais signalés : ce n’est donc pas une corvée pour vos fonctions utilitaires internes.
docs.rs : un hébergement gratuit pour votre documentation
Section intitulée « docs.rs : un hébergement gratuit pour votre documentation »Quand vous publiez une crate sur crates.io (voir Cargo et les crates en Rust), docs.rs construit sa documentation automatiquement et l’héberge gratuitement, pour chaque version. Vous n’avez rien à faire. C’est pour cela que presque toutes les crates Rust ont une documentation qui se ressemble et dans laquelle on cherche facilement.
Deux astuces de plus :
- Lancez
rustup doc --stdpour ouvrir la documentation de la bibliothèque standard hors ligne dans votre navigateur. - Sur n’importe quelle page de docs.rs ou de la std, cliquez sur le lien Source pour lire le code d’une fonction. C’est une excellente façon d’apprendre.
Erreurs courantes
Section intitulée « Erreurs courantes »Oublier le nom de la crate dans un doc test. Un doc test se trouve en dehors de votre crate, donc ceci échoue :
error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope --> src/lib.rs:17:9 |17 | let f = celsius_to_fahrenheit(180.0); | ^^^^^^^^^^^^^^^^^^^^^ not found in this scopeLe compilateur ne trouve pas la fonction « dans cette portée ». La solution : kitchen::celsius_to_fahrenheit(180.0), ou une ligne use kitchen::celsius_to_fahrenheit; au début de l’exemple (cachée avec # si vous voulez).
Un commentaire de documentation qui ne documente rien. /// doit se trouver juste avant un élément. À l’intérieur du corps d’une fonction, utilisez // :
error[E0585]: found a documentation comment that doesn't document anything --> src/main.rs:3:5 |3 | /// Partager la nourriture en trois portions. | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: doc comments must come before what they document, if a comment was intended use `//`L’aide explique que les commentaires de documentation doivent précéder ce qu’ils documentent, et que si vous vouliez un simple commentaire, il faut utiliser //.
S’attendre à ce que les doc tests d’un binaire s’exécutent. Les doc tests s’exécutent pour les crates de bibliothèque (src/lib.rs). Les exemples des commentaires de documentation de src/main.rs ne sont pas testés. Si votre programme grandit, déplacez la logique dans src/lib.rs et gardez main.rs court.
Écrire une documentation qui répète le code. « Additionne deux nombres » au-dessus de fn add(a, b) n’aide personne. Expliquez ce que le lecteur ne peut pas voir : les unités (des grammes ? des secondes ?), les cas limites et les cas d’échec.
Récapitulatif
Section intitulée « Récapitulatif »///documente l’élément suivant, et//!documente la crate ou le module dans lequel il se trouve. Les deux utilisent le Markdown.- Utilisez les sections habituelles :
# Examples,# Panics,# Errorset# Safety. cargo doc --opengénère un site HTML pour votre crate et ses dépendances.- Les blocs de code des commentaires de documentation sont des doc tests :
cargo testles compile et les exécute. Cachez les lignes de préparation avec#, et utilisezno_run,should_panicouignoresi besoin. [`Nom`]crée un lien intra-doc, et#![warn(missing_docs)]vous signale les éléments publics non documentés.- Les crates publiées sur crates.io voient leur documentation hébergée automatiquement sur docs.rs.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel commentaire documente toute la crate quand il est placé en haut de src/lib.rs ?
2. Que fait cargo test d'un bloc de code placé dans un commentaire /// d'une bibliothèque ?
3. Dans un doc test, que fait une ligne qui commence par # (un dièse puis une espace) ?
4. Un exemple ouvre une connexion réseau. Vous voulez qu'il soit compilé mais pas exécuté. Quelle annotation utilisez-vous ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- The rustdoc book (en anglais) : le guide officiel de l’outil de documentation de Rust.
- Documentation tests — the rustdoc book (en anglais) : toutes les options des doc tests, y compris les lignes cachées et
compile_fail. - Linking to items by name — the rustdoc book (en anglais) : toutes les façons d’écrire des liens intra-doc.
- Publishing a crate — The Rust Book (en anglais) : écrire des commentaires de documentation utiles avant de publier.
- Documentation — Rust API Guidelines (en anglais) : une liste de contrôle de ce que contient une bonne documentation de crate.