global-state
Level: warn · Article: Dependencies
Invisible dependencies are the cockroaches of software architecture: everywhere, impossible to count, surviving every refactor.
What it checks
A static with interior mutability (Mutex, RwLock, OnceLock,
OnceCell, LazyLock, Cell, RefCell, atomics), static mut, or a
lazy_static! block. Statics whose name contains LOG are exempt by
default: a logger is infrastructure nobody swaps in tests.
Don’t
#![allow(unused)]
fn main() {
static DATABASE: OnceLock<Database> = OnceLock::new();
impl User {
async fn load(id: &UserId) -> Result<User, LoadError> {
let db = DATABASE.get().ok_or(LoadError::NotConnected)?; // hidden dependency
db.query(id).await
}
}
}
Zero constructor parameters. Looks simple. Until two tests run in parallel against the same global, or you need to point it at another database.
Do
struct Users {
db: Database,
}
impl Users {
async fn load(&self, id: &UserId) -> Result<User, LoadError> {
self.db.query(id).await
}
}
#[tokio::main]
async fn main() -> Result<(), StartupError> {
// every dependency constructed in one place, then handed down
let config = Config::from_env()?;
let db = Database::connect(&config.db_url).await?;
let users = Users { db };
Api::new(users).serve().await
}
Exactly as complex as it actually is, and checked at compile time.
Options
[global-state]
allowed-names = ["LOG"] # substring match, case-insensitive
Silence it
#![allow(unused)]
fn main() {
// rabot: allow(global-state) compiled-once regex; a pure value, never swapped
static EMAIL: LazyLock<Regex> = LazyLock::new(|| Regex::new(..).unwrap());
}