Checklist

Project structure

File structure

Function structure

Principles

Writing software is not just an act of writing for the compiler but writing for your reviewer and those who will debug and change the code in the future. It should be viewed as a form of technical writing and apply the same principles, including:

Unlike writing, code is heavily cross-referenced (i.e. functions) and your reader has a limited memory shared with a lot of other context.

Project structure

Prefer mod.rs over name.rs (P-MOD)

When splitting a file into a directory, prefer mod.rs for the root module.

This ensures the module is an atomic unit for reading (e.g. browsing, searching) or operating on (e.g. moving, renaming). For example, when browsing on GitHub, it can be easy to overlook the existence of a name/ when seeing a name.rs. This is also consistent with lib.rs and main.rs.

Automation: enable clippy.self_named_module_files = "warn"

Example:

src/
  stuff/
    stuff_files.rs
  stuff.rs
  lib.rs

Use instead:

src/
  stuff/
    stuff_files.rs
    mod.rs
  lib.rs

Directory-root modules only re-export (P-DIR-MOD)

When splitting a file into a directory, the root of the directory (mod.rs, lib.rs) should only contain re-exports. All definitions should live in topically named files with mod.rs acting as a Table of Contents.

It can be confusing for a reader to figure out where to look when logic is split between topics and the root. It is better to consistently limit it to re-exporting.

Possible exceptions:

Prelude module only re-exports (P-PRELUDE-MOD)

If a prelude is desired, they are intended as an import convenience.

Users would not expect to find original logic in them.

Avoid #[path] (P-PATH-MOD)

Prefer the standard module lookup rules over #[path].

This makes the project structure more predictable.

Possible exceptions:

Example:

#[cfg(windows)]
#[path(foo_windows.rs)]
mod foo;
#[cfg(unix)]
#[path(foo_unix.rs)]
mod foo;

Use instead:

#[cfg(windows)]
#[path(foo_windows.rs)]
mod foo_windows;
#[cfg(windows)]
use foo_windows as foo;
#[cfg(unix)]
mod foo_unix;
#[cfg(unix)]
use foo_unix as foo;

API is a subset of file layout (P-API)

Modules should only re-export items from child modules and from sibling modules. Inline modules should be avoided.

A dependent of your package should be able to take what they know of the API to browse to the code in question, or at least a parent directory of it.

Exceptions:

Simple visibility (P-VISIBILITY)

Limit visibility to module-scope (no pub), crate-scope (pub(crate)), or dependent-scope (pub).

If an item is not fully abstracted within a module to have no pub, the differences in which crate-level module can access it is small and pub(crate) should be used. By using pub(crate), it reduces the friction in refactors. If there is a module from which it is fully abstracted, consider whether that module should instead be a crate.

Exceptions:

File structure

Private then public imports (M-PRIV-PUB-USE)

Group public imports with the file's public API.

Private imports are a detail of the implementation that mostly matters when editing code. A reader approaching the file is likely to skip over private imports to look at the public items to get the big picture.

Example:

use rand::RangeExt as _;
pub use regex::Regex;
use serde::Deserialize as _;

Use instead:

use rand::RangeExt as _;
use serde::Deserialize as _;

pub use regex::Regex;

Limit private imports (M-PRIV-USE)

Imports should be limited to where the meaning of the calling code remains obvious.

Uncommonly used imports can be frequently added and removed as the implementation changes, causing merge conflicts.

Expected use of imports

Example:

use std::collections::HashMap;
use std::collections::hash_map::Entry;
use serde::Deserialize;

Use instead:

use std::collections::HashMap;
use serde::Deserialize as _;

Import items individually (M-SINGLE-USE)

Compound imports increase the likelihood of merge conflicts.

Compound imports have higher friction for editing by hand, particularly when using editors optimize for line editing like vim.

Example:

use std::collections::{HashMap, hash_map::Entry};

Use instead:

use std::collections::HashMap;
use std::collections::hash_map::Entry;

Central item first (M-ITEM-TOC)

When there is a titular or quintessential item for a module, that should come first before any other items.

This is likely the item the reader is looking for when reading the module.

This item provides the context for understanding the rest of the module. The item serves as a Table of Contents for the rest of the module, providing jumping off points for what the reader might want to dig further into.

Note: this is a specialization of M-CALLER-CALLEE and M-PUB-PRIV.

