//! Print a Cargo workspace's internal crate graph by layer (layer 0 = no internal deps) and
//! check membership.
//! CW009 member-missing a `members` entry with no Cargo.toml / a glob matching nothing
//! CW010 crate-not-in-workspace a Cargo.toml under the root that no `members` entry covers
//! CW011 dependency-cycle internal crates depending on each other (normal/build edges)
//! CW018 default-member-invalid a `default-members` entry that is not a member
//!
//! Build: rustc -O --edition 2021 scripts/crate_graph.rs -o /tmp/crate_graph
//! Usage: crate_graph <DIR|Cargo.toml> [--dot | --json] [--strict]
//! Exit: 0 acyclic (findings listed), 1 a dependency cycle (or any finding with --strict), 2 usage error.
mod cli;
mod toml;
mod workspace;
use cli::{Finding, Rule};
use toml::Val;
use workspace::Workspace;
const RULES: &[Rule] = &[
Rule { id: "CW009", name: "member-missing", severity: "high",
why: "Cargo fails to load the workspace when a member path has no Cargo.toml",
fix: "fix or remove the `members` entry (globs must match at least one crate directory)" },
Rule { id: "CW010", name: "crate-not-in-workspace", severity: "medium",
why: "a package under the workspace root that is not a member fails with 'current package believes it's in a workspace when it's not'",
fix: "add it to `members`, list it in `exclude`, or give it its own [workspace]" },
Rule { id: "CW011", name: "dependency-cycle", severity: "high",
why: "Cargo rejects cycles between packages (dev-dependency cycles are allowed but make layering meaningless)",
fix: "move the shared types into a lower crate both depend on, or invert one edge behind a trait" },
Rule { id: "CW018", name: "default-member-invalid", severity: "high",
why: "every `default-members` entry must also be a member; Cargo errors otherwise",
fix: "point it at an existing member directory or drop it" },
];
const USAGE: &str = "usage: crate_graph <DIR|Cargo.toml> [--dot | --json] [--strict]
Print the internal dependency graph of a Cargo workspace by layer (layer 0 has no internal
dependencies; layer N depends only on lower layers), report dependency cycles (CW011) and
membership problems: missing members (CW009), crates under the root that are not members
(CW010) and invalid default-members (CW018). Dev-dependencies are not graph edges.
--dot Graphviz output (cycle edges in red): crate_graph . --dot | dot -Tsvg > crates.svg";
/// Internal edges: crate -> internal crates it depends on (normal + build).
fn edges(ws: &Workspace) -> Vec<(String, Vec<String>)> {
let pkgs = ws.packages();
let names: Vec<String> = pkgs.iter().map(|m| m.name()).collect();
let wdeps = ws.root.doc.path(&["workspace", "dependencies"]).and_then(Val::as_table);
pkgs.iter()
.map(|m| {
let mut deps: Vec<String> = m
.deps()
.into_iter()
.filter(|d| !d.section.ends_with("dev-dependencies") && !d.section.ends_with("dev_dependencies"))
.filter(|d| d.path.is_some() || d.workspace && wdeps.and_then(|w| w.table(&d.key)).is_some_and(|t| t.get("path").is_some()))
.map(|d| {
// a workspace dep may rename: `x = { path = .., package = "real" }`
wdeps.and_then(|w| w.table(&d.key)).and_then(|t| t.get("package")).and_then(Val::as_str).map_or(d.package.clone(), str::to_string)
})
.filter(|n| names.contains(n))
.collect();
deps.sort();
deps.dedup();
(m.name(), deps)
})
.collect()
}
/// Layer per crate (None for crates on or behind a cycle) and the cycles found.
fn layers(g: &[(String, Vec<String>)]) -> (Vec<(String, Option<usize>)>, Vec<Vec<String>>) {
fn visit(n: &str, g: &[(String, Vec<String>)], stack: &mut Vec<String>, memo: &mut Vec<(String, Option<usize>)>, cycles: &mut Vec<Vec<String>>) -> Option<usize> {
if let Some((_, l)) = memo.iter().find(|(m, _)| m == n) {
return *l;
}
if let Some(pos) = stack.iter().position(|s| s == n) {
let mut cyc = stack[pos..].to_vec();
cyc.push(n.to_string());
if !cycles.iter().any(|c| c.len() == cyc.len() && cyc.iter().all(|x| c.contains(x))) {
cycles.push(cyc);
}
return None;
}
stack.push(n.to_string());
let deps = g.iter().find(|(m, _)| m == n).map(|(_, d)| d.clone()).unwrap_or_default();
let mut layer = Some(0);
for d in &deps {
match visit(d, g, stack, memo, cycles) {
Some(l) => layer = layer.map(|x| x.max(l + 1)),
None => layer = None,
}
}
stack.pop();
memo.push((n.to_string(), layer));
layer
}
let (mut memo, mut cycles) = (Vec::new(), Vec::new());
for (n, _) in g {
visit(n, g, &mut Vec::new(), &mut memo, &mut cycles);
}
(memo, cycles)
}
fn json_str(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
fn main() {
let opts = cli::parse(USAGE, RULES, &[], &["--dot"]);
let dot = opts.on("--dot");
let paths: Vec<&String> = opts.paths.iter().collect();
let [arg] = paths.as_slice() else { cli::die("pass one workspace directory or Cargo.toml") };
if *arg == "-" {
cli::die("crate_graph needs a workspace directory (it reads every member)");
}
let ws = workspace::load_arg(arg).unwrap_or_else(|e| cli::die(&e));
for w in &ws.warnings {
eprintln!("warning: {w}");
}
let g = edges(&ws);
let (memo, cycles) = layers(&g);
let layer_of = |n: &str| memo.iter().find(|(m, _)| m == n).and_then(|(_, l)| *l);
let in_cycle = |a: &str, b: &str| cycles.iter().any(|c| c.windows(2).any(|w| w[0] == a && w[1] == b));
// Membership findings.
let mut findings = Vec::new();
let root_rel = ws.root.rel.clone();
for (pat, line) in &ws.missing {
findings.push(Finding { file: root_rel.clone(), line: *line, id: "CW009", msg: format!("members entry \"{pat}\" matches no directory with a Cargo.toml"), fix: String::new() });
}
for s in &ws.strays {
findings.push(Finding { file: s.clone(), line: 1, id: "CW010", msg: format!("{s} is under the workspace root but not a member"), fix: String::new() });
}
let wt = ws.root.doc.table("workspace");
if let Some(dm) = wt.and_then(|w| w.get("default-members")).and_then(Val::as_array) {
let root_dir = ws.root.dir.canonicalize().unwrap_or_else(|_| ws.root.dir.clone());
for d in dm.iter().filter_map(Val::as_str) {
let dir = root_dir.join(d);
let is_member = ws.members.iter().any(|m| m.dir.canonicalize().map_or(false, |p| p == dir)) || dir == root_dir;
if !is_member {
findings.push(Finding { file: root_rel.clone(), line: wt.map_or(1, |w| w.line_of("default-members")), id: "CW018",
msg: format!("default-members entry \"{d}\" is not a workspace member"), fix: String::new() });
}
}
}
for c in &cycles {
findings.push(Finding { file: root_rel.clone(), line: 1, id: "CW011", msg: format!("cyclic dependency between workspace crates: {}", c.join(" -> ")), fix: String::new() });
}
findings.retain(|f| RULES.iter().any(|r| r.id == f.id && opts.enabled(r)));
for f in &mut findings {
f.fix = RULES.iter().find(|r| r.id == f.id).map_or(String::new(), |r| r.fix.to_string());
}
if dot {
println!("digraph workspace {{\n rankdir=BT;\n node [shape=box];");
for (n, deps) in &g {
println!(" {};", json_str(n));
for d in deps {
let red = if in_cycle(n, d) { " [color=red]" } else { "" };
println!(" {} -> {}{red};", json_str(n), json_str(d));
}
}
println!("}}");
} else if opts.json {
let crates: Vec<String> = g
.iter()
.map(|(n, deps)| {
let d: Vec<String> = deps.iter().map(|x| json_str(x)).collect();
format!("{{\"name\":{},\"layer\":{},\"deps\":[{}]}}", json_str(n), layer_of(n).map_or("null".into(), |l| l.to_string()), d.join(","))
})
.collect();
let cyc: Vec<String> = cycles.iter().map(|c| format!("[{}]", c.iter().map(|x| json_str(x)).collect::<Vec<_>>().join(","))).collect();
let fs: Vec<String> = findings.iter().map(|f| format!("{{\"file\":{},\"line\":{},\"rule\":{},\"message\":{}}}", json_str(&f.file), f.line, json_str(f.id), json_str(&f.msg))).collect();
println!("{{\"crates\":[{}],\"cycles\":[{}],\"findings\":[{}]}}", crates.join(","), cyc.join(","), fs.join(","));
} else {
let max = memo.iter().filter_map(|(_, l)| *l).max();
for layer in 0..=max.unwrap_or(0) {
let mut at: Vec<&str> = g.iter().filter(|(n, _)| layer_of(n) == Some(layer)).map(|(n, _)| n.as_str()).collect();
at.sort();
if !at.is_empty() {
println!("layer {layer}: {}", at.join(", "));
}
}
let cyclic: Vec<&str> = g.iter().filter(|(n, _)| layer_of(n).is_none()).map(|(n, _)| n.as_str()).collect();
if !cyclic.is_empty() {
println!("no layer (in or depending on a cycle): {}", cyclic.join(", "));
}
let w = g.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
println!("\nedges:");
for (n, deps) in g.iter().filter(|(_, d)| !d.is_empty()) {
println!(" {n:<w$} -> {}", deps.join(", "));
}
for f in &findings {
println!("\n{}:{}: {} {}\n fix: {}", f.file, f.line, f.id, f.msg, f.fix);
}
println!("\n{} crate(s), {} cycle(s), {} finding(s).", g.len(), cycles.len(), findings.len());
}
let code = if !cycles.is_empty() || (opts.strict && !findings.is_empty()) { 1 } else { 0 };
std::process::exit(code);
}
rust-borrow-checker-error-explainer
Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.
rust-error-handling-thiserror-anyhow-designer
Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.
rust-lifetime-annotation-advisor
Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.
rust-tokio-async-service-builder
Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.
rust-trait-design-reviewer
Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.
rust-unsafe-block-auditor
Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.