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.
| Lint | Description |
|---|---|
comment-follows-attribute | Requires outer doc comments to appear before attributes. |
external-module-comments | Requires module documentation to be inside the module it documents. |
inconsistent-eyre-return | Requires eyre result return types to use color_eyre::eyre::Result<T>. |
inline-tests | Requires inline unit test modules to live in separate source files. |
long-queries | Flags sqlx query macros that span 50 or more lines. |
missing-context | Requires .context() on try expressions in non-test functions that return an eyre result. |
missing-install | Requires color_eyre::install()?; in test functions that return a color-eyre result. |
missing-instrument | Requires database query functions to use #[tracing::instrument]. |
pub-use-parent | Rejects public use declarations that reference parent paths through crate or super. |
test-attr-last | Requires the test marker attribute to be the last attribute. |
test-without-returntype | Requires 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(())
}