Item 27: Document public interfaces

If your crate is going to be used by other programmers, then it's a good idea to add documentation for its contents, particularly its public API. If your crate is more than just ephemeral, throw-away code, then that "other programmer" includes the you-of-the-future, when you have forgotten the details of your current code.

This is not advice that's specific to Rust, nor is it new advice – for example, Effective Java 2nd edition (from 2008) has Item 44: "Write doc comments for all exposed API elements".

The particulars of Rust's documentation comment format – Markdown-based, delimited with /// or //! – are covered in the Rust book, but there are some specific details worth highlighting.

/// Calculate the [`BoundingBox`] that exactly encompasses a pair
/// of [`BoundingBox`] objects.
pub fn union(a: &BoundingBox, b: &BoundingBox) -> BoundingBox {
  • Use a code font for code: For anything that would be typed into source code as-is, surround it with back-quotes to ensure that the resulting documentation is in a fixed-width font, making the distinction between code and text clear.
  • Add copious cross-references: Add a Markdown link for anything that might provide context for someone reading the documentation. In particular, cross-reference identifiers with the convenient [`SomeThing`] syntax – if SomeThing is in scope, then the resulting documentation will hyperlink to the right place.
  • Consider including example code: If it's not trivially obvious how to use an entrypoint, adding an # Examples section with sample code can be helpful.
  • Document panics and unsafe constraints: If there are inputs that cause a function to panic, document (in a # Panics section) the preconditions that are required to avoid the panic!. Similarly, document (in a # Safety section) any requirements for unsafe code.

The documentation for Rust's standard library provides an excellent example to emulate for all of these details.

Tooling

The Markdown format that's used for documentation comments results in elegant output, but this does also mean that there is an explicit conversion step (cargo doc). This in turn raises the possibility that something goes wrong along the way.

The simplest advice for this is just to read the rendered documentation after writing it, by running cargo doc --open.

You could also check that all the generated hyperlinks are valid, but that's a job more suited to a machine – via the broken_intra_doc_links crate attribute1:

#![deny(broken_intra_doc_links)]

/// The bounding box for a [`Polygone`].
#[derive(Clone, Debug)]
pub struct BoundingBox {

With this attribute enabled, cargo doc will detect invalid links:

error: unresolved link to `Polygone`
 --> docs/src/main.rs:4:29
  |
4 | /// The bounding box for a [`Polygone`].
  |                             ^^^^^^^^^^ no item named `Polygone` in scope
  |
note: the lint level is defined here
 --> docs/src/main.rs:2:9
  |
2 | #![deny(broken_intra_doc_links)]
  |         ^^^^^^^^^^^^^^^^^^^^^^
  = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: aborting due to previous error
error: could not document `docs`

You can also require documentation, by enabling the #![warn(missing_docs)] attribute for the crate. When this is enabled, the compiler will emit a warning for every undocumented public item. However, there's a risk that enabling this option will lead to poor quality documentation comments that are rushed out just to get the compiler to shut up – more on this below.

As ever, any tooling that detects potential problems should form a part of your continuous integration system (Item 32), to catch any regressions that creep in.

Additional Documentation Locations

The output from cargo doc is the primary place where your crate is documented, but it's not the only place – other parts of a Cargo project can help users figure out how to use your code.

The examples/ subdirectory of a Cargo project can hold the code for standalone binaries that make use of your crate. These programs are built and run very similarly to integration tests (Item 30), but are specifically intended to hold example code that illustrates the correct use of your crate's interface.

On a related note, bear in mind that the the integration tests under the tests/ subdirectory can also serve as examples for the confused user, even though their primary purpose is to test the crate's external interface.

For a published crate, any top-level README.md file2 for the project is presented as the main page content on the crates.io listing for the crate (for example, see the rand crate page). However, this content is not easily visible on the documentation pages, and so serves a somewhat different purpose – it's aimed at people who are choosing what crate to use, rather than figuring out how to use a crate they've already included (although there's obviously considerable overlap between the two).

What Not to Document

When a project requires that documentation be included for all public items (as mentioned in the first section), it's very easy to fall into the trap of having documentation that's a pointless waste of valuable pixels. Having the compiler warn about missing doc comments is only a proxy for what you really want – useful documentation – and is likely to incentivize programmers to do the minimum needed to silence the warning.

Good doc comments are a boon that help users understand the code they're using; bad doc comments impose a maintenance burden and increase the chance of user confusion when they get out of sync with the code. So how to distinguish between the two?

The primary advice is to avoid repeating in text something that's clear from the code. Item 1 exhorted you to encode as much semantics as possible into Rust's type system; once you've done that, allow the type system to document those semantics. Assume that the reader is familiar with Rust – possibly because they've read a helpful collection of Items describing effective use of the language – and don't repeat things that are clear from the signatures and types involved.

Returning to the previous example, an overly-verbose documentation comment might be:

    /// Return a new [`BoundingBox`] object that exactly encompasses a pair
    /// of [`BoundingBox`] objects.
    ///
    /// Parameters:
    ///  - `a`: an immutable reference to a `BoundingBox`
    ///  - `b`: an immutable reference to a `BoundingBox`
    /// Returns: new `BoundingBox` object.
    pub fn union(a: &BoundingBox, b: &BoundingBox) -> BoundingBox {

This comment repeats many details that are clear from the function signature, to no benefit.

Worse, consider what's likely to happen if the code gets refactored to store the result in one of the original arguments (which would be a breaking change, see Item 21). No compiler or tool complains that the comment isn't updated to match, so it's easy to end up with an out-of-sync comment:

    /// Return a new [`BoundingBox`] object that exactly encompasses a pair
    /// of [`BoundingBox`] objects.
    ///
    /// Parameters:
    ///  - `a`: an immutable reference to a `BoundingBox`
    ///  - `b`: an immutable reference to a `BoundingBox`
    /// Returns: new `BoundingBox` object.
    pub fn union(a: &mut BoundingBox, b: &BoundingBox) {

In contrast, the original comment survives the refactoring unscathed, because its text describes behaviour not syntactic details:

    /// Calculate the [`BoundingBox`] that exactly encompasses a pair
    /// of [`BoundingBox`] objects.
    pub fn union(a: &mut BoundingBox, b: &BoundingBox) {

The mirror image of the advice above also helps improve documentation: include in text anything that's not clear from the code. This includes preconditions, invariants, panics, error conditions and anything else that might surprise a user; if your code can't comply with the principle of least astonishment, make sure that the surprises are documented so you can at least say "I told you so".

Another common failure mode is when doc comments describe how some other code uses a method, rather than what the method does.

    /// Return the intersection of two [`BoundingBox`] objects, returning `None`
    /// if there is no intersection. The collision detection code in `hits.rs`
    /// uses this to do an initial check whether two objects might overlap, before
    /// perfoming the more expensive pixel-by-pixel check in `objects_overlap`.
    pub fn intersection(
        a: &BoundingBox,
        b: &BoundingBox,
    ) -> Option<BoundingBox> {

Comments like this are almost guaranteed to get out of sync: when the using code (here hits.rs) changes, the comment that describes the behaviour is nowhere nearby.

Rewording the comment to focus more on the why makes it more robust to future changes:

    /// Return the intersection of two [`BoundingBox`] objects, returning `None`
    /// if there is no intersection.  Note that intersection of bounding boxes
    /// is necessary but not sufficient for object collision -- pixel-by-pixel
    /// checks are still required on overlap.
    pub fn intersection(
        a: &BoundingBox,
        b: &BoundingBox,
    ) -> Option<BoundingBox> {

When writing software, it's good advice to "program in the future tense"3: structure the code to accommodate future changes. The same is true for documentation (albeit not literally): focusing on the semantics, the whys and the why nots, gives text that is more likely to remain helpful in the long run.


1: Historically, this option used to be called intra_doc_link_resolution_failure.

2: The default behaviour of automatically including README.md can be overridden with the readme field in Cargo.toml.

3: Scott Meyers "More Effective C++" Item 32.