use std::collections::{BTreeMap, BTreeSet, HashSet};
use crate::model::AnalysisResult;
use crate::reference::PathPrefix;
pub type Edge = (String, String);
pub type AnnotatedEdges = BTreeMap<Edge, BTreeSet<String>>;
#[must_use]
pub(crate) fn truncate_module_path(path: &str, depth: Option<usize>) -> String {
depth.map_or_else(
|| path.to_owned(),
|n| path.split("::").take(n).collect::<Vec<_>>().join("::"),
)
}
fn find_module_target<'a>(
segments: &[String],
known_modules: &'a HashSet<String>,
) -> Option<&'a str> {
for len in (1..=segments.len()).rev() {
let candidate = segments[..len].join("::");
if let Some(m) = known_modules.get(&candidate) {
return Some(m.as_str());
}
}
None
}
pub(crate) fn build_edges(
result: &AnalysisResult,
depth: Option<usize>,
known_modules: &HashSet<String>,
package_name: Option<&str>,
show_apis: bool,
) -> AnnotatedEdges {
let mut edges: AnnotatedEdges = BTreeMap::new();
for (source_key, refs) in result.dependencies() {
let source_full = if source_key.is_empty() {
result.module_path()
} else {
source_key.as_str()
};
let source = truncate_module_path(source_full, depth);
if source.is_empty() {
continue;
}
for reference in refs {
let segments = reference.segments();
if segments.is_empty() {
continue;
}
let resolved: Option<(&str, &[String])> = match reference.prefix() {
PathPrefix::Crate => find_module_target(segments, known_modules)
.map(|m| (m, &segments[m.split("::").count()..])),
PathPrefix::None => {
let is_pkg_ref = package_name
.is_some_and(|pkg| segments.first().map(String::as_str) == Some(pkg));
if !is_pkg_ref {
continue;
}
let rest = &segments[1..];
Some(
find_module_target(rest, known_modules)
.map_or(("lib", rest), |m| (m, &rest[m.split("::").count()..])),
)
}
_ => continue,
};
let Some((module_path, api_segments)) = resolved else {
continue;
};
let target = truncate_module_path(module_path, depth);
if target.is_empty() {
continue;
}
if source == target {
continue;
}
let apis = edges.entry((source.clone(), target)).or_default();
if show_apis && !api_segments.is_empty() {
apis.insert(api_segments.join("::"));
}
}
}
edges
}
#[cfg(test)]
mod tests {
use super::*;
fn module_set(names: &[&str]) -> HashSet<String> {
names.iter().map(ToString::to_string).collect()
}
fn segments(names: &[&str]) -> Vec<String> {
names.iter().map(ToString::to_string).collect()
}
#[test]
fn truncate_module_path_none_returns_full() {
assert_eq!(
truncate_module_path("parser::visitor", None),
"parser::visitor"
);
}
#[test]
fn truncate_module_path_depth_1() {
assert_eq!(truncate_module_path("parser::visitor", Some(1)), "parser");
}
#[test]
fn truncate_module_path_depth_equals_length() {
assert_eq!(
truncate_module_path("parser::visitor", Some(2)),
"parser::visitor"
);
}
#[test]
fn truncate_module_path_depth_exceeds_length() {
assert_eq!(truncate_module_path("parser", Some(5)), "parser");
}
#[test]
fn finds_direct_module_match() {
let known = module_set(&["discover", "discover::module_tree", "reference"]);
assert_eq!(
find_module_target(&segments(&["discover", "CrateInfo"]), &known),
Some("discover")
);
}
#[test]
fn finds_nested_module_match() {
let known = module_set(&["discover", "discover::module_tree"]);
assert_eq!(
find_module_target(&segments(&["discover", "module_tree", "Node"]), &known),
Some("discover::module_tree")
);
}
#[test]
fn prefers_longest_prefix() {
let known = module_set(&["discover", "discover::module_tree"]);
assert_eq!(
find_module_target(&segments(&["discover", "module_tree"]), &known),
Some("discover::module_tree")
);
}
#[test]
fn returns_none_when_no_prefix_matches() {
let known = module_set(&["cache", "parser"]);
assert_eq!(find_module_target(&segments(&["Analyzer"]), &known), None);
}
#[test]
fn returns_none_for_empty_segments() {
let known = module_set(&["parser"]);
assert_eq!(find_module_target(&[], &known), None);
}
}