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, throwaway 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, for example:

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

However, there are some specific details about the format that are worth highlighting:

  • 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. Note that sample code in doc comments gets compiled and executed when you run cargo test (see Item 30), which helps it stay in sync with the code it's demonstrating.
  • 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 also means 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 (or cargo doc --no-deps --open to restrict the generated documentation to just the current crate).

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 attribute:1

#![allow(unused)]
#![deny(broken_intra_doc_links)]

fn main() {
/// 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:30
  |
4 | /// The bounding box for a [`Polygone`].
  |                              ^^^^^^^^ no item named `Polygone` in scope
  |

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 to come.

As ever, any tooling that detects potential problems should form a part of your CI 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 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.

Published Crate Documentation

If you publish your crate to crates.io, the documentation for your project will be visible at docs.rs, which is an official Rust project that builds and hosts documentation for published crates.

Note that crates.io and docs.rs are intended for slightly different audiences: crates.io is aimed at people who are choosing what crate to use, whereas docs.rs is intended for people figuring out how to use a crate they've already included (although there's obviously considerable overlap between the two).

As a result, the home page for a crate shows different content in each location:

  • docs.rs: Shows the top-level page from the output of cargo doc, as generated from //! comments in the top-level src/lib.rs file.
  • crates.io: Shows the content of any top-level README.md file2 that's included in the project's repo.

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 helps 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 as follows:

/// 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 behavior, 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 preceding advice 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 to see whether two objects might overlap,
/// before performing 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 behavior 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 principle is true for documentation: focusing on the semantics, the whys and the why nots, gives text that is more likely to remain helpful in the long run.

Things to Remember

  • Add doc comments for public API items.
  • Describe aspects of the code—such as panics and safety criteria—that aren't obvious from the code itself.
  • Don't describe things that are obvious from the code itself.
  • Make navigation clearer by providing cross-references and by making identifiers stand out.

1

Historically, this option used to be called intra_doc_link_resolution_failure.

2

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

3

Scott Meyers, More Effective C++ (Addison-Wesley), Item 32.