xtask/src/clippy.rs
//! `cargo xtask clippy` — the lint gate: every crate monetize builds,
//! `--all-targets`, `-D warnings`.
//!
//! | question | answer |
//! |---|---|
//! | what does it lint? | the workspace, then each DETACHED crate (a `crates/*/Cargo.toml` with a `[workspace]` table of its own: monetize-admin, monetize-fast, monetize-build-thing) by `--manifest-path` |
//! | why the detached ones apart? | `--workspace` does not reach them, so a lint there was never seen: 2026-10-10 the toolchain bump to 1.97.1 left 117 in the workspace and 12 more in the detached three, unseen because nothing ran this |
//! | what does it need? | the sibling checkouts beside this one (`../edda`, `../Skidbladnir`, `../holger`), as every build of monetize does, and clippy for the pinned toolchain (`rustup component add clippy --toolchain 1.97.1`) |
//! | why not a hosted CI job? | a hosted runner has none of the private siblings, and a job that skips when they are absent is a check that can never fail. The gate is this verb; whoever builds monetize runs it |
//!
//! The fix for a lint is a fix in the code. A targeted `#[allow]` is for
//! generated code only (the `tonic` include modules carry theirs); a crate-
//! or workspace-wide allow turns this gate off without saying so.
use std::path::{Path, PathBuf};
/// The crates under `crates/` that are workspaces of their own, sorted.
///
/// Found rather than listed, so a new detached crate is gated the day it lands
/// instead of the day somebody remembers to add it here.
pub fn detached_crates(repo_root: &Path) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = std::fs::read_dir(repo_root.join("crates"))
.into_iter()
.flatten()
.flatten()
.map(|e| e.path().join("Cargo.toml"))
.filter(|m| std::fs::read_to_string(m).is_ok_and(|t| t.lines().any(|l| l.trim() == "[workspace]")))
.collect();
out.sort();
out
}
/// The clippy runs, in order: the workspace first, then each detached crate.
pub fn runs(repo_root: &Path) -> Vec<Vec<String>> {
let tail = ["--all-targets", "--", "-D", "warnings"].map(String::from);
let mut runs = vec![["clippy", "--workspace"].map(String::from).into_iter().chain(tail.clone()).collect()];
for m in detached_crates(repo_root) {
runs.push(["clippy".to_string(), "--manifest-path".to_string(), m.display().to_string()].into_iter().chain(tail.clone()).collect());
}
runs
}
/// Runs every clippy run, all of them even after a red one, and names the red.
pub fn run(repo_root: &Path) -> Result<usize, String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let all = runs(repo_root);
let mut red = Vec::new();
for args in &all {
eprintln!("\n── cargo {}", args.join(" "));
let ok = std::process::Command::new(&cargo)
.args(args)
.current_dir(repo_root)
.status()
.map_err(|e| format!("`{cargo}` could not start: {e}"))?
.success();
if !ok {
red.push(args[1..args.len() - 4].join(" "));
}
}
if red.is_empty() {
Ok(all.len())
} else {
Err(format!("clippy is red in: {}", red.join("; ")))
}
}