Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ast-grep

In this project, we use ast-grep (Website) to implement our own linter rules. The language server command is ast-grep lsp.

Rule Index

sgconfig.yml loads every rule file in .ast-grep/rules. The is-test-function.yml file is a helper used by other rules and is not an independent lint.

LintDescription
comment-follows-attributeRequires outer doc comments to appear before attributes.
external-module-commentsRequires module documentation to be inside the module it documents.
inconsistent-eyre-returnRequires eyre result return types to use color_eyre::eyre::Result<T>.
inline-testsRequires inline unit test modules to live in separate source files.
long-queriesFlags sqlx query macros that span 50 or more lines.
missing-contextRequires .context() on try expressions in non-test functions that return an eyre result.
missing-installRequires color_eyre::install()?; in test functions that return a color-eyre result.
missing-instrumentRequires database query functions to use #[tracing::instrument].
pub-use-parentRejects public use declarations that reference parent paths through crate or super.
test-attr-lastRequires the test marker attribute to be the last attribute.
test-without-returntypeRequires recognized test functions to return color_eyre::eyre::Result<()>.

Why These Rules Exist

The rules encode repository conventions that compiler and formatter checks do not cover. They keep error reporting, test coverage, tracing, query layout, module structure, and compiler reliability consistent.

Run the Checks

For instructions on setting up the Nix development environment, entering the development shell, and using direnv, see the development setup guide. The Nix development environment already includes ast-grep.

If you do not use Nix, follow the ast-grep installation guide to install ast-grep.

Run the repository lint scan with:

ast-grep scan

This reports lint findings without changing files.

To run the CI-equivalent lint scan, treat all findings as errors:

ast-grep scan --error

If the optional prek Git hook is installed, this scan also runs automatically via prek on staged Rust files.

Run the ast-grep rule fixture tests separately:

ast-grep test

Rules

comment-follows-attribute

What It Does

Check that we consistently order doc-comments before attributes, not after.

Why Restrict This

To ensure our code is consistent.

Example

#[derive(SomeAttribute)]
/// A struct which is cool
///
/// This is a struct which is cool. This implies a temperature below 5°C,
/// except in July, where anything below 35°C is considered cooler than cool.
/// Ice-cold.
pub struct MyCoolStruct;

should instead be expressed as

/// A struct which is cool
///
/// This is a struct which is cool. This implies a temperature below 5°C,
/// except in July, where anything below 35°C is considered cooler than cool.
/// Ice-cold.
#[derive(SomeAttribute)]
pub struct MyCoolStruct;

external-module-comments

What It Does

Check that module documentation is inside the module it documents.

Why Restrict This

Keeping module documentation with the module implementation makes it easier to find and maintain.

Example

// lib.rs
/// Documentation for the foo module.
mod foo;

should instead be expressed as

// lib.rs
mod foo;
// foo.rs
//! Documentation for the foo module.

inconsistent-eyre-return

What It Does

Checks that we consistently use the full path syntax color_eyre::eyre::Result<()> for return values.

Why Restrict This

To ensure our code is consistent.

Example

fn my_doubling_function() -> Result<u8, color_eyre::eyre::Error> {
  u8 * 2
}

should instead be expressed as

fn my_doubling_function() -> color_eyre::eyre::Result<u8> {
  u8 * 2
}

inline-tests

What It Does

Check that every unit test is stored in its own file in the source tree, opposed to being inline.

Why Restrict This

llvm-cov has problems distinguishing test code from production code when counting coverage, resulting in warnings about our tests themselves having insufficient coverage. By having tests in a distinct file, we can make sure that we only check whether production code is covered.

Example

// file.rs
#[cfg(test)]
mod tests {
  #[test]
  fn my_test() {
    assert_eq!(2, 2);
  }
}

should instead be expressed as

// file.rs
mod tests;
// file/tests.rs

#![cfg(test)]

#[test]
fn my_test() {
  assert_eq!(2, 2);
}

