use std::collections::BTreeSet;
use std::fmt::Write;
use crawk::{AnnotatedEdges, Cycle};
use super::format_api_suffix;
#[must_use]
pub(crate) fn render_cycles_plain(cycles: &[Cycle]) -> String {
if cycles.is_empty() {
return String::new();
}
let mut out = String::new();
for (i, cycle) in cycles.iter().enumerate() {
if i > 0 {
out.push('\n');
}
let _ = writeln!(out, "cycle {} ({} modules)", i + 1, cycle.modules.len());
for ((source, target), apis) in &cycle.edges {
let _ = writeln!(out, " {source} -> {target}{}", format_api_suffix(apis));
}
}
out
}
#[must_use]
pub(crate) fn render_cycles_grouped(cycles: &[Cycle]) -> String {
if cycles.is_empty() {
return String::new();
}
let mut out = String::new();
for (i, cycle) in cycles.iter().enumerate() {
if i > 0 {
out.push('\n');
}
let names: Vec<&str> = cycle.modules.iter().map(String::as_str).collect();
let _ = writeln!(
out,
"cycle {} ({} modules): {}",
i + 1,
cycle.modules.len(),
names.join(", ")
);
for ((source, target), apis) in &cycle.edges {
let _ = writeln!(out, " {source} -> {target}{}", format_api_suffix(apis));
}
}
out
}
fn write_cycle_subgraphs(out: &mut String, cycles: &[Cycle]) {
for (i, cycle) in cycles.iter().enumerate() {
let _ = write!(out, "\n subgraph cluster_cycle_{} {{\n", i + 1);
let _ = writeln!(
out,
" label=\"cycle {} ({} modules)\";",
i + 1,
cycle.modules.len()
);
out.push_str(" style=dashed;\n");
out.push_str(" color=\"#e67700\";\n");
out.push_str(" fontcolor=\"#e67700\";\n");
for module in &cycle.modules {
let _ = writeln!(out, " \"{module}\";");
}
out.push_str(" }\n");
}
}
#[must_use]
pub(crate) fn render_cycles_dot(cycles: &[Cycle]) -> String {
let mut out = String::new();
out.push_str("digraph dependencies {\n");
out.push_str(" rankdir=LR;\n");
out.push_str(" node [shape=box, style=rounded, fontname=\"monospace\", fontsize=10];\n");
out.push_str(" edge [color=\"#444444\"];\n");
write_cycle_subgraphs(&mut out, cycles);
let has_edges = cycles.iter().any(|c| !c.edges.is_empty());
if has_edges {
out.push('\n');
for cycle in cycles {
for ((source, target), apis) in &cycle.edges {
let _ = write!(
out,
" \"{source}\" -> \"{target}\" [color=\"#e67700\", style=bold, penwidth=2.0"
);
if !apis.is_empty() {
let items: Vec<&str> = apis.iter().map(String::as_str).collect();
let _ = write!(
out,
", label=\"{}\", fontcolor=\"#c96a00\"",
items.join(", ")
);
}
out.push_str("];\n");
}
}
}
out.push_str("}\n");
out
}
#[must_use]
pub(crate) fn render_cycles_dot_highlight(cycles: &[Cycle], all_edges: &AnnotatedEdges) -> String {
let mut cycle_edge_set: BTreeSet<(&str, &str)> = BTreeSet::new();
for cycle in cycles {
for (source, target) in cycle.edges.keys() {
cycle_edge_set.insert((source.as_str(), target.as_str()));
}
}
let mut cycle_nodes: BTreeSet<&str> = BTreeSet::new();
for cycle in cycles {
for module in &cycle.modules {
cycle_nodes.insert(module.as_str());
}
}
let mut out = String::new();
out.push_str("digraph dependencies {\n");
out.push_str(" rankdir=LR;\n");
out.push_str(" node [shape=box, style=rounded, fontname=\"monospace\", fontsize=10];\n");
out.push_str(" edge [color=\"#444444\"];\n");
write_cycle_subgraphs(&mut out, cycles);
let mut all_nodes: BTreeSet<&str> = BTreeSet::new();
for (source, target) in all_edges.keys() {
all_nodes.insert(source.as_str());
all_nodes.insert(target.as_str());
}
let non_cycle: Vec<&str> = all_nodes
.iter()
.filter(|n| !cycle_nodes.contains(**n))
.copied()
.collect();
if !non_cycle.is_empty() {
out.push('\n');
for node in &non_cycle {
let _ = writeln!(out, " \"{node}\";");
}
}
out.push('\n');
for ((source, target), apis) in all_edges {
let is_cycle = cycle_edge_set.contains(&(source.as_str(), target.as_str()));
let _ = write!(out, " \"{source}\" -> \"{target}\"");
if is_cycle {
out.push_str(" [color=\"#e67700\", style=bold, penwidth=2.0");
if !apis.is_empty() {
let items: Vec<&str> = apis.iter().map(String::as_str).collect();
let _ = write!(
out,
", label=\"{}\", fontcolor=\"#c96a00\"",
items.join(", ")
);
}
out.push(']');
} else if !apis.is_empty() {
let items: Vec<&str> = apis.iter().map(String::as_str).collect();
let _ = write!(
out,
" [label=\"{}\", fontcolor=\"#c96a00\"]",
items.join(", ")
);
}
out.push_str(";\n");
}
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn make_edges(pairs: &[(&str, &str)]) -> AnnotatedEdges {
pairs
.iter()
.map(|(s, t)| ((s.to_string(), t.to_string()), BTreeSet::new()))
.collect()
}
fn make_edges_with_apis(pairs: &[(&str, &str, &[&str])]) -> AnnotatedEdges {
pairs
.iter()
.map(|(s, t, apis)| {
let api_set: BTreeSet<String> = apis.iter().map(|a| (*a).to_owned()).collect();
((s.to_string(), t.to_string()), api_set)
})
.collect()
}
#[test]
fn render_plain_single_cycle() {
let cycles = vec![Cycle::new(
BTreeSet::from(["a".to_owned(), "b".to_owned()]),
make_edges(&[("a", "b"), ("b", "a")]),
)];
assert_eq!(
render_cycles_plain(&cycles),
"cycle 1 (2 modules)\n a -> b\n b -> a\n"
);
}
#[test]
fn render_plain_multiple_cycles() {
let cycles = vec![
Cycle::new(
BTreeSet::from(["a".to_owned(), "b".to_owned()]),
make_edges(&[("a", "b"), ("b", "a")]),
),
Cycle::new(
BTreeSet::from(["c".to_owned(), "d".to_owned()]),
make_edges(&[("c", "d"), ("d", "c")]),
),
];
let out = render_cycles_plain(&cycles);
assert!(out.contains("\n\ncycle 2"));
}
#[test]
fn render_plain_no_cycles() {
assert_eq!(render_cycles_plain(&[]), "");
}
#[test]
fn render_plain_with_apis() {
let cycles = vec![Cycle::new(
BTreeSet::from(["a".to_owned(), "b".to_owned()]),
make_edges_with_apis(&[("a", "b", &["FooType"]), ("b", "a", &["BarType"])]),
)];
let out = render_cycles_plain(&cycles);
assert!(out.contains("a -> b [FooType]"));
assert!(out.contains("b -> a [BarType]"));
}
#[test]
fn render_grouped_header_lists_modules() {
let cycles = vec![Cycle::new(
BTreeSet::from(["alpha".to_owned(), "beta".to_owned(), "gamma".to_owned()]),
make_edges(&[("alpha", "beta"), ("beta", "gamma"), ("gamma", "alpha")]),
)];
let out = render_cycles_grouped(&cycles);
assert!(out.contains("cycle 1 (3 modules): alpha, beta, gamma"));
}
#[test]
fn render_dot_detect_subgraph_clusters() {
let cycles = vec![Cycle::new(
BTreeSet::from(["a".to_owned(), "b".to_owned()]),
make_edges(&[("a", "b"), ("b", "a")]),
)];
let out = render_cycles_dot(&cycles);
assert!(out.contains("subgraph cluster_cycle_1"));
assert!(out.contains("color=\"#e67700\""));
assert!(out.contains("\"a\" -> \"b\" [color=\"#e67700\", style=bold, penwidth=2.0]"));
}
#[test]
fn render_dot_detect_empty() {
let out = render_cycles_dot(&[]);
assert!(out.starts_with("digraph dependencies {"));
assert!(out.ends_with("}\n"));
assert!(!out.contains("subgraph"));
}
#[test]
fn render_dot_highlight_all_edges_present() {
let all = make_edges(&[("a", "b"), ("b", "a"), ("c", "a")]);
let cycles = vec![Cycle::new(
BTreeSet::from(["a".to_owned(), "b".to_owned()]),
make_edges(&[("a", "b"), ("b", "a")]),
)];
let out = render_cycles_dot_highlight(&cycles, &all);
assert!(out.contains("\"a\" -> \"b\" [color=\"#e67700\", style=bold, penwidth=2.0]"));
assert!(out.contains("\"c\" -> \"a\""));
}
#[test]
fn render_dot_highlight_non_cycle_normal_color() {
let all = make_edges(&[("a", "b"), ("b", "a"), ("c", "a")]);
let cycles = vec![Cycle::new(
BTreeSet::from(["a".to_owned(), "b".to_owned()]),
make_edges(&[("a", "b"), ("b", "a")]),
)];
let out = render_cycles_dot_highlight(&cycles, &all);
let ca_line = out
.lines()
.find(|l| l.contains("\"c\" -> \"a\""))
.unwrap_or("");
assert!(!ca_line.contains("#cc0000"));
}
}