articles

Guide on how to write documentation for a Rust crate

I have received (a lot of) requests about writing a guide on how to write documentation in Rust lately. I'm quite happy that people finally gets interested into this area so let's not let it rest and let's go!

Basics

Before explaining how to write nice documentation and everything, we need to cover the basics on how to actually write documentation with Rust. First thing to note, you can generate it using rustdoc. It is provided alongside the Rust compiler and you can call it like this:

$ cargo doc

And that's it! It'll generate all your crate's documentation (only the types and modules for the moment since I assume there is nothing documented yet!). To open the documentation in your web browser:

$ cargo doc --open

We're now done with the tooling. Let's see how to document things now!

Doc comments

There are two doc comments: /// and //!.

/// documents the item following it whereas //! documents the parent item. Example:

Run//! I document the current module!

/// I document "Foo"!
pub struct Foo;

And you can even mix both of them if you want (even though I'm not sure to see the point but who knows!):

Run/// bar
pub mod foo {
    //! foo
}

The documentation on foo will be "bar foo".

One last thing to add on this: the doc comments are using the Markdown CommonMark format. Which means it supports links, images, code blocks, HTML, etc... In here, I won't explain the syntax for all of them but I strongly recommend you to take a look at the CommonMark website if you want more information.

Documenting a crate

When a crate is published on crates.io, its documentation is automatically generated on docs.rs. When a user arrive on a crate on docs.rs, the first thing they'll see is the crate-level documentation:


Explanations: if you use the //! doc comment on your "main" file (src/lib.rs generally), it'll document the highest module level, aka the crate-level.

I guess it's useless to say it at this point but this is a must have! If a user arrives on a crate with nothing documented at the top-level, they'll just think that this crate is unmaintained or not yet ready. Therefore, it's VERY important to have this documentation block! It doesn't have to be fully exhaustive but it must exist at minimum.

We can even extend this "rule" to all the public items of your crate: they must be documented.

Writing a good documentation block

Documenting every public items is nice, but documenting them nicely is even better. A good plan to follow which is used in the standard library is the following:

[short explanations of what the item does]

[code example showing how to use it]

[OPTIONAL: more explanations and code examples in case some specific
 cases have to be explained in details]

I'll take the String::new method from the Rust documentation as example:

Run/// Creates a new empty `String`.
///
/// Given that the `String` is empty, this will not allocate any initial
/// buffer. While that means that this initial operation is very
/// inexpensive, it may cause excessive allocation later when you add
/// data. If you have an idea of how much data the `String` will hold,
/// consider the [`with_capacity`] method to prevent excessive
/// re-allocation.
///
/// [`with_capacity`]: #method.with_capacity
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// let s = String::new();
/// ```
pub const fn new() -> String {
    String { vec: Vec::new() }
}

So just like I explained, first a small introduction explaining what it does followed by a code example. A very important note about the code examples in the Rust documentation: they are tested. Which means that when you run cargo test, it also tests the code examples in the documentation. I'll come back on this after.

Another thing which makes reading the documentation very nice for the users: when talking about a type/module/item, link to it, always. Just like it's done for the with_capacity method in the String::new documentation as seen above.

Code examples

Like said above, the code examples in the documentation are tested. If no annotation is used on them, they are considered as Rust ones by default. So for example:

Run/// Example 1:
///
/// ```
/// let x = 12;
/// ```
///
/// Example 2:
///
/// ```rust
/// let x = 12;
/// ```
pub fn item() {}

Those two code examples are considered as Rust ones and will be tested. If a code block isn't a Rust one, either use the language name or text.

More control over code examples testing

It's possible that you want a code example to be compiled but not run (it's very common for I/O examples). You can tell rustdoc by adding no_run to your block:

Run/// Example
///
/// ```no_run
/// use std::fs::File;
///
/// let mut f = File::open("some_file.txt").expect("failed");
/// ```
pub fn item() {}

If you want a code block to considered as a Rust one (for the syntax highlighting) but you don't want to compile it, you can use ignore:

Run/// Example
///
/// ```ignore
/// hello!
/// ```
pub fn item() {}

It'll be marked on the documentation with a little sign on the left of the code example:


Now, if you want to show a code example that fails to compile and you want to be sure it fails at compilation, use compile_fail. In case you wonder what it could be used for, remember that Rust macros can extend the language's syntax and have their own compilation error messages.

Also, just like for the ignore tag, it'll be marked on the documentation:


I went very quickly through those explanations and didn't cover everything but you can go further by reading the rustdoc book and more precisely the Documentation tests chapter.

Hiding lines

Last small thing: you can hide lines from the code example display. For example:

Run/// Example
///
/// ```no_run
/// use std::fs::File;
///
/// # fn foo() -> std::io::Result<()> {
/// let mut f = File::create("foo.txt")?;
/// # Ok(())
/// # }
/// ```
pub fn item() {}

The code example will look like this:

Runuse std::fs::File;

let mut f = File::create("foo.txt")?;

All lines starting with # won't be printed, but they'll be tested.

Using rustdoc lints

Now that you have started documenting every public items of your crate, you might wonder "how to be sure I didn't miss one?". Luckily for you, there is a solution!

rustdoc provides a few lints, but we'll focus on two:

They are both allowed by default, which means they won't get trigger unless you activate them. To do so:

Run#![warn(missing_docs)]
#![warn(missing_doc_code_examples)]

Now, when running cargo doc, you will see warnings pointing the places missing documentation or code examples. Of course, I strongly recommend to use them.

cfg

I recently wrote a blog post about #[cfg(doctest)] and how important it is. Instead of explaining everything again, you can read it here.

I didn't mention something in this blog post though: there is also #[cfg(doc)]. It's quite convenient to provide platform or feature-specific items/information. However, this seems to be advanced usage of rustdoc so I'll just give you the link to the rustdoc book advanced features chapter.

Conclusion

So to sum up everything I wrote:

And don't forget: writing documentation is annoying/boring but you'll very likely be the first to use it! So don't write documentation for others but for yourself!

Posted on the 12/03/2020 at 11:00 by @GuillaumeGomez

Next article

New process-viewer release

Previous article

cfg(doctest) is stable and you should use it
Back to articles list
RSS feedRSS feed