diff --git a/crates/notgraph/src/lib.rs b/crates/notgraph/src/lib.rs index b82ac0f..291f752 100644 --- a/crates/notgraph/src/lib.rs +++ b/crates/notgraph/src/lib.rs @@ -1,3 +1,4 @@ pub mod crate_graph; pub mod module_graph; +pub mod symbols; pub mod types; diff --git a/crates/notgraph/src/symbols.rs b/crates/notgraph/src/symbols.rs new file mode 100644 index 0000000..eb56472 --- /dev/null +++ b/crates/notgraph/src/symbols.rs @@ -0,0 +1,133 @@ +use crate::types::{CrateName, ModPath, Symbol, SymbolKind, SymbolTable}; +use anyhow::Result; +use std::path::Path; +use syn::{Visibility, visit::Visit}; +use walkdir::WalkDir; + +struct SymbolCollector { + mod_path: ModPath, + symbols: Vec, +} + +impl SymbolCollector { + fn new(mod_path: ModPath) -> Self { + Self { mod_path, symbols: Vec::new() } + } + + fn is_pub(vis: &Visibility) -> bool { + matches!(vis, Visibility::Public(_)) + } + + fn push(&mut self, kind: SymbolKind, name: String, is_pub: bool) { + self.symbols.push(Symbol { mod_path: self.mod_path.clone(), kind, name, is_pub }); + } +} + +impl<'ast> Visit<'ast> for SymbolCollector { + fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) { + self.push(SymbolKind::Struct, node.ident.to_string(), Self::is_pub(&node.vis)); + } + fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) { + self.push(SymbolKind::Enum, node.ident.to_string(), Self::is_pub(&node.vis)); + } + fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) { + self.push(SymbolKind::Trait, node.ident.to_string(), Self::is_pub(&node.vis)); + } + fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) { + self.push(SymbolKind::Fn, node.sig.ident.to_string(), Self::is_pub(&node.vis)); + } + fn visit_item_type(&mut self, node: &'ast syn::ItemType) { + self.push(SymbolKind::Type, node.ident.to_string(), Self::is_pub(&node.vis)); + } + fn visit_item_const(&mut self, node: &'ast syn::ItemConst) { + self.push(SymbolKind::Const, node.ident.to_string(), Self::is_pub(&node.vis)); + } +} + +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 symbols: Vec = 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 = SymbolCollector::new(mod_path); + collector.visit_file(&file); + symbols.extend(collector.symbols); + } + Ok(SymbolTable { krate, symbols }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture_table() -> SymbolTable { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/symbols/src"); + build("symbols".to_string(), &fixture).unwrap() + } + + #[test] + fn detects_pub_struct() { + let t = fixture_table(); + assert!(t.symbols.iter().any(|s| s.name == "Foo" && s.kind == SymbolKind::Struct && s.is_pub)); + } + + #[test] + fn detects_pub_enum() { + let t = fixture_table(); + assert!(t.symbols.iter().any(|s| s.name == "Bar" && s.kind == SymbolKind::Enum && s.is_pub)); + } + + #[test] + fn detects_pub_trait() { + let t = fixture_table(); + assert!(t.symbols.iter().any(|s| s.name == "Baz" && s.kind == SymbolKind::Trait && s.is_pub)); + } + + #[test] + fn detects_pub_fn() { + let t = fixture_table(); + assert!(t.symbols.iter().any(|s| s.name == "qux" && s.kind == SymbolKind::Fn && s.is_pub)); + } + + #[test] + fn detects_pub_type_alias() { + let t = fixture_table(); + assert!(t.symbols.iter().any(|s| s.name == "Alias" && s.kind == SymbolKind::Type && s.is_pub)); + } + + #[test] + fn detects_pub_const() { + let t = fixture_table(); + assert!(t.symbols.iter().any(|s| s.name == "VALUE" && s.kind == SymbolKind::Const && s.is_pub)); + } + + #[test] + fn private_struct_is_not_pub() { + let t = fixture_table(); + let p = t.symbols.iter().find(|s| s.name == "Private").unwrap(); + assert!(!p.is_pub); + } +} diff --git a/crates/notgraph/tests/fixtures/symbols/src/lib.rs b/crates/notgraph/tests/fixtures/symbols/src/lib.rs new file mode 100644 index 0000000..3050321 --- /dev/null +++ b/crates/notgraph/tests/fixtures/symbols/src/lib.rs @@ -0,0 +1,7 @@ +pub struct Foo; +pub enum Bar { A, B } +pub trait Baz { fn run(&self); } +pub fn qux() {} +pub type Alias = u32; +pub const VALUE: u32 = 42; +struct Private;