or

// file.rs
#[cfg(test)]
mod tests;
// file/tests.rs
#[test]
fn my_test() {
  assert_eq!(2, 2);
}

long-queries

What It Does

Check that we have no inline sqlx::query! invocations which exceed 50 lines.

Why Restrict This

At this length, the queries are nicer to read in their own file. And we get to use pg_format.

Example

sqlx::query!(r#"
  SELECT
    row1,
    row2,
    ...,
    row100
  FROM
      foo_fighters;
"#).await?

should instead be expressed as

sqlx::query_file!("src/fetch_foo_fighters.sql")

missing-context

What It Does

Check that try expressions in non-test functions returning an eyre result have .context().

Why Restrict This

Context gives errors enough information to identify the operation that failed.

Example

fn read_config() -> color_eyre::eyre::Result<String> {
  Ok(std::fs::read_to_string("config.toml")?)
}

should instead be expressed as

fn read_config() -> color_eyre::eyre::Result<String> {
  Ok(std::fs::read_to_string("config.toml").context("reading config.toml")?)
}

missing-install

What It Does

Check that test functions returning color_eyre::eyre::Result call color_eyre::install()?;.

Why Restrict This

Installing color_eyre gives failed tests the same readable error reports as other application code.

Example

#[test]
fn my_test() -> color_eyre::eyre::Result<()> {
  assert_eq!(2, 2);
  Ok(())
}

should instead be expressed as

#[test]
fn my_test() -> color_eyre::eyre::Result<()> {
  color_eyre::install()?;
  assert_eq!(2, 2);
  Ok(())
}

missing-instrument

What It Does

Check that every database query has the #[tracing::instrument] annotation

Why Restrict This

We want every database query to create its own span, such that it’s easier to get the overview of the performance of individual queries.

Example

impl FooQueries : Database {
  async fn fetch_foo(&mut self) -> Result<i32, DatabaseError> {
    /* some code */
    Ok(0)
  }
}

should instead be expressed as

impl FooQueries : Database {
  // Arguments to the query will be logged if not skipped, be mindful of data sensitivity
  #[tracing::instrument(skip(self))]
  async fn fetch_foo(&mut self) -> Result<i32, DatabaseError> {
    /* some code */
    Ok(0)
  }
}

pub-use-parent

What It Does

Check that every instance of pub use only exports child elements. It does this by looking for instances of pub use crate::*. Sometimes, this can actually be legitimate, specifically in lib.rs, since it has no parent modules. But in this case, pub use crate::module can simply be replaced in pub use module, which is easier to read.

Why Restrict This

Having a clear hierarchical structure, where modules are only responsible for exporting their child modules makes visibility rules easier to follow.

Example

// src/lib.rs

mod mod1
mod mod2;

pub use mod2::my_cool_function;
// src/mod1.rs

pub fn my_cool_function() {

}
// src/mod2.rs

pub use crate::mod2::my_cool_function;

should instead be expressed as

// src/lib.rs

mod mod1
mod mod2;
pub use mod1::my_cool_function;
// src/mod1.rs

pub fn my_cool_function() {

}

test-attr-last

What It Does

Check that the attribute marking a function as a test is the last attribute.

Why Restrict This

Keeping the test marker last gives test declarations a consistent form. It also makes it much easier to recognize tests in ast-grep lints.

Example

#[test]
#[tracing::instrument]
fn my_test() {}

should instead be expressed as

#[tracing::instrument]
#[test]
fn my_test() {}

test-without-returntype

What It Does

Check that recognized test functions return color_eyre::eyre::Result<()>.

Why Restrict This

Returning a result gives tests better error reporting than panicking.

Example

#[test]
fn my_test() {
  assert_eq!(2, 2);
}

should instead be expressed as

#[test]
fn my_test() -> color_eyre::eyre::Result<()> {
  color_eyre::install()?;
  assert_eq!(2, 2);
  Ok(())
}