use super::*;
#[derive(Parser, Debug, Clone)]
pub(crate) struct ModulesArgs {
#[arg(long, default_value = "Cargo.toml")]
pub(crate) manifest_path: PathBuf,
#[arg(short = 'p', long)]
pub(crate) package: Option<String>,
#[arg(long)]
pub(crate) lib: bool,
#[arg(long)]
pub(crate) bin: Option<String>,
#[arg(long)]
pub(crate) cfg_test: bool,
#[arg(short, long, value_enum, default_value_t = Metric::Pagerank)]
pub(crate) metric: Metric,
#[arg(long, value_enum, default_value_t = ModulesPreset::None)]
pub(crate) preset: ModulesPreset,
#[arg(short = 'n', long, default_value_t = 25)]
pub(crate) top: usize,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub(crate) format: OutputFormat,
#[arg(long, value_enum, default_value_t = ModuleEdgeKind::Uses)]
pub(crate) edge_kind: ModuleEdgeKind,
#[arg(long, value_enum, default_value_t = ModuleAggregate::File)]
pub(crate) aggregate: ModuleAggregate,
#[arg(long, default_value_t = 1.0)]
pub(crate) uses_weight: f64,
#[arg(long, default_value_t = 0.2)]
pub(crate) owns_weight: f64,
#[arg(long, default_value_t = 3)]
pub(crate) edges_top: usize,
#[arg(long, default_value_t = 2)]
pub(crate) members_top: usize,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) cache: bool,
#[arg(long, default_value_t = false)]
pub(crate) cache_refresh: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_externs: bool,
#[arg(long, default_value_t = false)]
pub(crate) include_externs: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_sysroot: bool,
#[arg(long, default_value_t = false)]
pub(crate) include_sysroot: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_fns: bool,
#[arg(long, default_value_t = false)]
pub(crate) include_fns: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_traits: bool,
#[arg(long, default_value_t = true)]
pub(crate) include_traits: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_types: bool,
#[arg(long, default_value_t = true)]
pub(crate) include_types: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub(crate) enum ModuleEdgeKind {
Uses,
Owns,
Both,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum ModulesPreset {
None,
FileFull,
FileApi,
NodeFull,
NodeApi,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum ModuleAggregate {
Node,
Module,
File,
}
#[derive(Parser, Debug, Clone)]
pub(crate) struct ModulesSweepArgs {
#[arg(long, default_value = "Cargo.toml")]
pub(crate) manifest_path: PathBuf,
#[arg(short = 'p', long)]
pub(crate) package: Vec<String>,
#[arg(long, default_value_t = false)]
pub(crate) all_packages: bool,
#[arg(long)]
pub(crate) lib: bool,
#[arg(long)]
pub(crate) bin: Option<String>,
#[arg(long)]
pub(crate) cfg_test: bool,
#[arg(short, long, value_enum, default_value_t = Metric::Pagerank)]
pub(crate) metric: Metric,
#[arg(long, value_enum, default_value_t = ModulesPreset::None)]
pub(crate) preset: ModulesPreset,
#[arg(long, default_value_t = true)]
pub(crate) summary_only: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) continue_on_error: bool,
#[arg(long, default_value_t = false)]
pub(crate) fail_fast: bool,
#[arg(short = 'n', long, default_value_t = 12)]
pub(crate) top: usize,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub(crate) format: OutputFormat,
#[arg(long, value_enum, default_value_t = ModuleEdgeKind::Uses)]
pub(crate) edge_kind: ModuleEdgeKind,
#[arg(long, value_enum, default_value_t = ModuleAggregate::File)]
pub(crate) aggregate: ModuleAggregate,
#[arg(long, default_value_t = 1.0)]
pub(crate) uses_weight: f64,
#[arg(long, default_value_t = 0.2)]
pub(crate) owns_weight: f64,
#[arg(long, default_value_t = 3)]
pub(crate) edges_top: usize,
#[arg(long, default_value_t = 2)]
pub(crate) members_top: usize,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) cache: bool,
#[arg(long, default_value_t = false)]
pub(crate) cache_refresh: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_externs: bool,
#[arg(long, default_value_t = false)]
pub(crate) include_externs: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_sysroot: bool,
#[arg(long, default_value_t = false)]
pub(crate) include_sysroot: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_fns: bool,
#[arg(long, default_value_t = false)]
pub(crate) include_fns: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_traits: bool,
#[arg(long, default_value_t = true)]
pub(crate) include_traits: bool,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub(crate) no_types: bool,
#[arg(long, default_value_t = true)]
pub(crate) include_types: bool,
}
pub(crate) fn run_modules_sweep(args: &ModulesSweepArgs) -> Result<()> {
let continue_on_error = if args.fail_fast {
false
} else {
args.continue_on_error
};
let packages: Vec<String> = if !args.package.is_empty() {
args.package.clone()
} else if args.all_packages {
let mut cmd = MetadataCommand::new();
cmd.manifest_path(&args.manifest_path);
let md = cmd
.exec()
.map_err(|e| anyhow!(e))
.with_context(|| "cargo metadata failed for modules-sweep")?;
let mut names: Vec<String> = md
.workspace_members
.iter()
.filter_map(|id| {
md.packages
.iter()
.find(|p| &p.id == id)
.map(|p| p.name.to_string())
})
.collect();
names.sort();
names
} else {
return Err(anyhow!(
"modules-sweep requires at least one -p/--package, or --all-packages"
));
};
if packages.is_empty() {
return Err(anyhow!("no packages selected"));
}
#[derive(Debug)]
struct SweepOneOk {
pkg: String,
rows: Vec<ModuleRow>,
nodes: usize,
edges: usize,
aggregate_label: String,
top_edges: Vec<(String, String, f64)>,
args: ModulesArgs,
}
#[derive(Debug)]
struct SweepOneErr {
pkg: String,
err: String,
}
#[derive(Debug)]
enum SweepOne {
Ok(SweepOneOk),
Err(SweepOneErr),
}
let template = ModulesArgs {
manifest_path: args.manifest_path.clone(),
package: None,
lib: args.lib,
bin: args.bin.clone(),
cfg_test: args.cfg_test,
metric: args.metric,
preset: args.preset,
top: args.top,
format: args.format,
edge_kind: args.edge_kind,
aggregate: args.aggregate,
uses_weight: args.uses_weight,
owns_weight: args.owns_weight,
edges_top: args.edges_top,
members_top: args.members_top,
cache: args.cache,
cache_refresh: args.cache_refresh,
no_externs: args.no_externs,
include_externs: args.include_externs,
no_sysroot: args.no_sysroot,
include_sysroot: args.include_sysroot,
no_fns: args.no_fns,
include_fns: args.include_fns,
no_traits: args.no_traits,
include_traits: args.include_traits,
no_types: args.no_types,
include_types: args.include_types,
};
let template = apply_modules_preset(&template);
let mut results: Vec<SweepOne> = Vec::new();
for pkg in &packages {
let mut one_args = template.clone();
one_args.package = Some(pkg.clone());
match run_modules_core(&one_args) {
Ok((rows, nodes, edges, aggregate_label, top_edges)) => {
results.push(SweepOne::Ok(SweepOneOk {
pkg: pkg.clone(),
rows,
nodes,
edges,
aggregate_label,
top_edges,
args: one_args,
}));
}
Err(e) => {
if !continue_on_error {
return Err(e)
.with_context(|| format!("modules-sweep failed for package `{pkg}`"));
}
results.push(SweepOne::Err(SweepOneErr {
pkg: pkg.clone(),
err: format!("{:#}", e),
}));
}
}
}
match args.format {
OutputFormat::Json => {
#[derive(Debug, Serialize)]
struct ModulesSweepPackageOut {
ok: bool,
error: Option<String>,
nodes: Option<usize>,
edges: Option<usize>,
aggregate_label: Option<String>,
rows: Option<Vec<ModuleRow>>,
}
#[derive(Debug, Serialize)]
struct ModulesSweepOut {
schema_version: u32,
ok: bool,
command: &'static str,
manifest_path: String,
preset: Option<String>,
effective: ModulesSweepEffective,
packages: HashMap<String, ModulesSweepPackageOut>,
}
#[derive(Debug, Serialize)]
struct ModulesSweepEffective {
aggregate: String,
edge_kind: String,
include_fns: bool,
include_types: bool,
include_traits: bool,
cache: bool,
cache_refresh: bool,
}
let mut pkgs: HashMap<String, ModulesSweepPackageOut> = HashMap::new();
for r in results {
match r {
SweepOne::Ok(ok) => {
pkgs.insert(
ok.pkg,
ModulesSweepPackageOut {
ok: true,
error: None,
nodes: Some(ok.nodes),
edges: Some(ok.edges),
aggregate_label: Some(ok.aggregate_label),
rows: Some(ok.rows),
},
);
}
SweepOne::Err(er) => {
pkgs.insert(
er.pkg,
ModulesSweepPackageOut {
ok: false,
error: Some(er.err),
nodes: None,
edges: None,
aggregate_label: None,
rows: None,
},
);
}
}
}
let out = ModulesSweepOut {
schema_version: 1,
ok: pkgs.values().all(|p| p.ok),
command: "modules-sweep",
manifest_path: args.manifest_path.display().to_string(),
preset: match args.preset {
ModulesPreset::None => None,
p => Some(format!("{p:?}")),
},
effective: ModulesSweepEffective {
aggregate: format!("{:?}", template.aggregate),
edge_kind: format!("{:?}", template.edge_kind),
include_fns: template.include_fns,
include_types: template.include_types,
include_traits: template.include_traits,
cache: template.cache,
cache_refresh: template.cache_refresh,
},
packages: pkgs,
};
println!("{}", serde_json::to_string_pretty(&out)?);
Ok(())
}
OutputFormat::Text => {
println!("pkgrank modules-sweep");
println!(" manifest: {}", args.manifest_path.display());
println!(" packages: {} ({})", packages.len(), packages.join(", "));
if !matches!(args.preset, ModulesPreset::None) {
println!(" preset: {:?}", args.preset);
}
println!(
" effective: aggregate={:?} edge_kind={:?} include=[fns:{} types:{} traits:{}] cache={} refresh={}",
template.aggregate,
template.edge_kind,
template.include_fns,
template.include_types,
template.include_traits,
template.cache,
template.cache_refresh
);
println!(
" mode: summary_only={} continue_on_error={}\n",
args.summary_only, continue_on_error
);
let header = format!(
"{:<14} {:>6} {:>5} {:>5} {:>10} {:>10} {:>10} {}",
"package", "status", "nodes", "edges", "top_pr", "top_cons", "top_between", "error"
);
println!("{header}");
println!("{:─<width$}", "", width = header.chars().count());
for r in &results {
match r {
SweepOne::Ok(ok) => {
let top_pr = ok
.rows
.iter()
.max_by(|a, b| a.pagerank.total_cmp(&b.pagerank))
.map(|x| format!("{}({:.3})", x.node, x.pagerank))
.unwrap_or_else(|| "-".to_string());
let top_cons = ok
.rows
.iter()
.max_by(|a, b| a.consumers_pagerank.total_cmp(&b.consumers_pagerank))
.map(|x| format!("{}({:.3})", x.node, x.consumers_pagerank))
.unwrap_or_else(|| "-".to_string());
let top_between = ok
.rows
.iter()
.max_by(|a, b| a.betweenness.total_cmp(&b.betweenness))
.map(|x| format!("{}({:.3})", x.node, x.betweenness))
.unwrap_or_else(|| "-".to_string());
println!(
"{:<14} {:>6} {:>5} {:>5} {:>10} {:>10} {:>10}",
ok.pkg,
"ok",
ok.nodes,
ok.edges,
truncate_cell(&top_pr, 10),
truncate_cell(&top_cons, 10),
truncate_cell(&top_between, 10)
);
}
SweepOne::Err(er) => {
let first_line = er.err.lines().next().unwrap_or(&er.err);
println!(
"{:<14} {:>6} {:>5} {:>5} {:>10} {:>10} {:>10} {}",
er.pkg,
"err",
"-",
"-",
"-",
"-",
"-",
truncate_cell(first_line, 60)
);
}
}
}
if args.summary_only {
Ok(())
} else {
for r in &results {
match r {
SweepOne::Ok(ok) => {
println!("\n{:═<110}", "");
println!("package: {}", ok.pkg);
print_modules_text(
&ok.args,
&ok.rows,
ok.nodes,
ok.edges,
&ok.aggregate_label,
&ok.top_edges,
);
}
SweepOne::Err(er) => {
println!("\n{:═<110}", "");
println!("package: {}", er.pkg);
println!("status: error");
println!("error:\n{}", er.err.trim());
}
}
}
Ok(())
}
}
}
}
fn truncate_cell(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let mut out = String::new();
for c in s.chars().take(max.saturating_sub(1)) {
out.push(c);
}
out.push('\u{2026}');
out
}
#[derive(Debug, Serialize)]
pub(crate) struct ModuleRow {
pub(crate) node: String,
pub(crate) aggregate: String,
pub(crate) kind: Option<String>,
pub(crate) visibility: Option<String>,
pub(crate) group_size: Option<usize>,
pub(crate) members_preview: Option<String>,
pub(crate) top_members_pr: Option<String>,
pub(crate) transitive_dependencies: usize,
pub(crate) transitive_dependents: usize,
pub(crate) in_degree: usize,
pub(crate) out_degree: usize,
pub(crate) pagerank: f64,
pub(crate) consumers_pagerank: f64,
pub(crate) betweenness: f64,
}
#[derive(Debug, Clone)]
struct CargoModulesNodeMeta {
kind: Option<String>,
visibility: Option<String>,
}
pub(crate) fn run_modules(args: &ModulesArgs) -> Result<()> {
let eff = apply_modules_preset(args);
let (rows, nodes, edges, aggregate_label, top_edges) = run_modules_core(&eff)?;
match eff.format {
OutputFormat::Json => {
#[derive(Serialize)]
struct ModulesJsonOut<'a> {
schema_version: u32,
ok: bool,
command: &'a str,
rows: Vec<ModuleRow>,
}
let out = ModulesJsonOut {
schema_version: 1,
ok: true,
command: "modules",
rows,
};
println!("{}", serde_json::to_string_pretty(&out)?);
}
OutputFormat::Text => {
print_modules_text(&eff, &rows, nodes, edges, &aggregate_label, &top_edges);
}
}
Ok(())
}
#[allow(clippy::type_complexity)]
pub(crate) fn run_modules_core(
args: &ModulesArgs,
) -> Result<(
Vec<ModuleRow>,
usize,
usize,
String,
Vec<(String, String, f64)>,
)> {
let mut cmd = ProcessCommand::new("cargo");
cmd.args(["modules", "dependencies"]);
cmd.args(["--manifest-path", &args.manifest_path.to_string_lossy()]);
let selected_pkg: Option<String> = args.package.clone();
if selected_pkg.is_none() {
if let Ok(raw) = fs::read_to_string(&args.manifest_path) {
if raw.contains("[workspace]") {
anyhow::bail!(
"cargo-modules requires an explicit package when analyzing a workspace manifest.\n\
Fix: pass `--package <crate>` (or point `--manifest-path` at the crate's Cargo.toml)."
);
}
}
}
if let Some(pkg) = &selected_pkg {
cmd.args(["-p", pkg]);
}
if args.lib {
cmd.arg("--lib");
}
if let Some(bin) = &args.bin {
cmd.args(["--bin", bin]);
}
if args.cfg_test {
cmd.arg("--cfg-test");
}
let no_externs = if args.include_externs {
false
} else {
args.no_externs
};
let no_sysroot = if args.include_sysroot {
false
} else {
args.no_sysroot
};
let no_fns = if args.include_fns { false } else { args.no_fns };
let no_traits = if args.include_traits {
false
} else {
args.no_traits
};
let no_types = if args.include_types {
false
} else {
args.no_types
};
if no_externs {
cmd.arg("--no-externs");
}
if no_sysroot {
cmd.arg("--no-sysroot");
}
if no_fns {
cmd.arg("--no-fns");
}
if no_traits {
cmd.arg("--no-traits");
}
if no_types {
cmd.arg("--no-types");
}
match args.edge_kind {
ModuleEdgeKind::Uses => {
cmd.arg("--no-owns");
}
ModuleEdgeKind::Owns => {
cmd.arg("--no-uses");
}
ModuleEdgeKind::Both => {}
}
let dot = cargo_modules_dot_cached(args, &cmd, selected_pkg.as_deref())?;
let (node_names, edges, node_meta) = parse_cargo_modules_dot(&dot, args.edge_kind);
let node_index_by_name: HashMap<String, usize> = node_names
.iter()
.enumerate()
.map(|(i, n)| (n.clone(), i))
.collect();
let mut g: DiGraph<String, f64> = DiGraph::new();
let mut idx: Vec<NodeIndex> = Vec::with_capacity(node_names.len());
for n in &node_names {
idx.push(g.add_node(n.clone()));
}
for e in edges {
let w = match (args.edge_kind, e.label.as_str()) {
(ModuleEdgeKind::Both, "uses") => args.uses_weight,
(ModuleEdgeKind::Both, "owns") => args.owns_weight,
(_, _) => 1.0,
};
if w > 0.0 {
g.update_edge(idx[e.u], idx[e.v], w);
}
}
let node_pr = pagerank_auto(&g);
let (g2, members_map, aggregate_label) = if matches!(args.aggregate, ModuleAggregate::Node) {
(g, None, "node".to_string())
} else if matches!(args.aggregate, ModuleAggregate::Module) {
let (ng, labels) = contract_graph(&g, owning_module);
(ng, Some(labels), "module".to_string())
} else {
let crate_name = selected_pkg.clone().unwrap_or_else(|| "crate".to_string());
let crate_dir =
resolve_package_dir(&args.manifest_path, &crate_name).unwrap_or_else(|| {
args.manifest_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."))
});
let root_file = infer_rust_crate_root_file(&crate_dir, args, &crate_name);
let (ng, labels) = contract_graph(&g, |name| {
let module = owning_module(name);
module_to_file_key(&crate_dir, &crate_name, &root_file, &module)
});
(ng, Some(labels), "file".to_string())
};
let mut edge_pairs: Vec<(usize, usize)> = Vec::new();
for e in g2.edge_references() {
edge_pairs.push((e.source().index(), e.target().index()));
}
let (transitive_dependents, transitive_dependencies) =
reachability_counts_edges(g2.node_count(), &edge_pairs);
let pr = pagerank_auto(&g2);
let consumers_pr = pagerank_auto(&reverse_graph(&g2));
let bc = betweenness_centrality(&g2);
let mut rows: Vec<ModuleRow> = g2
.node_indices()
.map(|n| {
let in_degree = g2.neighbors_directed(n, Direction::Incoming).count();
let out_degree = g2.neighbors_directed(n, Direction::Outgoing).count();
let node = g2.nw(n).clone();
let (kind, visibility, group_size, members_preview, top_members_pr) =
match (&args.aggregate, &members_map) {
(ModuleAggregate::Node, _) => {
let meta = node_meta.get(&node);
(
meta.and_then(|m| m.kind.clone()),
meta.and_then(|m| m.visibility.clone()),
None,
None,
None,
)
}
(_, Some(map)) => {
let members = map.get(&node).cloned().unwrap_or_default();
let group_size = members.len();
let mut preview = String::new();
for (i, m) in members.iter().take(3).enumerate() {
if i > 0 {
preview.push_str(", ");
}
preview.push_str(m);
}
if group_size > 3 {
preview.push_str(&format!(", …(+{})", group_size - 3));
}
let mut scored: Vec<(&str, f64)> = members
.iter()
.filter_map(|m| {
node_index_by_name.get(m).map(|&i| (m.as_str(), node_pr[i]))
})
.collect();
scored.sort_by(|a, b| b.1.total_cmp(&a.1));
let mut tops = String::new();
for (i, (m, s)) in scored.into_iter().take(args.members_top).enumerate() {
if i > 0 {
tops.push_str(", ");
}
tops.push_str(&format!("{}({:.4})", m, s));
}
let top_members_pr = if tops.is_empty() { None } else { Some(tops) };
(None, None, Some(group_size), Some(preview), top_members_pr)
}
_ => (None, None, None, None, None),
};
ModuleRow {
node,
aggregate: aggregate_label.clone(),
kind,
visibility,
group_size,
members_preview,
top_members_pr,
transitive_dependencies: transitive_dependencies[n.index()],
transitive_dependents: transitive_dependents[n.index()],
in_degree,
out_degree,
pagerank: pr[n.index()],
consumers_pagerank: consumers_pr[n.index()],
betweenness: bc[n.index()],
}
})
.collect();
rows.sort_by(|a, b| b.pagerank.total_cmp(&a.pagerank));
let mut top_edges: Vec<(String, String, f64)> = Vec::new();
if args.edges_top > 0 {
for e in g2.edge_references() {
let u = g2.nw(e.source()).clone();
let v = g2.nw(e.target()).clone();
let w = (*e.weight()).max(0.0);
top_edges.push((u, v, w));
}
top_edges.sort_by(|a, b| b.2.total_cmp(&a.2));
top_edges.truncate(args.edges_top);
}
Ok((
rows,
g2.node_count(),
g2.edge_count(),
aggregate_label,
top_edges,
))
}
pub(crate) fn apply_modules_preset(args: &ModulesArgs) -> ModulesArgs {
let mut out = args.clone();
let p = args.preset;
let mut set_if = |cond: bool, f: &mut dyn FnMut(&mut ModulesArgs)| {
if cond {
f(&mut out);
}
};
match p {
ModulesPreset::None => return out,
ModulesPreset::FileFull => {
set_if(!matches!(args.aggregate, ModuleAggregate::File), &mut |a| {
a.aggregate = ModuleAggregate::File
});
set_if(matches!(args.edge_kind, ModuleEdgeKind::Uses), &mut |_a| {});
set_if(!args.include_fns, &mut |a| a.include_fns = true);
set_if(!args.include_types, &mut |a| a.include_types = true);
set_if(!args.include_traits, &mut |a| a.include_traits = true);
set_if(args.edges_top == 0, &mut |a| a.edges_top = 5);
set_if(args.members_top == 3, &mut |_a| {});
}
ModulesPreset::FileApi => {
set_if(!matches!(args.aggregate, ModuleAggregate::File), &mut |a| {
a.aggregate = ModuleAggregate::File
});
set_if(!args.include_types, &mut |a| a.include_types = true);
set_if(!args.include_traits, &mut |a| a.include_traits = true);
set_if(args.edges_top == 0, &mut |a| a.edges_top = 3);
set_if(args.members_top == 3, &mut |a| a.members_top = 2);
}
ModulesPreset::NodeFull => {
set_if(!matches!(args.aggregate, ModuleAggregate::Node), &mut |a| {
a.aggregate = ModuleAggregate::Node
});
set_if(!args.include_fns, &mut |a| a.include_fns = true);
set_if(!args.include_types, &mut |a| a.include_types = true);
set_if(!args.include_traits, &mut |a| a.include_traits = true);
set_if(args.edge_kind == ModuleEdgeKind::Uses, &mut |a| {
a.edge_kind = ModuleEdgeKind::Both
});
set_if(args.edges_top == 0, &mut |a| a.edges_top = 8);
}
ModulesPreset::NodeApi => {
set_if(!matches!(args.aggregate, ModuleAggregate::Node), &mut |a| {
a.aggregate = ModuleAggregate::Node
});
set_if(!args.include_types, &mut |a| a.include_types = true);
set_if(!args.include_traits, &mut |a| a.include_traits = true);
set_if(args.edges_top == 0, &mut |a| a.edges_top = 5);
}
}
out
}
#[derive(Debug, Serialize)]
struct ModulesCacheMeta {
generated_at_unix: i64,
pkg: Option<String>,
manifest_path: String,
target: String,
cmd: String,
key: String,
}
fn cargo_modules_dot_cached(
args: &ModulesArgs,
cmd: &ProcessCommand,
pkg: Option<&str>,
) -> Result<String> {
let workspace_root = args
.manifest_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let cache_root = workspace_root.join("evals/pkgrank/modules_cache");
fs::create_dir_all(&cache_root).ok();
if !args.cache {
return run_cargo_modules_dot(cmd);
}
let target = if args.lib {
"lib".to_string()
} else if let Some(bin) = &args.bin {
format!("bin={bin}")
} else {
"default".to_string()
};
let key_material = format!(
"pkg={:?}\nmanifest={}\ntarget={}\nedge={:?}\nagg={:?}\nweights={:.3}/{:.3}\nfilters=no_externs:{} no_sysroot:{} no_fns:{} no_types:{} no_traits:{}\ninclude=externs:{} sysroot:{} fns:{} types:{} traits:{}\ncmd={:?}",
pkg,
args.manifest_path.display(),
target,
args.edge_kind,
args.aggregate,
args.uses_weight,
args.owns_weight,
args.no_externs,
args.no_sysroot,
args.no_fns,
args.no_types,
args.no_traits,
args.include_externs,
args.include_sysroot,
args.include_fns,
args.include_types,
args.include_traits,
cmd
);
let key = format!("{:016x}", fnv1a64(key_material.as_bytes()));
let dot_path = cache_root.join(format!("modules_{key}.dot"));
let meta_path = cache_root.join(format!("modules_{key}.json"));
if !args.cache_refresh && dot_path.exists() {
return fs::read_to_string(&dot_path).map_err(Into::into);
}
let dot = run_cargo_modules_dot(cmd)?;
fs::write(&dot_path, &dot).ok();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let meta = ModulesCacheMeta {
generated_at_unix: now,
pkg: pkg.map(|s| s.to_string()),
manifest_path: args.manifest_path.display().to_string(),
target,
cmd: format!("{:?}", cmd),
key,
};
fs::write(
&meta_path,
serde_json::to_string_pretty(&meta).unwrap_or_default(),
)
.ok();
Ok(dot)
}
fn run_cargo_modules_dot(cmd: &ProcessCommand) -> Result<String> {
let program = cmd.get_program().to_string_lossy().to_string();
let mut cmd2 = ProcessCommand::new(program);
cmd2.args(cmd.get_args());
cmd2.envs(cmd.get_envs().filter_map(|(k, v)| v.map(|vv| (k, vv))));
cmd2.current_dir(
cmd.get_current_dir()
.unwrap_or_else(|| std::path::Path::new(".")),
);
let out = cmd2
.output()
.with_context(|| format!("failed to spawn: {:?}", cmd))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(anyhow!(
"cargo modules dependencies failed (exit={:?}): {}",
out.status.code(),
stderr.trim()
));
}
Ok(String::from_utf8_lossy(&out.stdout).to_string())
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
fn print_modules_text(
args: &ModulesArgs,
rows: &[ModuleRow],
nodes: usize,
edges: usize,
aggregate_label: &str,
top_edges: &[(String, String, f64)],
) {
let mut sorted: Vec<&ModuleRow> = rows.iter().collect();
sorted.sort_by(|a, b| match args.metric {
Metric::Pagerank => b.pagerank.total_cmp(&a.pagerank),
Metric::ConsumersPagerank => b.consumers_pagerank.total_cmp(&a.consumers_pagerank),
Metric::Indegree => b.in_degree.cmp(&a.in_degree),
Metric::Outdegree => b.out_degree.cmp(&a.out_degree),
Metric::Betweenness => b.betweenness.total_cmp(&a.betweenness),
});
let target = if args.lib {
"lib".to_string()
} else if let Some(bin) = &args.bin {
format!("bin={bin}")
} else {
"default".to_string()
};
println!("pkgrank modules");
println!(" manifest: {}", args.manifest_path.display());
if let Some(p) = &args.package {
println!(" package: {}", p);
}
println!(" target: {}", target);
if !matches!(args.preset, ModulesPreset::None) {
println!(" preset: {:?}", args.preset);
}
println!(" edges: {:?} (from cargo-modules)", args.edge_kind);
println!(" aggregate:{}", aggregate_label);
println!(
" include: fns={} types={} traits={} externs={} sysroot={} cache={} refresh={}\n",
args.include_fns,
args.include_types,
args.include_traits,
args.include_externs,
args.include_sysroot,
args.cache,
args.cache_refresh
);
println!(
"{:>4} {:>10} {:>10} {:>9} {:>6} {:>6} {:>3} {:>3} {:<10} {:<8} node",
"rank", "pr", "cons_pr", "between", "depsT", "consT", "in", "out", "kind", "vis"
);
println!("{:─<110}", "");
for (i, r) in sorted.into_iter().take(args.top).enumerate() {
let kind = r.kind.as_deref().unwrap_or("-");
let vis = r.visibility.as_deref().unwrap_or("-");
let mut node = r.node.clone();
if let Some(gs) = r.group_size {
node.push_str(&format!(" [n={}]", gs));
}
if let Some(prev) = &r.members_preview {
node.push_str(&format!(" members: {}", prev));
}
if let Some(tops) = &r.top_members_pr {
node.push_str(&format!(" top_pr: {}", tops));
}
println!(
"{:>4}. {:>10.6} {:>10.6} {:>9.6} {:>6} {:>6} {:>3} {:>3} {:<10} {:<8} {}",
i + 1,
r.pagerank,
r.consumers_pagerank,
r.betweenness,
r.transitive_dependencies,
r.transitive_dependents,
r.in_degree,
r.out_degree,
kind,
vis,
node
);
}
println!(
"\n{} nodes, {} edges\nEdge semantics: A -> B means A {} B",
nodes,
edges,
match args.edge_kind {
ModuleEdgeKind::Uses => "uses",
ModuleEdgeKind::Owns => "owns",
ModuleEdgeKind::Both => "relates to",
}
);
if args.edges_top > 0 {
println!("\nTop {} edges (by weight):", args.edges_top);
println!("{:─<110}", "");
for (i, (u, v, w)) in top_edges.iter().take(args.edges_top).enumerate() {
println!("{:>4}. w={:>7.3} {} -> {}", i + 1, w, u, v);
}
}
}
fn owning_module(node: &str) -> String {
node.rsplit_once("::")
.map(|(p, _)| p.to_string())
.unwrap_or_else(|| node.to_string())
}
fn contract_graph<F>(
g: &DiGraph<String, f64>,
key_fn: F,
) -> (DiGraph<String, f64>, HashMap<String, Vec<String>>)
where
F: Fn(&str) -> String,
{
let mut members: HashMap<String, Vec<String>> = HashMap::new();
for n in g.node_indices() {
let name = g.nw(n);
let key = key_fn(name);
members.entry(key).or_default().push(name.clone());
}
let mut keys: Vec<String> = members.keys().cloned().collect();
keys.sort();
let mut groups: HashMap<String, usize> = HashMap::new();
for (i, k) in keys.iter().enumerate() {
groups.insert(k.clone(), i);
}
let mut ng: DiGraph<String, f64> = DiGraph::new();
let mut idx: Vec<NodeIndex> = vec![NodeIndex::new(0); groups.len()];
for (i, k) in keys.iter().enumerate() {
idx[i] = ng.add_node(k.clone());
}
for e in g.edge_references() {
let u = e.source().index();
let v = e.target().index();
let from = g.nw(NodeIndex::new(u));
let to = g.nw(NodeIndex::new(v));
let gu = groups[&key_fn(from)];
let gv = groups[&key_fn(to)];
if gu == gv {
continue;
}
let w = (*e.weight()).max(0.0);
let cur = ng
.find_edge(idx[gu], idx[gv])
.and_then(|ei| ng.edge_weight(ei).copied())
.unwrap_or(0.0);
ng.update_edge(idx[gu], idx[gv], cur + w);
}
for v in members.values_mut() {
v.sort();
}
(ng, members)
}
fn infer_rust_crate_root_file(crate_dir: &Path, args: &ModulesArgs, _crate_name: &str) -> PathBuf {
let src = crate_dir.join("src");
if let Some(bin) = &args.bin {
let candidate = src.join("bin").join(format!("{bin}.rs"));
if candidate.exists() {
return candidate;
}
let candidate = src.join("main.rs");
if candidate.exists() {
return candidate;
}
}
if args.lib {
let candidate = src.join("lib.rs");
if candidate.exists() {
return candidate;
}
}
let lib = src.join("lib.rs");
if lib.exists() {
return lib;
}
src.join("main.rs")
}
fn module_to_file_key(
crate_dir: &Path,
crate_name: &str,
root_file: &Path,
module: &str,
) -> String {
let src = crate_dir.join("src");
if module == crate_name {
return rel_path_display(crate_dir, root_file);
}
let rel = module
.strip_prefix(&format!("{crate_name}::"))
.unwrap_or(module);
let segs: Vec<&str> = rel.split("::").filter(|s| !s.is_empty()).collect();
if segs.is_empty() {
return rel_path_display(crate_dir, root_file);
}
let mut cur = segs.as_slice();
while !cur.is_empty() {
let as_file = src.join(cur.join("/")).with_extension("rs");
if as_file.exists() {
return rel_path_display(crate_dir, &as_file);
}
let as_mod = src.join(cur.join("/")).join("mod.rs");
if as_mod.exists() {
return rel_path_display(crate_dir, &as_mod);
}
cur = &cur[..cur.len() - 1];
}
if root_file.exists() {
return rel_path_display(crate_dir, root_file);
}
"<unknown>".to_string()
}
fn rel_path_display(base: &Path, p: &Path) -> String {
match p.strip_prefix(base) {
Ok(rp) => rp.display().to_string(),
Err(_) => p.display().to_string(),
}
}
fn resolve_package_dir(workspace_manifest: &Path, package_name: &str) -> Option<PathBuf> {
let mut cmd = MetadataCommand::new();
cmd.manifest_path(workspace_manifest);
let md = cmd.exec().ok()?;
let pkg = md
.packages
.iter()
.find(|p| p.name.as_str() == package_name)?;
let mp = PathBuf::from(pkg.manifest_path.to_string());
mp.parent().map(|p| p.to_path_buf())
}
#[derive(Debug, Clone)]
struct ParsedEdge {
u: usize,
v: usize,
label: String,
}
fn parse_cargo_modules_dot(
dot: &str,
edge_kind: ModuleEdgeKind,
) -> (
Vec<String>,
Vec<ParsedEdge>,
HashMap<String, CargoModulesNodeMeta>,
) {
let want_uses = matches!(edge_kind, ModuleEdgeKind::Uses | ModuleEdgeKind::Both);
let want_owns = matches!(edge_kind, ModuleEdgeKind::Owns | ModuleEdgeKind::Both);
let mut nodes: HashMap<String, usize> = HashMap::new();
let mut edges: Vec<ParsedEdge> = Vec::new();
let mut meta: HashMap<String, CargoModulesNodeMeta> = HashMap::new();
let intern = |s: &str, nodes: &mut HashMap<String, usize>| -> usize {
if let Some(&i) = nodes.get(s) {
return i;
}
let i = nodes.len();
nodes.insert(s.to_string(), i);
i
};
for line in dot.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with('"')
&& line.contains(" [label=\"")
&& line.contains("];")
&& !line.contains("->")
{
if let Some((id, m)) = parse_cargo_modules_node_line(line) {
meta.insert(id.clone(), m);
let _ = intern(&id, &mut nodes);
}
continue;
}
if !line.contains("->") || !line.contains("label=") {
continue;
}
let Some(s1) = line.find('"') else { continue };
let rest = &line[s1 + 1..];
let Some(e1) = rest.find('"') else { continue };
let source = &rest[..e1];
let after_src = &rest[e1 + 1..];
let Some(arrow) = after_src.find("->") else {
continue;
};
let after_arrow = &after_src[arrow + 2..];
let Some(s2q) = after_arrow.find('"') else {
continue;
};
let rest2 = &after_arrow[s2q + 1..];
let Some(e2) = rest2.find('"') else { continue };
let target = &rest2[..e2];
let Some(li) = line.find("label=\"") else {
continue;
};
let restl = &line[li + "label=\"".len()..];
let Some(le) = restl.find('"') else { continue };
let label = &restl[..le];
let include = match label {
"uses" => want_uses,
"owns" => want_owns,
_ => false,
};
if !include {
continue;
}
let u = intern(source, &mut nodes);
let v = intern(target, &mut nodes);
edges.push(ParsedEdge {
u,
v,
label: label.to_string(),
});
}
let mut node_names = vec![String::new(); nodes.len()];
for (name, i) in nodes {
node_names[i] = name;
}
(node_names, edges, meta)
}
fn parse_cargo_modules_node_line(line: &str) -> Option<(String, CargoModulesNodeMeta)> {
let s1 = line.find('"')?;
let rest = &line[s1 + 1..];
let e1 = rest.find('"')?;
let id = rest[..e1].to_string();
let li = line.find("label=\"")?;
let restl = &line[li + "label=\"".len()..];
let le = restl.find('"')?;
let label = &restl[..le];
let (header, _body) = label.split_once('|').unwrap_or((label, ""));
let header = header.trim();
if header.is_empty() {
return Some((
id,
CargoModulesNodeMeta {
kind: None,
visibility: None,
},
));
}
let parts: Vec<&str> = header.split_whitespace().collect();
if parts.is_empty() {
return Some((
id,
CargoModulesNodeMeta {
kind: None,
visibility: None,
},
));
}
let kind = parts.last().map(|s| s.to_string());
let visibility = if parts.len() >= 2 {
Some(parts[..parts.len() - 1].join(" "))
} else {
None
};
Some((id, CargoModulesNodeMeta { kind, visibility }))
}