Rust Documentation and Doc Tests
Every page on docs.rs and every page of the standard library docs was written by a programmer, inside the source code, using special comments. Rust turns those comments into a website with one command. You can do exactly the same for your own code.
There’s a second reason to care. The code examples in your documentation are real tests. When you run cargo test, Rust compiles and runs them. If you change your function and forget to update the example, the test fails. Your documentation can’t quietly go out of date. That’s rare in programming languages, and it’s one of the nicest things about Rust.
Doc comments: /// and //!
Section titled “Doc comments: /// and //!”You met normal comments on the Comments page. Doc comments look similar but have three characters:
///documents the item below it: a function, struct, enum, field, or module.//!documents the item it is inside. At the top ofsrc/lib.rsorsrc/main.rs, it describes the whole crate.
//! A tiny recipe program.
/// Splits `total` grams evenly into `portions`.fn split_evenly(total: u32, portions: u32) -> u32 { total / portions}The first line of a doc comment should be a short summary sentence. Tools show that line in lists and search results, so make it count.
Doc comments don’t change what your program does. Here is a documented function used from main:
/// Splits `total` grams evenly into `portions`.////// Any grams left over are ignored, so 10 grams split 3 ways gives 3.////// # Panics////// Panics if `portions` is zero.fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions must be greater than zero"); } total / portions}
fn main() { println!("{}", split_evenly(600, 3)); println!("{}", split_evenly(10, 3));}Output:
2003In most editors, hovering over split_evenly in main now shows this text in a pop-up. That alone is worth the effort.
Markdown in doc comments
Section titled “Markdown in doc comments”Doc comments are written in Markdown, the same format used on GitHub and in many chat apps. You can use `code`, **bold**, *italics*, lists, links, and headings:
/// Returns the oven temperature for a dish, in **degrees Celsius**.////// Supported dishes:////// - `"bread"`: 220 degrees/// - `"cake"`: 180 degrees/// - anything else: 200 degrees////// *Always* check your own oven, because ovens differ./// See [Celsius on Wikipedia](https://en.wikipedia.org/wiki/Celsius).pub fn oven_temperature(dish: &str) -> u32 { match dish { "bread" => 220, "cake" => 180, _ => 200, }}Leave an empty /// line between paragraphs, just like you’d leave an empty line in a normal Markdown file. Wrap names of functions, types, and values in backticks so they show up as code.
The standard sections
Section titled “The standard sections”Rust programmers use a few headings again and again, so readers know where to look. You’ve already seen one. These are the common ones:
# Examples: how to use the item. Almost every public item should have one.# Panics: when the function can panic, so callers can avoid it.# Errors: when a function returning aResultgives back anErr, and why.# Safety: for anunsafe fn, the rules the caller must follow (see Unsafe Rust).
Here are # Errors and # Safety in a library file, src/lib.rs:
/// Reads an amount like `"250g"` and returns the number of grams.////// # Errors////// Returns an error if the text does not end in `g`,/// or if the part before it is not a whole number.pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' should end in g"))?; number .parse() .map_err(|_| format!("'{number}' is not a whole number"))}
/// Returns the first ingredient without checking that the list is empty.////// # Safety////// The caller must make sure `ingredients` is not empty./// Calling this on an empty slice is undefined behavior.pub unsafe fn first_unchecked(ingredients: &[String]) -> &String { unsafe { ingredients.get_unchecked(0) }}Notice that the headings use one #. Inside a doc comment, # is a Markdown heading, not a Rust attribute.
Building the docs with cargo doc
Section titled “Building the docs with cargo doc”For the rest of this page we’ll work on a small library. Create it with cargo new --lib kitchen. Then, inside the project, run:
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 builds an HTML website in target/doc/ and --open opens it in your browser. It looks exactly like the pages on docs.rs: a sidebar, a search box, and one page per function and type. It also documents every crate you depend on, which is handy when you’re offline.
Only public items (marked pub) show up by default, because the docs are for people who use your library. Add --document-private-items to include everything.
Doc tests: examples that run
Section titled “Doc tests: examples that run”Here is the start of src/lib.rs in the kitchen library. The function has an # Examples section with a code block inside the doc comment:
//! Small helpers for the kitchen.//!//! `kitchen` converts oven temperatures, splits food into portions,//! and reads amounts like `"250g"`.
/// Converts a temperature from Celsius to 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}Now run 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 found the code block at line 10, turned it into a tiny program, compiled it, and ran it. The assert_eq! checked the answer. A few things to notice:
- The example calls
kitchen::celsius_to_fahrenheit, with the crate name. A doc test is compiled as if it were someone else’s program using your library, so it only seespubitems, and you need the full path or ause kitchen::...;line. - You don’t write
fn main. Rustdoc wraps your example in one for you. - Use
assert!andassert_eq!(from the Testing page) to show and check the result at the same time.
What if the example is wrong? Change 356.0 to 350.0 and run the tests again:
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.00sThat’s the point. A wrong example is caught right away, instead of confusing a reader months later. To run only the doc tests, use cargo test --doc.
Hiding setup lines with #
Section titled “Hiding setup lines with #”Sometimes an example needs lines that would distract the reader. A line in a doc test that starts with # (hash and space) is compiled but not shown in the generated docs.
The most common use: you want to show the ? operator, but ? only works inside a function that returns a Result. So you hide your own main:
/// Reads an amount like `"250g"` and returns the number of grams.////// # Examples////// ```/// use kitchen::parse_grams;////// # fn main() -> Result<(), String> {/// let flour = parse_grams("250g")?;/// assert_eq!(flour, 250);////// assert!(parse_grams("a lot").is_err());/// # Ok(())/// # }/// ```pub fn parse_grams(text: &str) -> Result<u32, String> { let number = text .strip_suffix('g') .ok_or(format!("'{text}' should end in g"))?; number .parse() .map_err(|_| format!("'{number}' is not a whole number"))}In the browser, the reader only sees the four interesting lines: the use, the ? call, and the two asserts. The test still compiles everything.
ignore, no_run, and should_panic
Section titled “ignore, no_run, and should_panic”Not every example can simply run. You can put a word after the opening backticks to tell rustdoc what to do:
```no_run: compile the example, but don’t run it. Good for code that reads files, opens network connections, or loops forever.```should_panic: run it and expect a panic. The test fails if it doesn’t panic.```ignore: don’t compile it at all. Use it for sketches and pseudo-code. Tryno_runfirst, because an ignored example can go out of date without anyone noticing.```text: not Rust code at all, just text (for example, sample output).
/// Splits `total` grams evenly into `portions`.////// # Examples////// ```/// assert_eq!(kitchen::split_evenly(600, 3), 200);/// ```////// Zero portions makes no sense, so this panics:////// ```should_panic/// kitchen::split_evenly(600, 0);/// ```pub fn split_evenly(total: u32, portions: u32) -> u32 { if portions == 0 { panic!("portions must be greater than zero"); } total / portions}
/// Loads a shopping list from a file, one item per line.////// ```no_run/// let items = kitchen::load_list("shopping.txt").unwrap();/// println!("{} items to buy", 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())}With all of these in kitchen, plus an ignore sketch in the crate’s //! comment, cargo test reports:
running 7 teststest src/lib.rs - (line 8) ... ignoredtest src/lib.rs - load_list (line 87) - compile ... oktest src/lib.rs - split_evenly (line 34) ... oktest src/lib.rs - split_evenly (line 40) - should panic ... oktest src/lib.rs - parse_grams (line 59) ... oktest src/lib.rs - celsius_to_fahrenheit (line 16) ... oktest src/lib.rs - Recipe (line 104) ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01sThe no_run test says - compile: it was only built. The order of the lines can change between runs, because tests run in parallel.
Linking to other items
Section titled “Linking to other items”Inside a doc comment, write a name in backticks and square brackets, like [`Vec`], and rustdoc turns it into a link to that item’s page. This is called an intra-doc link. It works for your own items and for anything in the standard library:
//! A tiny recipe program.
/// A recipe: a name and a list of ingredients with their weight in grams.////// Create one with [`Recipe::new`] and fill it with [`Recipe::add`]./// The ingredients are kept in a [`Vec`], in the order you add them.struct Recipe { /// The name shown at the top of the recipe. name: String, /// Each ingredient and its weight in grams. ingredients: Vec<(String, u32)>,}
impl Recipe { /// Creates an empty recipe with the given name. fn new(name: &str) -> Recipe { Recipe { name: name.to_string(), ingredients: Vec::new() } }
/// Adds an ingredient to the end of the list. fn add(&mut self, ingredient: &str, grams: u32) { self.ingredients.push((ingredient.to_string(), grams)); }
/// Adds up the weight of every ingredient. fn total_grams(&self) -> u32 { self.ingredients.iter().map(|(_, grams)| grams).sum() }}
fn main() { let mut pancakes = Recipe::new("Pancakes"); pancakes.add("flour", 200); pancakes.add("milk", 300); pancakes.add("egg", 50);
println!("{}", pancakes.name); for (ingredient, grams) in &pancakes.ingredients { println!("- {ingredient}: {grams} g"); } println!("Total: {} g", pancakes.total_grams());}Output:
Pancakes- flour: 200 g- milk: 300 g- egg: 50 gTotal: 550 gFields and methods can have their own /// comments too, and each one shows up on the struct’s page. If a link points to something that doesn’t exist, cargo doc warns you. Here Recipe::push was a typo for Recipe::add:
warning: unresolved link to `Recipe::push` --> src/lib.rs:100:56 |100 | /// Create one with [`Recipe::new`] and fill it with [`Recipe::push`]. | ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push` | = note: `#[warn(rustdoc::broken_intra_doc_links)]` on by defaultRequiring docs with missing_docs
Section titled “Requiring docs with missing_docs”For a library other people will use, you can ask the compiler to remind you about every public item without docs. Put this at the very top of src/lib.rs:
#![warn(missing_docs)]Now forgetting a doc comment gives a warning:
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)] | ^^^^^^^^^^^^The #! (with an exclamation mark) means the attribute applies to the whole crate. It also wants a //! comment for the crate itself. Private items are never reported, so this is not a burden for internal helper functions.
docs.rs: free hosting for your docs
Section titled “docs.rs: free hosting for your docs”When you publish a crate to crates.io (see Cargo & Crates), docs.rs builds its documentation automatically and hosts it for free, for every version. You don’t have to do anything. That’s why nearly every Rust crate has docs that look the same and are easy to search.
Two more tips:
- Run
rustup doc --stdto open the standard library docs offline in your browser. - On any docs.rs or std page, click the Source link to read the code behind a function. It’s a great way to learn.
Common mistakes
Section titled “Common mistakes”Forgetting the crate name in a doc test. A doc test is outside your crate, so this fails:
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 scopeThe fix is kitchen::celsius_to_fahrenheit(180.0), or a use kitchen::celsius_to_fahrenheit; line at the top of the example (hidden with # if you like).
A doc comment that doesn’t document anything. /// must come right before an item. Inside a function body, use //:
error[E0585]: found a documentation comment that doesn't document anything --> src/main.rs:3:5 |3 | /// Split the food into three portions. | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: doc comments must come before what they document, if a comment was intended use `//`Expecting doc tests in a binary to run. Doc tests run for library crates (src/lib.rs). Examples in the doc comments of src/main.rs are not tested. If your program grows, move the logic into src/lib.rs and keep main.rs small.
Writing docs that repeat the code. “Adds two numbers” above fn add(a, b) doesn’t help. Explain what the reader can’t see: units (grams? seconds?), edge cases, and when it fails.
///documents the next item, and//!documents the crate or module it’s inside. Both use Markdown.- Use the standard sections:
# Examples,# Panics,# Errors, and# Safety. cargo doc --openbuilds an HTML site for your crate and its dependencies.- Code blocks in doc comments are doc tests:
cargo testcompiles and runs them. Hide setup lines with#, and useno_run,should_panic, orignorewhen needed. [`Name`]creates an intra-doc link, and#![warn(missing_docs)]reminds you about undocumented public items.- Crates published to crates.io get their docs hosted on docs.rs automatically.
Quick check
Section titled “Quick check”Quick check
1. Which comment documents the whole crate when placed at the top of src/lib.rs?
2. What does cargo test do with a code block inside a /// comment of a library?
3. In a doc test, what does a line starting with # (hash and space) do?
4. An example opens a network connection. You want it compiled but not run. Which annotation do you use?
Score: 0 / 4
Read more
Section titled “Read more”- The rustdoc book — the official guide to Rust’s documentation tool.
- Documentation tests — the rustdoc book — every doc test option, including hidden lines and
compile_fail. - Linking to items by name — the rustdoc book — all the ways to write intra-doc links.
- Publishing a crate — The Rust Book — writing useful doc comments before you publish.
- Documentation — Rust API Guidelines — a checklist of what good crate documentation includes.