mod cycles;
mod edges;
mod orphans;
mod paths;
use std::collections::BTreeSet;
pub use cycles::Cycle;
pub use edges::{AnnotatedEdges, Edge};
pub use paths::ShortestPaths;
pub(crate) use cycles::detect_cycles;
pub(crate) use edges::truncate_module_path;
pub(crate) use orphans::find_orphans;
pub(crate) use paths::compute_shortest_paths;
pub(crate) use edges::build_edges;
pub(crate) use edges::resolve_reference_target;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct DependencyGraphOptions {
pub include_tests: bool,
pub depth: Option<usize>,
pub show_apis: bool,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
edges: AnnotatedEdges,
modules: BTreeSet<String>,
}
impl DependencyGraph {
pub(crate) const fn new(edges: AnnotatedEdges, modules: BTreeSet<String>) -> Self {
Self { edges, modules }
}
#[must_use]
pub const fn edges(&self) -> &AnnotatedEdges {
&self.edges
}
#[must_use]
pub const fn modules(&self) -> &BTreeSet<String> {
&self.modules
}
#[must_use]
pub fn cycles(&self) -> Vec<Cycle> {
detect_cycles(&self.edges)
}
#[must_use]
pub fn orphans(&self) -> Vec<String> {
find_orphans(&self.edges, &self.modules)
}
#[must_use]
pub fn truncated_edges(&self, depth: Option<usize>) -> BTreeSet<Edge> {
self.edges
.keys()
.map(|(source, target)| {
(
truncate_module_path(source, depth),
truncate_module_path(target, depth),
)
})
.filter(|(source, target)| source != target)
.map(|(source, target)| (source.to_owned(), target.to_owned()))
.collect()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
pub fn shortest_paths(
&self,
source: &str,
target: &str,
) -> crate::error::Result<ShortestPaths> {
if !self.modules.contains(source) {
return Err(crate::error::AnalysisError::ModuleNotFound {
module_path: source.to_owned(),
});
}
if !self.modules.contains(target) {
return Err(crate::error::AnalysisError::ModuleNotFound {
module_path: target.to_owned(),
});
}
Ok(compute_shortest_paths(
&self.edges,
&self.modules,
source,
target,
))
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
fn make_graph(pairs: &[(&str, &str)], module_names: &[&str]) -> DependencyGraph {
let edges: AnnotatedEdges = pairs
.iter()
.map(|(s, t)| ((s.to_string(), t.to_string()), BTreeSet::new()))
.collect();
let modules: BTreeSet<String> = module_names.iter().map(|s| (*s).to_owned()).collect();
DependencyGraph::new(edges, modules)
}
#[test]
fn empty_graph() {
let g = DependencyGraph::new(BTreeMap::new(), BTreeSet::new());
assert!(g.is_empty());
assert!(g.cycles().is_empty());
assert!(g.orphans().is_empty());
assert!(g.modules().is_empty());
}
#[test]
fn edges_and_modules_accessible() {
let g = make_graph(&[("a", "b")], &["a", "b"]);
assert_eq!(g.edges().len(), 1);
assert_eq!(g.modules().len(), 2);
assert!(!g.is_empty());
}
#[test]
fn cycles_detected() {
let g = make_graph(&[("a", "b"), ("b", "a")], &["a", "b"]);
let cycles = g.cycles();
assert_eq!(cycles.len(), 1);
assert!(cycles[0].modules.contains("a"));
assert!(cycles[0].modules.contains("b"));
}
#[test]
fn orphans_detected() {
let g = make_graph(&[("a", "b")], &["a", "b", "c"]);
let orphans = g.orphans();
assert_eq!(orphans, vec!["a", "c"]);
}
#[test]
fn no_cycles_in_dag() {
let g = make_graph(&[("a", "b"), ("b", "c")], &["a", "b", "c"]);
assert!(g.cycles().is_empty());
}
#[test]
fn truncated_edges_none_returns_all() {
let g = make_graph(&[("a::x", "b::y"), ("b::y", "c")], &["a::x", "b::y", "c"]);
let edges = g.truncated_edges(None);
assert_eq!(edges.len(), 2);
assert!(edges.contains(&("a::x".to_owned(), "b::y".to_owned())));
}
#[test]
fn truncated_edges_drops_self_loops_after_collapse() {
let g = make_graph(&[("a::x", "a::y"), ("a::x", "b::z")], &["a::x", "a::y"]);
let edges = g.truncated_edges(Some(1));
assert_eq!(
edges.into_iter().collect::<Vec<_>>(),
vec![("a".to_owned(), "b".to_owned())]
);
}
#[test]
fn truncated_edges_dedups_collapsed_pairs() {
let g = make_graph(
&[("a::x", "b::y"), ("a::x", "b::z"), ("a::w", "b::y")],
&["a::x", "a::w", "b::y", "b::z"],
);
let edges = g.truncated_edges(Some(1));
assert_eq!(
edges.into_iter().collect::<Vec<_>>(),
vec![("a".to_owned(), "b".to_owned())]
);
}
}