use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::resolve::{ModuleImports, collect_module_imports};
use crate::types::TypePath;
#[derive(Debug)]
pub enum ScanError {
Io {
path: PathBuf,
message: String,
},
Parse {
path: PathBuf,
message: String,
},
}
impl std::fmt::Display for ScanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, message } => write!(f, "I/O error reading `{}`: {message}", path.display()),
Self::Parse { path, message } => write!(f, "syn parse error in `{}`: {message}", path.display()),
}
}
}
impl std::error::Error for ScanError {}
pub fn scan_src_dir(src_dir: &Path) -> Result<BTreeMap<TypePath, syn::Item>, ScanError> {
scan_src_dir_with_imports(src_dir).map(|(pool, _imports)| pool)
}
pub fn scan_src_dir_with_imports(src_dir: &Path) -> Result<(BTreeMap<TypePath, syn::Item>, ModuleImports), ScanError> {
let mut pool = BTreeMap::new();
let mut imports = ModuleImports::default();
scan_dir_recursive(src_dir, &[], &mut pool, &mut imports)?;
Ok((pool, imports))
}
fn scan_dir_recursive(
dir: &Path,
module_prefix: &[String],
pool: &mut BTreeMap<TypePath, syn::Item>,
imports: &mut ModuleImports,
) -> Result<(), ScanError> {
let entries =
std::fs::read_dir(dir).map_err(|e| ScanError::Io { path: dir.to_path_buf(), message: e.to_string() })?;
let mut sorted: Vec<_> = entries.filter_map(|e| e.ok()).map(|e| e.path()).collect();
sorted.sort();
for path in sorted {
let file_name = match path.file_name().and_then(|s| s.to_str()) {
Some(name) => name.to_string(),
None => continue,
};
if path.is_dir() {
let mut next_prefix = module_prefix.to_vec();
next_prefix.push(file_name);
scan_dir_recursive(&path, &next_prefix, pool, imports)?;
continue;
}
if !file_name.ends_with(".rs") {
continue;
}
if file_name == "build.rs" {
continue;
}
let file_prefix: Vec<String> = if matches!(file_name.as_str(), "lib.rs" | "main.rs" | "mod.rs") {
module_prefix.to_vec()
} else {
let stem = file_name.trim_end_matches(".rs");
let mut p = module_prefix.to_vec();
p.push(stem.to_string());
p
};
let src =
std::fs::read_to_string(&path).map_err(|e| ScanError::Io { path: path.clone(), message: e.to_string() })?;
let parsed: syn::File =
syn::parse_file(&src).map_err(|e| ScanError::Parse { path: path.clone(), message: e.to_string() })?;
collect_items(&parsed.items, &file_prefix, pool);
collect_module_imports(&parsed, &file_prefix, imports);
}
Ok(())
}
fn collect_items(items: &[syn::Item], module_prefix: &[String], pool: &mut BTreeMap<TypePath, syn::Item>) {
for item in items {
match item {
syn::Item::Struct(s) => insert(pool, module_prefix, &s.ident, item.clone()),
syn::Item::Enum(e) => insert(pool, module_prefix, &e.ident, item.clone()),
syn::Item::Type(t) => insert(pool, module_prefix, &t.ident, item.clone()),
syn::Item::Mod(m) => {
if let Some((_, inner_items)) = &m.content {
let mut sub_prefix = module_prefix.to_vec();
sub_prefix.push(m.ident.to_string());
collect_items(inner_items, &sub_prefix, pool);
}
}
_ => {} }
}
}
fn insert(pool: &mut BTreeMap<TypePath, syn::Item>, prefix: &[String], ident: &syn::Ident, item: syn::Item) {
let mut segments = prefix.to_vec();
segments.push(ident.to_string());
if let Ok(path) = TypePath::new(segments) {
pool.insert(path, item);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn make_tempdir(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
for (rel, content) in files {
let abs = dir.path().join(rel);
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent).expect("create parent");
}
fs::write(&abs, content).expect("write file");
}
dir
}
fn tp(segments: &[&str]) -> TypePath {
TypePath::new(segments.iter().map(|s| (*s).to_string()).collect()).expect("non-empty")
}
#[test]
fn scans_lib_rs_top_level_struct() {
let dir = make_tempdir(&[("lib.rs", "pub struct Foo { pub bar: u32 }")]);
let pool = scan_src_dir(dir.path()).unwrap();
assert_eq!(pool.len(), 1);
assert!(pool.contains_key(&tp(&["Foo"])));
match pool.get(&tp(&["Foo"])).unwrap() {
syn::Item::Struct(s) => assert_eq!(s.ident.to_string(), "Foo"),
other => panic!("expected ItemStruct, got {other:?}"),
}
}
#[test]
fn scans_module_file_paths() {
let dir = make_tempdir(&[("lib.rs", ""), ("models.rs", "pub struct Workout { pub id: u64 }")]);
let pool = scan_src_dir(dir.path()).unwrap();
assert!(pool.contains_key(&tp(&["models", "Workout"])));
}
#[test]
fn scans_nested_directory_paths() {
let dir = make_tempdir(&[
("lib.rs", "pub mod outer;"),
("outer/mod.rs", "pub mod inner;"),
("outer/inner.rs", "pub enum Status { Live, Dead }"),
]);
let pool = scan_src_dir(dir.path()).unwrap();
assert!(
pool.contains_key(&tp(&["outer", "inner", "Status"])),
"pool keys: {:?}",
pool.keys().collect::<Vec<_>>()
);
}
#[test]
fn collects_all_three_item_kinds() {
let dir = make_tempdir(&[(
"lib.rs",
r#"
pub struct S { pub x: u32 }
pub enum E { A, B }
pub type T = u32;
"#,
)]);
let pool = scan_src_dir(dir.path()).unwrap();
assert!(pool.contains_key(&tp(&["S"])));
assert!(pool.contains_key(&tp(&["E"])));
assert!(pool.contains_key(&tp(&["T"])));
}
#[test]
fn ignores_functions_and_impls() {
let dir = make_tempdir(&[(
"lib.rs",
r#"
pub struct S { pub x: u32 }
pub fn unrelated() {}
impl S {
pub fn method(&self) {}
}
"#,
)]);
let pool = scan_src_dir(dir.path()).unwrap();
assert_eq!(pool.len(), 1);
assert!(pool.contains_key(&tp(&["S"])));
}
#[test]
fn collects_pub_crate_types() {
let dir = make_tempdir(&[(
"lib.rs",
r#"
pub(crate) struct Internal { pub x: u32 }
"#,
)]);
let pool = scan_src_dir(dir.path()).unwrap();
assert!(pool.contains_key(&tp(&["Internal"])));
}
#[test]
fn collects_inline_module_blocks() {
let dir = make_tempdir(&[(
"lib.rs",
r#"
pub mod nested {
pub struct Inner { pub x: u32 }
pub enum Sub { A }
}
"#,
)]);
let pool = scan_src_dir(dir.path()).unwrap();
assert!(pool.contains_key(&tp(&["nested", "Inner"])));
assert!(pool.contains_key(&tp(&["nested", "Sub"])));
}
#[test]
fn parse_error_surfaces_with_path() {
let dir = make_tempdir(&[("lib.rs", "pub struct Broken { this is not valid rust")]);
let err = scan_src_dir(dir.path()).unwrap_err();
match err {
ScanError::Parse { path, .. } => {
assert!(path.to_string_lossy().ends_with("lib.rs"));
}
other => panic!("expected Parse error, got {other:?}"),
}
}
#[test]
fn missing_directory_yields_io_error() {
let dir = make_tempdir(&[]);
let phantom = dir.path().join("does_not_exist");
let err = scan_src_dir(&phantom).unwrap_err();
assert!(matches!(err, ScanError::Io { .. }));
}
#[test]
fn skips_non_rust_files() {
let dir = make_tempdir(&[
("lib.rs", "pub struct S { pub x: u32 }"),
("README.md", "# unrelated"),
("data.json", "{}"),
]);
let pool = scan_src_dir(dir.path()).unwrap();
assert_eq!(pool.len(), 1);
}
#[test]
fn deterministic_ordering_via_btreemap() {
let files: &[(&str, &str)] =
&[("lib.rs", ""), ("z.rs", "pub struct Zee;"), ("a.rs", "pub struct Aye;"), ("m.rs", "pub struct Em;")];
let dir1 = make_tempdir(files);
let dir2 = make_tempdir(files);
let pool1 = scan_src_dir(dir1.path()).unwrap();
let pool2 = scan_src_dir(dir2.path()).unwrap();
let keys1: Vec<_> = pool1.keys().collect();
let keys2: Vec<_> = pool2.keys().collect();
assert_eq!(keys1, keys2);
let names: Vec<&str> = keys1.iter().map(|p| p.terminal()).collect();
assert_eq!(names, vec!["Aye", "Em", "Zee"]);
}
}