mod visitor;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::cache::ParseCache;
use crate::utils::{ReadFileError, descend_inline_module, read_source_file};
use syn::File;
use syn::visit::Visit;
use thiserror::Error;
use tracing::info;
use crate::reference::{PathPrefix, TypeReference};
use visitor::ModuleVisitor;
#[derive(Debug, Error)]
pub(crate) enum AnalyzerError {
#[error("Failed to read file '{path}': {source}")]
FileRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("File too large '{path}': {size} bytes (limit {limit} bytes)")]
FileTooLarge {
path: PathBuf,
size: u64,
limit: u64,
},
#[error("Failed to parse file '{path}': {message}")]
Parse {
path: PathBuf,
message: String,
},
}
pub(crate) type Result<T> = std::result::Result<T, AnalyzerError>;
#[derive(Debug, Clone)]
pub(crate) struct CrateAnalyzer {
crate_name: String,
files: HashMap<String, Vec<TypeReference>>,
file_order: Vec<String>,
}
impl CrateAnalyzer {
pub(crate) fn new(crate_name: impl Into<String>) -> Self {
Self {
crate_name: crate_name.into(),
files: HashMap::new(),
file_order: Vec::new(),
}
}
pub(crate) fn parse_file(
&mut self,
module: impl Into<String>,
path: &Path,
inline_scope: &[String],
children: HashSet<String>,
cache: &mut ParseCache,
) -> Result<Vec<TypeReference>> {
let syntax: Rc<File> = cache.get_or_parse(path, |p| {
let content = read_source_file(p).map_err(|e| match e {
ReadFileError::Io(source) => AnalyzerError::FileRead {
path: p.to_path_buf(),
source,
},
ReadFileError::TooLarge { size, limit } => AnalyzerError::FileTooLarge {
path: p.to_path_buf(),
size,
limit,
},
})?;
syn::parse_file(&content).map_err(|e| AnalyzerError::Parse {
path: p.to_path_buf(),
message: e.to_string(),
})
})?;
let module = module.into();
let package_name = Some(self.crate_name.clone());
let mut visitor = ModuleVisitor::new(module.clone(), children, package_name);
if inline_scope.is_empty() {
visitor.visit_file(&syntax);
} else if let Some(items) = descend_inline_module(&syntax.items, inline_scope) {
for item in items {
visitor.visit_item(item);
}
}
let result: Vec<TypeReference> = visitor.references.all().cloned().collect();
info!(
"Parsed '{module}': {} references from {}{}",
result.len(),
path.display(),
if inline_scope.is_empty() {
String::new()
} else {
format!(" (inline {inline_scope:?})")
}
);
if !self.files.contains_key(&module) {
self.file_order.push(module.clone());
}
self.files.insert(module, result.clone());
Ok(result)
}
pub(crate) fn all_crate_references<'a>(
&'a self,
children_map: &'a HashMap<String, HashSet<String>>,
) -> impl Iterator<Item = (&'a String, Vec<&'a TypeReference>)> {
self.file_order.iter().filter_map(move |module| {
self.files.get(module).map(|refs| {
let crate_refs: Vec<&TypeReference> = refs
.iter()
.filter(|r| {
r.is_relative()
|| r.is_from_crate(&self.crate_name)
|| Self::is_bare_child(r, module, children_map)
})
.collect();
(module, crate_refs)
})
})
}
fn is_bare_child(
r: &TypeReference,
module: &str,
children_map: &HashMap<String, HashSet<String>>,
) -> bool {
if r.prefix() != PathPrefix::None {
return false;
}
let first = r.segments().first();
first.is_some_and(|s| {
children_map
.get(module)
.is_some_and(|ch| ch.contains(s.as_str()))
|| children_map
.get("")
.is_some_and(|ch| ch.contains(s.as_str()))
})
}
}
#[cfg(test)]
impl CrateAnalyzer {
pub(crate) fn crate_name(&self) -> &str {
&self.crate_name
}
pub(crate) fn total_references(&self) -> usize {
self.files.values().map(Vec::len).sum()
}
pub(crate) fn file_count(&self) -> usize {
self.files.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reference::PathPrefix;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
fn parse_use(code: &str) -> Vec<TypeReference> {
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("", HashSet::new(), None);
visitor.visit_file(&syntax);
visitor.references.all().cloned().collect()
}
#[test]
fn test_simple_use() {
let refs = parse_use("use std::collections::HashMap;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "std::collections::HashMap");
}
#[test]
fn test_use_alias() {
let refs = parse_use("use std::collections::HashMap as Map;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "std::collections::HashMap as Map");
}
#[test]
fn test_use_glob() {
let refs = parse_use("use std::collections::*;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "std::collections::*");
assert!(refs[0].has_glob());
}
#[test]
fn test_use_crate() {
let refs = parse_use("use crate::module::Type;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "crate::module::Type");
assert!(refs[0].is_relative());
}
#[test]
fn test_use_self() {
let refs = parse_use("use self::submodule::Type;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "crate::submodule::Type");
assert_eq!(refs[0].prefix(), PathPrefix::Crate);
}
#[test]
fn test_use_super() {
let refs = parse_use("use super::sibling::Type;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "super::sibling::Type");
assert_eq!(refs[0].prefix(), PathPrefix::Super(1));
}
#[test]
fn test_use_super_multiple() {
let refs = parse_use("use super::super::ancestor::Type;");
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "super::super::ancestor::Type");
assert_eq!(refs[0].prefix(), PathPrefix::Super(2));
}
#[test]
fn test_use_group() {
let refs = parse_use("use std::collections::{HashMap, HashSet};");
assert_eq!(refs.len(), 1);
assert!(refs[0].has_group());
assert_eq!(
refs[0].to_path_string(),
"std::collections::{HashMap, HashSet}"
);
}
#[test]
fn test_use_group_with_self() {
let refs = parse_use("use std::collections::{self, HashMap};");
assert_eq!(refs.len(), 1);
assert!(refs[0].has_group());
}
#[test]
fn test_use_nested_group() {
let refs = parse_use("use std::{collections::{HashMap, HashSet}, io::Read};");
assert_eq!(refs.len(), 1);
assert!(refs[0].has_group());
}
#[test]
fn test_use_group_path_items_produce_nested_with_items() {
use crate::analyzer::expand_groups;
use crate::reference::GroupItem;
let refs = parse_use(
"use crate::args::{client::ClientAction, system::{PingArgs, StatsArgs}, topic::TopicAction};",
);
assert_eq!(refs.len(), 1);
assert!(refs[0].has_group());
let expanded = expand_groups(&refs[0]);
let paths: Vec<String> = expanded.iter().map(TypeReference::to_path_string).collect();
assert!(
paths.contains(&"crate::args::client::ClientAction".to_owned()),
"client::ClientAction missing from expanded: {paths:?}"
);
assert!(
paths.contains(&"crate::args::topic::TopicAction".to_owned()),
"topic::TopicAction missing from expanded: {paths:?}"
);
assert!(
paths.contains(&"crate::args::system::PingArgs".to_owned()),
"system::PingArgs missing from expanded: {paths:?}"
);
assert!(
paths.contains(&"crate::args::system::StatsArgs".to_owned()),
"system::StatsArgs missing from expanded: {paths:?}"
);
assert_eq!(expanded.len(), 4);
if let crate::reference::PathSuffix::Group(items) = refs[0].suffix() {
for item in items {
if let GroupItem::Nested { prefix, items } = item {
assert!(
!items.is_empty(),
"Nested group item {prefix:?} has empty items"
);
}
}
}
}
#[test]
fn test_crate_analyzer() {
let analyzer = CrateAnalyzer::new("test_crate");
assert_eq!(analyzer.crate_name(), "test_crate");
assert_eq!(analyzer.file_count(), 0);
assert_eq!(analyzer.total_references(), 0);
}
#[test]
fn test_type_path_collection() {
let code = "
fn foo(x: crate::MyType) -> crate::Result {
let y: crate::Other = x;
y
}
";
let refs = parse_use(code);
assert!(refs.len() >= 2, "Should capture type annotations");
let paths: Vec<String> = refs.iter().map(TypeReference::to_path_string).collect();
assert!(paths.iter().any(|p| p.contains("MyType")));
assert!(paths.iter().any(|p| p.contains("Result")));
}
#[test]
fn test_expr_path_collection() {
let code = "
fn foo() {
crate::module::function();
let x = crate::module::Type::new();
}
";
let refs = parse_use(code);
assert!(refs.len() >= 2, "Should capture expression paths");
let paths: Vec<String> = refs.iter().map(TypeReference::to_path_string).collect();
assert!(paths.iter().any(|p| p.contains("function")));
assert!(paths.iter().any(|p| p.contains("Type")));
}
#[test]
fn test_impl_trait_collection() {
let code = "
impl crate::MyTrait for Foo {
fn bar() {}
}
";
let refs = parse_use(code);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].to_path_string(), "crate::MyTrait");
}
#[test]
fn test_struct_pattern_collection() {
let code = "
fn foo(x: Something) {
match x {
crate::module::Variant { field } => field,
}
}
";
let refs = parse_use(code);
assert!(refs.iter().any(|r| r.to_path_string().contains("Variant")));
}
#[test]
fn test_macro_path_collection() {
let code = "
fn foo() {
crate::macros::my_macro!();
}
";
let refs = parse_use(code);
assert_eq!(refs.len(), 1);
assert!(refs[0].to_path_string().contains("my_macro"));
}
#[test]
fn test_resolve_self_in_module() {
let code = "use self::submodule::Type;";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("utils::parser", HashSet::new(), None);
visitor.visit_file(&syntax);
let uses = &visitor.references.use_statements;
assert_eq!(uses.len(), 1);
assert_eq!(
uses[0].to_path_string(),
"crate::utils::parser::submodule::Type"
);
assert_eq!(uses[0].prefix(), PathPrefix::Crate);
}
#[test]
fn test_resolve_super_in_nested_module() {
let code = "use super::sibling::Type;";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("utils::parser", HashSet::new(), None);
visitor.visit_file(&syntax);
let uses = &visitor.references.use_statements;
assert_eq!(uses.len(), 1);
assert_eq!(uses[0].to_path_string(), "crate::utils::sibling::Type");
assert_eq!(uses[0].prefix(), PathPrefix::Crate);
}
#[test]
fn test_resolve_super_multiple_in_deeply_nested_module() {
let code = "use super::super::ancestor::Type;";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("a::b::c", HashSet::new(), None);
visitor.visit_file(&syntax);
let uses = &visitor.references.use_statements;
assert_eq!(uses.len(), 1);
assert_eq!(uses[0].to_path_string(), "crate::a::ancestor::Type");
assert_eq!(uses[0].prefix(), PathPrefix::Crate);
}
#[test]
fn test_resolve_preserves_groups() {
let code = "use self::{foo, bar::Baz};";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("utils", HashSet::new(), None);
visitor.visit_file(&syntax);
let uses = &visitor.references.use_statements;
assert_eq!(uses.len(), 1);
assert_eq!(uses[0].to_path_string(), "crate::utils::{foo, bar::Baz}");
assert_eq!(uses[0].prefix(), PathPrefix::Crate);
}
#[test]
fn test_resolve_preserves_glob() {
let code = "use self::submodule::*;";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("utils", HashSet::new(), None);
visitor.visit_file(&syntax);
let uses = &visitor.references.use_statements;
assert_eq!(uses.len(), 1);
assert_eq!(uses[0].to_path_string(), "crate::utils::submodule::*");
assert_eq!(uses[0].prefix(), PathPrefix::Crate);
assert!(uses[0].has_glob());
}
#[test]
fn test_resolve_preserves_alias() {
let code = "use self::submodule::Type as MyType;";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("utils", HashSet::new(), None);
visitor.visit_file(&syntax);
let uses = &visitor.references.use_statements;
assert_eq!(uses.len(), 1);
assert_eq!(
uses[0].to_path_string(),
"crate::utils::submodule::Type as MyType"
);
assert_eq!(uses[0].prefix(), PathPrefix::Crate);
}
#[test]
fn test_resolve_expression_paths() {
let code = "
fn foo() {
self::helper::do_something();
super::sibling::bar();
}
";
let syntax: File = syn::parse_file(code).unwrap();
let mut visitor = ModuleVisitor::new("utils::parser", HashSet::new(), None);
visitor.visit_file(&syntax);
assert!(visitor.references.value_refs.len() >= 2);
let paths: Vec<String> = visitor
.references
.value_refs
.iter()
.map(TypeReference::to_path_string)
.collect();
assert!(
paths
.iter()
.any(|p| p.contains("crate::utils::parser::helper"))
);
assert!(paths.iter().any(|p| p.contains("crate::utils::sibling")));
}
#[test]
fn parse_file_returns_file_read_error_for_nonexistent_file() {
let mut analyzer = CrateAnalyzer::new("test");
let mut cache = ParseCache::new();
let err = analyzer
.parse_file(
"mod",
Path::new("/nonexistent/file.rs"),
&[],
HashSet::new(),
&mut cache,
)
.unwrap_err();
assert!(matches!(err, AnalyzerError::FileRead { .. }));
}
#[test]
fn parse_file_returns_parse_error_for_invalid_syntax() {
let mut f = NamedTempFile::new().unwrap();
writeln!(f, "this is not valid rust !!!").unwrap();
let mut analyzer = CrateAnalyzer::new("test");
let mut cache = ParseCache::new();
let err = analyzer
.parse_file("mod", f.path(), &[], HashSet::new(), &mut cache)
.unwrap_err();
assert!(matches!(err, AnalyzerError::Parse { .. }));
}
#[test]
fn read_source_file_returns_file_too_large_when_size_exceeds_limit() {
use crate::utils::{MAX_FILE_BYTES, ReadFileError};
use std::io::Write;
let mut f = NamedTempFile::new().unwrap();
let chunk = vec![b' '; 1024];
for _ in 0..=(MAX_FILE_BYTES / 1024) {
f.write_all(&chunk).unwrap();
}
f.flush().unwrap();
let err = read_source_file(f.path()).unwrap_err();
assert!(matches!(err, ReadFileError::TooLarge { limit, .. } if limit == MAX_FILE_BYTES));
}
}