Types then associated functions (M-TYPE-ASSOC)

To understand the intent, role, and how to use a type, you need to see the public interface for it as that provides the abstraction over the fields or variants.

Exceptions:

Example:

pub struct Foo { ... }

pub struct Bar { inner: BarInner }

pub enum BarInner { ... }

impl Foo {}

impl Bar {}

Use instead:

pub struct Foo { ... }

impl Foo {}

pub struct Bar { inner: BarInner }

pub enum BarInner { ... }

impl Bar {}

Associated functions then trait impls (M-ASSOC-TRAIT)

Typically, associated functions form the core API for a type and trait implementations augment that API.

Example:

pub struct Foo { ... }

impl Display for Foo {}

impl Foo {}

Instead use:

pub struct Foo { ... }

impl Foo {}

impl Display for Foo {}

Caller then callee (M-CALLER-CALLEE)

The caller provides context for the callees. The caller provides the context for understanding the callees. The caller serves as a Table of Contents, providing jumping off points for what the reader might want to dig further into.

The weaker the abstraction of the callee, the more immediately after the caller it should be.

Note: this is a generalization of M-ITEM-TOC.

Example:

const FOO: str = "...";

fn bar(s: &str) -> Bar { ... }

fn foo(arg: Arg) -> Foo {
    // ...
    let b = bar(FOO);
    // ...
}

Use instead:

fn foo(arg: Arg) -> Foo {
    // ...
    let b = bar(FOO);
    // ...
}

const FOO: &str = "...";

fn bar(s: &str) -> Bar { ... }

Public then private items (M-PUB-PRIV)

Group public items before private items, whether in a module, struct, or an impl block.

These are likely the item the reader is looking for when reading the block.

Grouping public items provides the context for understanding the rest of the module. Grouping public items serves as a Table of Contents for the rest of the module, providing jumping off points for what the reader might want to dig further into.

Note: this is a generalization of M-ITEM-TOC.

Use your judgement on ambiguity (M-AMBIGUITY)

The file structure guidelines above can be in tension with each other. They are roughly ordered but use your judgement for how to apply them in any given situation.

Function structure

Group related logic (F-GROUP)

Use newlines, or the lack of them, to group logic that shares a purpose. These are your "paragraphs" of your function.

Example:

fn report_warning_count(&self, ...) {
    let gctx = runner.bcx.gctx;
    runner.compilation.lint_warning_count += count.lints;
    let mut message = descriptive_pkg_name(&unit.pkg.name(), &unit.target, &unit.mode);
    message.push_str(" generated ");
    // ... builds up `message`
    gctx.shell().warn(message)
}

Use instead:

fn report_warning_count(&self, ...) {
    let gctx = runner.bcx.gctx;

    runner.compilation.lint_warning_count += count.lints;

    let mut message = descriptive_pkg_name(&unit.pkg.name(), &unit.target, &unit.mode);
    message.push_str(" generated ");
    // ... builds up `message`
    gctx.shell().warn(message)
}

Open with output variables (F-OUT)

If a block exists to incrementally build up state, open with the declaration of that state.

This will announce the intent of that group.

Example:

let sentinel = get_sentinel();
let items = get_items();
let mut message = String::new();
for item in items {
    // ...
}

Use instead:

let mut message = String::new();
let sentinel = get_sentinel();
let items = get_items();
for item in items {
    // ...
}

Blocks reflect business logic (F-VISUAL)

Emphasize the business logic using blocks, like the bodies of ifs, elses, and matchs. Where business logic has mutually exclusive paths, prefer if and else or match over early returns. Instead, prefer early returns for non-business bookkeeping.

Also prefer combinators (e.g. Iterator, Option, and Result methods) for non-business transformations.

This draws the reader's attention to the details that matter most, allowing them to dig in further as needed.

Example:

if let Some(foo) = foo {
    // ...
    if case {
      return Ok(...);
    }

    Ok(...)
} else {
    Err(...)
}

Use instead:

let foo = foo.ok_or_else(|| ...)?;
if case {
    Ok(...)
} else {
    Ok(...)
}

Pure combinators (F-COMBINATOR)

Closures passed to combinators (e.g. Iterator, Option, and Result methods) should not have side effects in the business logic.

When a reader is scanning the file, they are unlikely to parse through the details of the combinators and so should not have any surprises.

Exceptions: