use std::collections::{BTreeMap, BTreeSet, HashSet};
use crate::model::AnalysisResult;
use crate::reference::{PathPrefix, TypeReference};
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>) -> &str {
let Some(n) = depth else { return path };
if n == 0 {
return "";
}
path.match_indices("::")
.nth(n - 1)
.map_or(path, |(idx, _)| path.get(..idx).unwrap_or(path))
}
pub(crate) 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 resolve_reference_target<'a>(
reference: &'a TypeReference,
known_modules: &'a HashSet<String>,
package_name: Option<&str>,
) -> Option<(&'a str, &'a [String])> {
let segments = reference.segments();
if segments.is_empty() {
return None;
}
match reference.prefix() {
PathPrefix::Crate => Some(
find_module_target(segments, known_modules).map_or(("lib", segments), |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 {
return None;
}
let rest = &segments[1..];
Some(
find_module_target(rest, known_modules)
.map_or(("lib", rest), |m| (m, &rest[m.split("::").count()..])),
)
}
_ => 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 Some((module_path, api_segments)) =
resolve_reference_target(reference, known_modules, package_name)
else {
continue;
};
let target = truncate_module_path(module_path, depth);
if target.is_empty() {
continue;
}
if source == target {
continue;
}
let apis = edges
.entry((source.to_owned(), target.to_owned()))
.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 truncate_module_path_depth_zero_yields_empty() {
assert_eq!(truncate_module_path("parser::visitor", Some(0)), "");
}
#[test]
fn truncate_module_path_empty_input() {
assert_eq!(truncate_module_path("", Some(2)), "");
assert_eq!(truncate_module_path("", None), "");
}
#[test]
fn truncate_module_path_repeated_separator_matches_split_semantics() {
for n in 0..=4 {
let expected = "a::::b".split("::").take(n).collect::<Vec<_>>().join("::");
assert_eq!(truncate_module_path("a::::b", Some(n)), expected, "n = {n}");
}
}
#[test]
fn truncate_module_path_returns_slice_of_input() {
let path = String::from("parser::visitor::inner");
let truncated = truncate_module_path(&path, Some(2));
assert_eq!(truncated, "parser::visitor");
assert!(std::ptr::eq(truncated.as_ptr(), path.as_ptr()));
}
#[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);
}
}