Documentação e doc tests em Rust
Toda página do docs.rs e toda página da documentação da biblioteca padrão foi escrita por um programador, dentro do próprio código-fonte, usando comentários especiais. O Rust transforma esses comentários em um site com um único comando. Você pode fazer exatamente a mesma coisa com o seu código.
E existe um segundo motivo para se importar com isso. Os exemplos de código da sua documentação são testes de verdade, os doc tests (testes de documentação). Quando você roda cargo test, o Rust compila e executa esses exemplos. Se você mudar a sua função e esquecer de atualizar o exemplo, o teste falha. A sua documentação não tem como ficar desatualizada sem ninguém perceber. Isso é raro em linguagens de programação, e é uma das coisas mais legais do Rust.
Comentários de documentação: /// e //!
Seção intitulada “Comentários de documentação: /// e //!”Você já viu os comentários normais na página de Comentários. Os comentários de documentação (doc comments) são parecidos, mas têm três caracteres:
///documenta o item logo abaixo dele: uma função, struct, enum, campo ou módulo.//!documenta o item dentro do qual ele está. No topo dosrc/lib.rsou dosrc/main.rs, ele descreve o crate inteiro.
//! Um programa de receitas bem pequeno.
/// Divide `total` gramas igualmente em `portions` porções.fn split_evenly(total: u32, portions: u32) -> u32 { total / portions}A primeira linha de um comentário de documentação deve ser uma frase curta de resumo. As ferramentas mostram essa linha em listas e resultados de busca, então capriche nela.
Comentários de documentação não mudam o que o seu programa faz. Aqui está uma função documentada sendo usada no main:
/// Divide `total` gramas igualmente em `portions` porções.////// O que sobrar é ignorado, então 10 gramas divididos por 3 dão 3.////// # Panics////// Entra em panic se `portions` for zero.fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("o número de porções deve ser maior que zero"); } total / portions}
fn main() { println!("{}", split_evenly(600, 3)); println!("{}", split_evenly(10, 3));}Saída:
2003Na maioria dos editores, se você passar o mouse sobre split_evenly no main, agora aparece esse texto numa janelinha. Só isso já vale o esforço.
Markdown nos comentários de documentação
Seção intitulada “Markdown nos comentários de documentação”Os comentários de documentação são escritos em Markdown, o mesmo formato usado no GitHub e em muitos aplicativos de chat. Você pode usar `código`, **negrito**, *itálico*, listas, links e títulos:
/// Devolve a temperatura do forno para um prato, em **graus Celsius**.////// Pratos conhecidos:////// - `"pão"`: 220 graus/// - `"bolo"`: 180 graus/// - qualquer outro: 200 graus////// *Sempre* confira o seu próprio forno, porque cada forno é diferente./// Veja [Celsius na Wikipédia](https://pt.wikipedia.org/wiki/Grau_Celsius).pub fn oven_temperature(dish: &str) -> u32 { match dish { "pão" => 220, "bolo" => 180, _ => 200, }}Deixe uma linha /// vazia entre os parágrafos, do mesmo jeito que você deixaria uma linha em branco num arquivo Markdown normal. Coloque nomes de funções, tipos e valores entre crases, para que eles apareçam como código.
As seções padrão
Seção intitulada “As seções padrão”Os programadores Rust usam alguns títulos sempre, para que quem lê saiba onde procurar. Você já viu um deles. Estes são os mais comuns:
# Examples: como usar o item. Quase todo item público deveria ter um.# Panics: quando a função pode entrar em panic, para quem chama conseguir evitar.# Errors: quando uma função que devolve umResultdevolve umErr, e por quê.# Safety: numaunsafe fn, as regras que quem chama precisa seguir (veja Unsafe em Rust).
Esses títulos ficam em inglês mesmo quando o resto do comentário está em português. É a convenção da comunidade, e ferramentas como o Clippy procuram exatamente esses nomes.
Aqui estão # Errors e # Safety num arquivo de biblioteca, o src/lib.rs:
/// Lê uma quantidade como `"250g"` e devolve o número de gramas.////// # Errors////// Devolve um erro se o texto não terminar em `g`/// ou se a parte antes dele não for um número inteiro.pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' deveria terminar em g"))?; number .parse() .map_err(|_| format!("'{number}' não é um número inteiro"))}
/// Devolve o primeiro ingrediente sem verificar se a lista está vazia.////// # Safety////// Quem chama precisa garantir que `ingredients` não está vazio./// Chamar isto com um slice vazio é comportamento indefinido.pub unsafe fn first_unchecked(ingredients: &[String]) -> &String { unsafe { ingredients.get_unchecked(0) }}Repare que os títulos usam um só #. Dentro de um comentário de documentação, # é um título de Markdown, não um atributo do Rust.
Gerando a documentação com cargo doc
Seção intitulada “Gerando a documentação com cargo doc”No resto desta página, vamos trabalhar numa pequena biblioteca. Crie com cargo new --lib kitchen. Depois, dentro do projeto, rode:
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.htmlO Cargo monta um site em HTML na pasta target/doc/, e o --open abre esse site no seu navegador. Ele tem exatamente a cara das páginas do docs.rs: uma barra lateral, uma caixa de busca e uma página para cada função e tipo. Ele também documenta todos os crates de que você depende, o que ajuda muito quando você está sem internet.
Por padrão, só os itens públicos (marcados com pub) aparecem, porque a documentação é para quem usa a sua biblioteca. Acrescente --document-private-items para incluir tudo.
Doc tests: exemplos que rodam
Seção intitulada “Doc tests: exemplos que rodam”Aqui está o começo do src/lib.rs da biblioteca kitchen. A função tem uma seção # Examples com um bloco de código dentro do comentário de documentação:
//! Pequenas ajudas para a cozinha.//!//! `kitchen` converte temperaturas do forno, divide comida em porções//! e lê quantidades como `"250g"`.
/// Converte uma temperatura de Celsius para 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}Agora rode 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.00sO Cargo encontrou o bloco de código na linha 10 (line 10), transformou esse bloco num programinha, compilou e executou. O assert_eq! conferiu a resposta. Algumas coisas para notar:
- O exemplo chama
kitchen::celsius_to_fahrenheit, com o nome do crate. Um doc test é compilado como se fosse o programa de outra pessoa usando a sua biblioteca, então ele só enxerga os itenspub, e você precisa do caminho completo ou de uma linhause kitchen::...;. - Você não escreve
fn main. O rustdoc coloca o seu exemplo dentro de um para você. - Use
assert!eassert_eq!(da página Testes em Rust) para mostrar e conferir o resultado ao mesmo tempo.
E se o exemplo estiver errado? Troque 356.0 por 350.0 e rode os testes de novo:
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.00sO Cargo escreve tudo isso em inglês: o teste falhou (FAILED), o valor calculado (left) foi 356.0 e o valor esperado (right) era 350.0. E é justamente essa a ideia. Um exemplo errado é pego na hora, em vez de confundir alguém meses depois. Para rodar só os doc tests, use cargo test --doc.
Escondendo linhas de preparação com #
Seção intitulada “Escondendo linhas de preparação com #”Às vezes um exemplo precisa de linhas que só distrairiam quem está lendo. Uma linha de um doc test que começa com # (cerquilha e espaço) é compilada, mas não aparece na documentação gerada.
O uso mais comum: você quer mostrar o operador ?, mas o ? só funciona dentro de uma função que devolve um Result. Então você esconde o seu próprio main:
/// Lê uma quantidade como `"250g"` e devolve o número de gramas.////// # Examples////// ```/// use kitchen::parse_grams;////// # fn main() -> Result<(), String> {/// let flour = parse_grams("250g")?;/// assert_eq!(flour, 250);////// assert!(parse_grams("bastante").is_err());/// # Ok(())/// # }/// ```pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' deveria terminar em g"))?; number .parse() .map_err(|_| format!("'{number}' não é um número inteiro"))}No navegador, quem lê só vê as quatro linhas interessantes: o use, a chamada com ? e os dois asserts. O teste continua compilando tudo.
ignore, no_run e should_panic
Seção intitulada “ignore, no_run e should_panic”Nem todo exemplo pode simplesmente rodar. Você pode colocar uma palavra logo depois das crases de abertura para dizer ao rustdoc o que fazer:
```no_run: compila o exemplo, mas não executa. Bom para código que lê arquivos, abre conexões de rede ou fica num loop infinito.```should_panic: executa e espera um panic. O teste falha se não acontecer um panic.```ignore: nem compila. Use para rascunhos e pseudocódigo. Tenteno_runprimeiro, porque um exemplo ignorado pode ficar desatualizado sem ninguém perceber.```text: não é código Rust, só texto (por exemplo, uma saída de exemplo).
/// Divide `total` gramas igualmente em `portions` porções.////// # Examples////// ```/// assert_eq!(kitchen::split_evenly(600, 3), 200);/// ```////// Zero porções não faz sentido, então isto entra em panic:////// ```should_panic/// kitchen::split_evenly(600, 0);/// ```pub fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("o número de porções deve ser maior que zero"); } total / portions}
/// Carrega uma lista de compras de um arquivo, um item por linha.////// ```no_run/// let items = kitchen::load_list("compras.txt").unwrap();/// println!("{} itens para comprar", 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())}Com tudo isso no kitchen, mais um rascunho com ignore no comentário //! do crate, o cargo test mostra:
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.09sO teste com no_run diz - compile: ele só foi compilado. O exemplo com ignore aparece como ignored (ignorado). A ordem das linhas pode mudar de uma execução para outra, porque os testes rodam em paralelo.
Links para outros itens
Seção intitulada “Links para outros itens”Dentro de um comentário de documentação, escreva um nome entre crases e colchetes, como [`Vec`], e o rustdoc transforma isso num link para a página daquele item. Isso se chama intra-doc link (link interno da documentação). Funciona para os seus próprios itens e para qualquer coisa da biblioteca padrão:
//! Um programa de receitas bem pequeno.
/// Uma receita: um nome e uma lista de ingredientes com o peso em gramas.////// Crie uma com [`Recipe::new`] e preencha com [`Recipe::add`]./// Os ingredientes ficam guardados em um [`Vec`], na ordem em que você os adiciona.struct Recipe { /// O nome que aparece no topo da receita. name: String, /// Cada ingrediente e o seu peso em gramas. ingredients: Vec<(String, u32)>,}
impl Recipe { /// Cria uma receita vazia com o nome informado. fn new(name: &str) -> Recipe { Recipe { name: name.to_string(), ingredients: Vec::new() } }
/// Adiciona um ingrediente ao fim da lista. fn add(&mut self, ingredient: &str, grams: u32) { self.ingredients.push((ingredient.to_string(), grams)); }
/// Soma o peso de todos os ingredientes. fn total_grams(&self) -> u32 { self.ingredients.iter().map(|(_, grams)| grams).sum() }}
fn main() { let mut pancakes = Recipe::new("Panquecas"); pancakes.add("farinha", 200); pancakes.add("leite", 300); pancakes.add("ovo", 50);
println!("{}", pancakes.name); for (ingredient, grams) in &pancakes.ingredients { println!("- {ingredient}: {grams} g"); } println!("Total: {} g", pancakes.total_grams());}Saída:
Panquecas- farinha: 200 g- leite: 300 g- ovo: 50 gTotal: 550 gCampos e métodos também podem ter os seus próprios comentários ///, e cada um aparece na página da struct. Se um link apontar para algo que não existe, o cargo doc avisa. Aqui, Recipe::push foi um erro de digitação no lugar de Recipe::add:
warning: unresolved link to `Recipe::push` --> src/lib.rs:98:51 |98 | /// Crie uma com [`Recipe::new`] e preencha com [`Recipe::push`]. | ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push` | = note: `#[warn(rustdoc::broken_intra_doc_links)]` on by defaultO aviso diz que o link não foi resolvido (unresolved link) porque a struct Recipe não tem nenhum campo ou item associado chamado push.
Exigindo documentação com missing_docs
Seção intitulada “Exigindo documentação com missing_docs”Numa biblioteca que outras pessoas vão usar, você pode pedir para o compilador lembrar você de todo item público sem documentação. Coloque isto bem no topo do src/lib.rs:
#![warn(missing_docs)]Agora, esquecer um comentário de documentação gera um aviso (warning) de documentação faltando (missing documentation):
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)] | ^^^^^^^^^^^^O #! (com ponto de exclamação) quer dizer que o atributo vale para o crate inteiro. Ele também pede um comentário //! para o próprio crate. Itens privados nunca entram no aviso, então isso não pesa nas suas funções auxiliares internas.
docs.rs: hospedagem grátis para a sua documentação
Seção intitulada “docs.rs: hospedagem grátis para a sua documentação”Quando você publica um crate no crates.io (veja Cargo e crates), o docs.rs gera a documentação automaticamente e hospeda de graça, para todas as versões. Você não precisa fazer nada. É por isso que quase todo crate Rust tem uma documentação com a mesma cara e fácil de pesquisar.
Mais duas dicas:
- Rode
rustup doc --stdpara abrir a documentação da biblioteca padrão sem internet, no seu navegador. - Em qualquer página do docs.rs ou da std, clique no link Source para ler o código por trás de uma função. É um ótimo jeito de aprender.
Erros comuns
Seção intitulada “Erros comuns”Esquecer o nome do crate num doc test. Um doc test fica fora do seu crate, então isto falha:
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 scopeO compilador diz que não encontrou a função neste escopo (not found in this scope). A correção é kitchen::celsius_to_fahrenheit(180.0), ou uma linha use kitchen::celsius_to_fahrenheit; no começo do exemplo (escondida com #, se você quiser).
Um comentário de documentação que não documenta nada. O /// precisa vir logo antes de um item. Dentro do corpo de uma função, use //:
error[E0585]: found a documentation comment that doesn't document anything --> src/main.rs:3:5 |3 | /// Divide a comida em três porções. | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: doc comments must come before what they document, if a comment was intended use `//`A mensagem diz que os comentários de documentação precisam vir antes daquilo que documentam e que, se a ideia era um comentário comum, você deveria usar //.
Esperar que os doc tests de um binário rodem. Os doc tests rodam em crates de biblioteca (src/lib.rs). Os exemplos nos comentários de documentação do src/main.rs não são testados. Se o seu programa crescer, passe a lógica para o src/lib.rs e deixe o main.rs pequeno.
Escrever documentação que só repete o código. “Soma dois números” em cima de fn add(a, b) não ajuda ninguém. Explique o que quem lê não consegue ver: unidades (gramas? segundos?), casos extremos e quando a função falha.
///documenta o próximo item, e//!documenta o crate ou módulo em que ele está. Os dois usam Markdown.- Use as seções padrão:
# Examples,# Panics,# Errorse# Safety. cargo doc --opengera um site em HTML para o seu crate e as dependências dele.- Blocos de código nos comentários de documentação são doc tests: o
cargo testcompila e executa esses blocos. Esconda linhas de preparação com#e useno_run,should_panicouignorequando precisar. [`Nome`]cria um intra-doc link, e#![warn(missing_docs)]lembra você dos itens públicos sem documentação.- Crates publicados no crates.io ganham a documentação hospedada no docs.rs automaticamente.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Qual comentário documenta o crate inteiro quando fica no topo do src/lib.rs?
2. O que o cargo test faz com um bloco de código dentro de um comentário /// de uma biblioteca?
3. Num doc test, o que faz uma linha que começa com # (cerquilha e espaço)?
4. Um exemplo abre uma conexão de rede. Você quer que ele seja compilado, mas não executado. Qual anotação você usa?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- The rustdoc book (em inglês) — o guia oficial da ferramenta de documentação do Rust.
- Documentation tests — the rustdoc book (em inglês) — todas as opções de doc tests, incluindo linhas escondidas e
compile_fail. - Linking to items by name — the rustdoc book (em inglês) — todos os jeitos de escrever intra-doc links.
- Publishing a crate — The Rust Book (em inglês) — como escrever comentários de documentação úteis antes de publicar.
- Documentation — Rust API Guidelines (em inglês) — uma lista do que uma boa documentação de crate inclui.