use crate::cache::ParseCache;
use crate::discover::{CrateInfo, ModuleInfo};
use crate::error::{AnalysisError, Result};
use crate::model::{AnalysisOptions, AnalysisResult};
use crate::parser::CrateAnalyzer;
use crate::reference::{GroupItem, PathSuffix, TypeReference};
use crate::resolve::resolve_glob;
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Formatter, Result as FmtResult};
use std::path::{Path, PathBuf};
use tracing::{debug, error, info, trace};
pub(crate) fn expand_groups(reference: &TypeReference) -> Vec<TypeReference> {
match reference.suffix() {
PathSuffix::None | PathSuffix::Alias(_) => vec![reference.clone_with(true, false)],
PathSuffix::Glob => vec![reference.clone_with(true, true)],
PathSuffix::Group(items) => {
let mut result = Vec::new();
for item in items {
match item {
GroupItem::Simple(name) | GroupItem::Aliased { name, alias: _ } => {
result.push(reference.clone_with(true, false).append_segment(name));
}
GroupItem::SelfItem { alias: _ } => {
result.push(reference.clone_with(true, false));
}
GroupItem::Glob => {
result.push(reference.clone_with(true, true));
}
GroupItem::Nested {
prefix,
items: nested_items,
} => {
let mut nested = reference.clone_with(true, false);
for seg in prefix {
nested = nested.append_segment(seg);
}
let nested = nested.with_group(nested_items.clone());
result.extend(expand_groups(&nested));
}
}
}
result
}
}
}
#[derive(Clone)]
pub struct Analyzer {
crate_info: CrateInfo,
parser: CrateAnalyzer,
parse_cache: ParseCache,
}
impl Debug for Analyzer {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("Analyzer")
.field("crate_info", &self.crate_info)
.field("parser", &self.parser)
.field(
"parse_cache",
&format!("<{} entries>", self.parse_cache.len()),
)
.finish()
}
}
impl Analyzer {
pub fn new(crate_root: impl AsRef<Path>) -> Result<Self> {
let crate_info = CrateInfo::new(crate_root.as_ref())?;
let name = crate_info.root_package_name();
let parser = CrateAnalyzer::new(name);
Ok(Self {
crate_info,
parser,
parse_cache: ParseCache::new(),
})
}
pub fn analyze_module(
&mut self,
module_path: impl Into<String>,
options: &AnalysisOptions,
) -> Result<AnalysisResult> {
let module_path = module_path.into();
let modules = self.crate_info.get_module_tree(
&module_path,
options.recursive,
options.include_tests,
&mut self.parse_cache,
)?;
let source_file = modules
.first()
.map(|m| m.source().to_path_buf())
.unwrap_or_default();
let file_root = self.build_file_root_map(&modules);
self.parse_all_modules(modules, &file_root)?;
let dependencies = self.collect_references(options);
Ok(AnalysisResult::new(module_path, dependencies, source_file))
}
fn parse_all_modules(
&mut self,
modules: Vec<ModuleInfo>,
file_root: &HashMap<PathBuf, String>,
) -> Result<()> {
for module in modules {
let root_path = &file_root[module.source()];
let inline_scope = Self::compute_inline_scope(module.path(), root_path);
trace!(
"Module '{}' inline_scope={:?} (file root: '{}')",
module.path(),
inline_scope,
root_path
);
info!(
"Analyzing module: {} (file: {})",
module.path(),
module.source().display()
);
match self.parser.parse_file(
module.path(),
module.source(),
&inline_scope,
&mut self.parse_cache,
) {
Err(e) => {
error!("Error while analyzing module '{}': {e}", module.path());
return Err(AnalysisError::ModuleAnalysisFailed {
module_path: module.path().to_owned(),
file: module.source().to_path_buf(),
source: e,
});
}
Ok(type_list) => {
info!("Analyzed {}", module.path());
for reference in &type_list {
debug!("Analyzed {reference:?}");
info!("Found reference: {}", reference.to_path_string());
}
}
}
}
Ok(())
}
fn collect_references(
&mut self,
options: &AnalysisOptions,
) -> HashMap<String, HashSet<TypeReference>> {
let mut dependencies = HashMap::new();
for (module, module_references) in self.parser.all_crate_references() {
debug!("Processing module: {}", module);
let mut refs = HashSet::new();
for reference in module_references {
debug!("Found crate reference: {}", reference.to_path_string());
let after_expand = if options.expand_groups {
debug!(
"Expanding groups for reference: {}",
reference.to_path_string()
);
let expanded = expand_groups(reference);
for exp in &expanded {
debug!("Expanded reference: {}", exp.to_path_string());
}
expanded
} else {
vec![reference.clone()]
};
for r in after_expand {
if options.resolve_globs && r.has_glob() {
debug!("Resolving glob: {}", r.to_path_string());
let resolved =
resolve_glob(&r, module, &self.crate_info, &mut self.parse_cache);
for res in resolved {
debug!("Resolved glob item: {}", res.to_path_string());
refs.insert(res);
}
} else {
refs.insert(r);
}
}
}
debug!(
"Processing module: {module} complete, found {} dependencies",
dependencies.len()
);
dependencies.insert(module.clone(), refs);
}
dependencies
}
fn build_file_root_map(&self, modules: &[ModuleInfo]) -> HashMap<PathBuf, String> {
let mut file_root: HashMap<PathBuf, String> = HashMap::new();
for module in modules {
let source_path = module.source().to_path_buf();
let actual_root = self.find_actual_file_root(module.path(), &source_path);
match file_root.entry(source_path) {
std::collections::hash_map::Entry::Occupied(mut e) => {
if actual_root.len() < e.get().len() {
*e.get_mut() = actual_root;
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(actual_root);
}
}
}
file_root
}
fn find_actual_file_root(&self, module_path: &str, source_file: &Path) -> String {
trace!(
"Finding file root for module '{}' in file '{}'",
module_path,
source_file.display()
);
if module_path.is_empty() {
return String::new();
}
let is_crate_root = source_file.to_string_lossy().ends_with("src/lib.rs")
|| source_file.to_string_lossy().ends_with("src/main.rs");
if is_crate_root {
trace!(
"Source file is crate root, returning empty string for module '{}'",
module_path
);
return String::new();
}
let segments: Vec<&str> = module_path.split("::").collect();
for len in (1..segments.len()).rev() {
let prefix = segments[..len].join("::");
trace!("Trying prefix: '{}'", prefix);
match self.crate_info.resolve_module_path_to_file(&prefix) {
Ok(ref resolved) => {
trace!("Prefix '{}' resolved to '{}'", prefix, resolved.display());
if resolved == source_file {
trace!("Found file root: '{}' for module '{}'", prefix, module_path);
return prefix;
}
}
Err(e) => {
trace!("Prefix '{}' failed to resolve: {}", prefix, e);
}
}
}
trace!("No shorter prefix found, using original: '{}'", module_path);
module_path.to_owned()
}
fn compute_inline_scope(module_path: &str, root_path: &str) -> Vec<String> {
if module_path == root_path {
vec![]
} else if root_path.is_empty() {
module_path.split("::").map(String::from).collect()
} else {
module_path
.strip_prefix(root_path)
.and_then(|s| s.strip_prefix("::"))
.map(|s| s.split("::").map(String::from).collect())
.unwrap_or_default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reference::PathSuffix;
fn make_ref(segments: &[&str], suffix: PathSuffix) -> TypeReference {
let base = TypeReference::new(segments.iter().copied());
match suffix {
PathSuffix::None => base,
PathSuffix::Alias(a) => base.with_alias(a),
PathSuffix::Glob => base.with_glob(),
PathSuffix::Group(g) => base.with_group(g),
}
}
fn expand_to_segments(r: &TypeReference) -> Vec<Vec<String>> {
expand_groups(r)
.into_iter()
.map(|t| t.segments().to_vec())
.collect()
}
#[test]
fn test_expand_groups_none_passthrough() {
let r = make_ref(&["std", "collections"], PathSuffix::None);
assert_eq!(expand_to_segments(&r), vec![vec!["std", "collections"]]);
}
#[test]
fn test_expand_groups_alias_passthrough() {
let r = make_ref(
&["std", "collections", "HashMap"],
PathSuffix::Alias("Map".into()),
);
assert_eq!(
expand_to_segments(&r),
vec![vec!["std", "collections", "HashMap"]]
);
}
#[test]
fn test_expand_groups_glob_passthrough() {
let r = make_ref(&["std", "collections"], PathSuffix::Glob);
assert_eq!(expand_to_segments(&r), vec![vec!["std", "collections"]]);
}
#[test]
fn test_expand_groups_simple() {
let r = make_ref(
&["std", "collections"],
PathSuffix::Group(vec![
GroupItem::Simple("HashMap".into()),
GroupItem::Simple("HashSet".into()),
]),
);
assert_eq!(
expand_to_segments(&r),
vec![
vec!["std", "collections", "HashMap"],
vec!["std", "collections", "HashSet"],
]
);
}
#[test]
fn test_expand_groups_aliased_uses_original_name() {
let r = make_ref(
&["std", "collections"],
PathSuffix::Group(vec![GroupItem::Aliased {
name: "HashMap".into(),
alias: "Map".into(),
}]),
);
assert_eq!(
expand_to_segments(&r),
vec![vec!["std", "collections", "HashMap"]]
);
}
#[test]
fn test_expand_groups_self_item_no_alias() {
let r = make_ref(
&["std", "collections", "module"],
PathSuffix::Group(vec![GroupItem::SelfItem { alias: None }]),
);
assert_eq!(
expand_to_segments(&r),
vec![vec!["std", "collections", "module"]]
);
}
#[test]
fn test_expand_groups_self_item_with_alias() {
let r = make_ref(
&["std", "collections", "module"],
PathSuffix::Group(vec![GroupItem::SelfItem {
alias: Some("Alias".into()),
}]),
);
assert_eq!(
expand_to_segments(&r),
vec![vec!["std", "collections", "module"]]
);
}
#[test]
fn test_expand_groups_self_item_empty_base_no_alias() {
let r = make_ref(
&[],
PathSuffix::Group(vec![GroupItem::SelfItem { alias: None }]),
);
assert_eq!(expand_to_segments(&r), vec![vec![] as Vec<String>]);
}
#[test]
fn test_expand_groups_glob_returns_base() {
let r = make_ref(
&["std", "collections"],
PathSuffix::Group(vec![GroupItem::Glob]),
);
assert_eq!(expand_to_segments(&r), vec![vec!["std", "collections"]]);
}
#[test]
fn test_expand_groups_nested() {
let r = make_ref(
&["std"],
PathSuffix::Group(vec![GroupItem::Nested {
prefix: vec!["collections".into()],
items: vec![
GroupItem::Simple("HashMap".into()),
GroupItem::Simple("HashSet".into()),
],
}]),
);
assert_eq!(
expand_to_segments(&r),
vec![
vec!["std", "collections", "HashMap"],
vec!["std", "collections", "HashSet"],
]
);
}
#[test]
fn test_expand_groups_mixed() {
let r = make_ref(
&["m", "n"],
PathSuffix::Group(vec![
GroupItem::Simple("a".into()),
GroupItem::Aliased {
name: "b".into(),
alias: "B".into(),
},
GroupItem::Nested {
prefix: vec!["c".into()],
items: vec![GroupItem::Simple("x".into()), GroupItem::Simple("y".into())],
},
GroupItem::Glob,
]),
);
assert_eq!(
expand_to_segments(&r),
vec![
vec!["m", "n", "a"],
vec!["m", "n", "b"],
vec!["m", "n", "c", "x"],
vec!["m", "n", "c", "y"],
vec!["m", "n"],
]
);
}
#[test]
fn test_expand_groups_deeply_nested() {
let r = make_ref(
&["a"],
PathSuffix::Group(vec![GroupItem::Nested {
prefix: vec!["b".into()],
items: vec![GroupItem::Nested {
prefix: vec!["c".into()],
items: vec![
GroupItem::Simple("d".into()),
GroupItem::Simple("e".into()),
GroupItem::Simple("f".into()),
],
}],
}]),
);
assert_eq!(
expand_to_segments(&r),
vec![
vec!["a", "b", "c", "d"],
vec!["a", "b", "c", "e"],
vec!["a", "b", "c", "f"],
]
);
}
}