diff --git a/crates/notgraph/src/main.rs b/crates/notgraph/src/main.rs index 7f7732e..4be8a6a 100644 --- a/crates/notgraph/src/main.rs +++ b/crates/notgraph/src/main.rs @@ -1,3 +1,90 @@ -fn main() { - println!("notgraph"); +use notgraph_lib::{analysis, crate_graph, emit, module_graph, symbols}; +use anyhow::{Context, Result}; +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "notgraph", about = "Workspace module graph and symbol analysis")] +struct Cli { + #[arg(long, default_value = "docs/graph")] + output: PathBuf, + + #[arg(long)] + fail_on_cycles: bool, + + #[arg(long, default_value_t = 10)] + top: usize, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + let manifest = find_workspace_manifest()?; + let workspace_root = manifest.parent().unwrap().to_path_buf(); + + let crate_g = crate_graph::build(&manifest) + .context("failed to build crate graph")?; + + let meta = cargo_metadata::MetadataCommand::new() + .manifest_path(&manifest) + .no_deps() + .exec()?; + + let mut module_graphs = Vec::new(); + let mut symbol_tables = Vec::new(); + + for pkg in &meta.packages { + if !meta.workspace_members.contains(&pkg.id) { + continue; + } + let src_dir = PathBuf::from(pkg.manifest_path.as_str()) + .parent().unwrap().join("src"); + if !src_dir.exists() { + continue; + } + module_graphs.push( + module_graph::build(pkg.name.clone(), &src_dir) + .with_context(|| format!("module_graph failed for {}", pkg.name))? + ); + symbol_tables.push( + symbols::build(pkg.name.clone(), &src_dir) + .with_context(|| format!("symbols failed for {}", pkg.name))? + ); + } + + let stats = analysis::analyse(&crate_g, &module_graphs, cli.top); + + let output_dir = if cli.output.is_absolute() { + cli.output.clone() + } else { + workspace_root.join(&cli.output) + }; + + emit::write_all(&output_dir, &stats, &symbol_tables) + .context("failed to write reports")?; + + println!("notgraph: reports written to {}", output_dir.display()); + + if cli.fail_on_cycles && !stats.cycles.is_empty() { + eprintln!("notgraph: {} cycle(s) detected", stats.cycles.len()); + for cycle in &stats.cycles { + eprintln!(" {}", cycle.join(" -> ")); + } + std::process::exit(1); + } + + Ok(()) +} + +fn find_workspace_manifest() -> Result { + let mut dir = std::env::current_dir()?; + loop { + let candidate = dir.join("Cargo.toml"); + if candidate.exists() { + let content = std::fs::read_to_string(&candidate)?; + if content.contains("[workspace]") { + return Ok(candidate); + } + } + anyhow::ensure!(dir.pop(), "could not find workspace Cargo.toml"); + } } diff --git a/crates/notgraph/src/module_graph.rs b/crates/notgraph/src/module_graph.rs index ca184c1..0d2c857 100644 --- a/crates/notgraph/src/module_graph.rs +++ b/crates/notgraph/src/module_graph.rs @@ -55,7 +55,7 @@ pub fn build(krate: CrateName, src_dir: &Path) -> Result { for entry in WalkDir::new(src_dir) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map_or(false, |x| x == "rs")) + .filter(|e| e.path().extension().is_some_and(|x| x == "rs")) { let path = entry.path(); let content = std::fs::read_to_string(path)?; diff --git a/crates/notgraph/src/symbols.rs b/crates/notgraph/src/symbols.rs index eb56472..7e6a833 100644 --- a/crates/notgraph/src/symbols.rs +++ b/crates/notgraph/src/symbols.rs @@ -64,7 +64,7 @@ pub fn build(krate: CrateName, src_dir: &Path) -> Result { for entry in WalkDir::new(src_dir) .into_iter() .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map_or(false, |x| x == "rs")) + .filter(|e| e.path().extension().is_some_and(|x| x == "rs")) { let path = entry.path(); let content = std::fs::read_to_string(path)?;