From 2b46656d3eab7286e60442f4e69351d009ac5737 Mon Sep 17 00:00:00 2001 From: Joseph O'Brien <98370624+89jobrien@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:26:53 -0400 Subject: [PATCH] feat(notgraph): implement module_graph collector --- crates/notgraph/src/lib.rs | 1 + crates/notgraph/src/module_graph.rs | 105 ++++++++++++++++++ .../tests/fixtures/clean/src/alpha.rs | 1 + .../notgraph/tests/fixtures/clean/src/beta.rs | 1 + .../notgraph/tests/fixtures/clean/src/lib.rs | 2 + 5 files changed, 110 insertions(+) create mode 100644 crates/notgraph/src/module_graph.rs create mode 100644 crates/notgraph/tests/fixtures/clean/src/alpha.rs create mode 100644 crates/notgraph/tests/fixtures/clean/src/beta.rs create mode 100644 crates/notgraph/tests/fixtures/clean/src/lib.rs diff --git a/crates/notgraph/src/lib.rs b/crates/notgraph/src/lib.rs index 02a3c82..b82ac0f 100644 --- a/crates/notgraph/src/lib.rs +++ b/crates/notgraph/src/lib.rs @@ -1,2 +1,3 @@ pub mod crate_graph; +pub mod module_graph; pub mod types; diff --git a/crates/notgraph/src/module_graph.rs b/crates/notgraph/src/module_graph.rs new file mode 100644 index 0000000..ca184c1 --- /dev/null +++ b/crates/notgraph/src/module_graph.rs @@ -0,0 +1,105 @@ +use crate::types::{CrateName, ModPath, ModuleGraph}; +use anyhow::Result; +use std::path::Path; +use syn::visit::Visit; +use walkdir::WalkDir; + +struct ModCollector { + current_mod: ModPath, + edges: Vec<(ModPath, ModPath)>, + nodes: Vec, +} + +impl ModCollector { + fn new(root_mod: ModPath) -> Self { + Self { + current_mod: root_mod.clone(), + edges: Vec::new(), + nodes: vec![root_mod], + } + } +} + +impl<'ast> Visit<'ast> for ModCollector { + fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) { + let child = format!("{}::{}", self.current_mod, node.ident); + if !self.nodes.contains(&child) { + self.nodes.push(child.clone()); + } + self.edges.push((self.current_mod.clone(), child.clone())); + let parent = std::mem::replace(&mut self.current_mod, child); + syn::visit::visit_item_mod(self, node); + self.current_mod = parent; + } +} + +fn path_to_mod(krate: &str, rel: &Path) -> ModPath { + let mut parts: Vec = vec![krate.to_string()]; + for component in rel.components() { + let s = component.as_os_str().to_string_lossy(); + if s == "lib.rs" || s == "main.rs" { + break; + } + let s = s.trim_end_matches(".rs").to_string(); + if s != "mod" { + parts.push(s); + } + } + parts.join("::") +} + +pub fn build(krate: CrateName, src_dir: &Path) -> Result { + let mut all_nodes: Vec = Vec::new(); + let mut all_edges: Vec<(ModPath, ModPath)> = Vec::new(); + + 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")) + { + let path = entry.path(); + let content = std::fs::read_to_string(path)?; + let file = syn::parse_file(&content)?; + let rel = path.strip_prefix(src_dir)?; + let mod_path = path_to_mod(&krate, rel); + + let mut collector = ModCollector::new(mod_path); + collector.visit_file(&file); + + for node in collector.nodes { + if !all_nodes.contains(&node) { + all_nodes.push(node); + } + } + all_edges.extend(collector.edges); + } + + all_edges.sort(); + all_edges.dedup(); + + Ok(ModuleGraph { krate, nodes: all_nodes, edges: all_edges }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clean_fixture_has_expected_nodes() { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/clean/src"); + let graph = build("clean".to_string(), &fixture).unwrap(); + assert!(graph.nodes.contains(&"clean".to_string())); + assert!(graph.nodes.contains(&"clean::alpha".to_string())); + assert!(graph.nodes.contains(&"clean::beta".to_string())); + } + + #[test] + fn clean_fixture_has_edges_from_lib() { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/clean/src"); + let graph = build("clean".to_string(), &fixture).unwrap(); + assert!(graph.edges.contains(&("clean".to_string(), "clean::alpha".to_string()))); + assert!(graph.edges.contains(&("clean".to_string(), "clean::beta".to_string()))); + } +} diff --git a/crates/notgraph/tests/fixtures/clean/src/alpha.rs b/crates/notgraph/tests/fixtures/clean/src/alpha.rs new file mode 100644 index 0000000..c340dba --- /dev/null +++ b/crates/notgraph/tests/fixtures/clean/src/alpha.rs @@ -0,0 +1 @@ +pub fn hello() -> &'static str { "alpha" } diff --git a/crates/notgraph/tests/fixtures/clean/src/beta.rs b/crates/notgraph/tests/fixtures/clean/src/beta.rs new file mode 100644 index 0000000..0930fde --- /dev/null +++ b/crates/notgraph/tests/fixtures/clean/src/beta.rs @@ -0,0 +1 @@ +pub fn greet() -> &'static str { "beta" } diff --git a/crates/notgraph/tests/fixtures/clean/src/lib.rs b/crates/notgraph/tests/fixtures/clean/src/lib.rs new file mode 100644 index 0000000..cfa8271 --- /dev/null +++ b/crates/notgraph/tests/fixtures/clean/src/lib.rs @@ -0,0 +1,2 @@ +pub mod alpha; +pub mod beta;