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—ifSomeThing
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 runcargo 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 thepanic!
. Similarly, document (in a# Safety
section) any requirements forunsafe
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 ofcargo 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.
Historically, this option used to be called intra_doc_link_resolution_failure
.
The default behavior of automatically
including README.md can be overridden with the readme
field in
Cargo.toml.