Pular para o conteúdo

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.

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 do src/lib.rs ou do src/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:

200
3
▶ Experimente você mesmo

Na 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.

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.

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 um Result devolve um Err, e por quê.
  • # Safety: numa unsafe 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.

No resto desta página, vamos trabalhar numa pequena biblioteca. Crie com cargo new --lib kitchen. Depois, dentro do projeto, rode:

Terminal window
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.html

O 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.

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 test
test 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.00s

O 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 itens pub, e você precisa do caminho completo ou de uma linha use kitchen::...;.
  • Você não escreve fn main. O rustdoc coloca o seu exemplo dentro de um para você.
  • Use assert! e assert_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 test
test 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.00s

O 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.

À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.

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. Tente no_run primeiro, 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 tests
test src/lib.rs - (line 8) ... ignored
test src/lib.rs - load_list (line 87) - compile ... ok
test src/lib.rs - celsius_to_fahrenheit (line 16) ... ok
test src/lib.rs - Recipe (line 104) ... ok
test src/lib.rs - parse_grams (line 59) ... ok
test src/lib.rs - split_evenly (line 34) ... ok
test 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.09s

O 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.

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 g
Total: 550 g
▶ Experimente você mesmo

Campos 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 default

O 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.

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 --std para 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.

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 scope

O 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, # Errors e # Safety.
  • cargo doc --open gera 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 test compila e executa esses blocos. Esconda linhas de preparação com # e use no_run, should_panic ou ignore quando 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

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