| use super::cfg::GraphHighlight; |
| use crate::{DepKind, Kid, Krate}; |
| use anyhow::{Context, Error}; |
| use krates::petgraph as pg; |
| use semver::Version; |
| use std::{ |
| borrow::Cow, |
| collections::{btree_map::Entry, BTreeMap, HashSet}, |
| fmt, |
| }; |
| |
| #[derive(Hash, Copy, Clone, PartialEq, Eq)] |
| struct Node<'a> { |
| name: &'a str, |
| version: &'a Version, |
| } |
| |
| impl<'a, 'b: 'a> From<&'b Krate> for Node<'a> { |
| fn from(d: &'b Krate) -> Self { |
| Self { |
| name: &d.name, |
| version: &d.version, |
| } |
| } |
| } |
| |
| impl<'a> fmt::Debug for Node<'a> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{}({})", self.name, self.version) |
| } |
| } |
| |
| impl<'a> fmt::Display for Node<'a> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{}({})", self.name, self.version) |
| } |
| } |
| |
| type Id = pg::graph::NodeIndex<u32>; |
| |
| #[allow(non_camel_case_types)] |
| #[derive(Debug)] |
| pub enum Shape { |
| r#box, |
| diamond, |
| } |
| |
| #[allow(non_camel_case_types)] |
| #[derive(Debug)] |
| pub enum Style { |
| rounded, |
| } |
| |
| #[derive(Default)] |
| struct NodeAttributes<'a> { |
| label: Option<Cow<'a, str>>, |
| shape: Option<Shape>, |
| style: Option<Style>, |
| color: Option<&'static str>, |
| fill_color: Option<&'static str>, |
| } |
| |
| impl<'a> NodeAttributes<'a> { |
| fn has_attrs(&self) -> bool { |
| self.label.is_some() |
| || self.shape.is_some() |
| || self.style.is_some() |
| || self.color.is_some() |
| || self.fill_color.is_some() |
| } |
| } |
| |
| #[derive(Default)] |
| struct EdgeAttributes<'a> { |
| color: Option<&'static str>, |
| label: Option<&'a str>, |
| } |
| |
| const INDENT: &str = " "; |
| |
| #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] |
| struct DupNode<'k> { |
| kid: &'k Kid, |
| feature: Option<&'k str>, |
| } |
| |
| pub(crate) fn create_graph( |
| dup_name: &str, |
| highlight: GraphHighlight, |
| krates: &crate::Krates, |
| dup_ids: &[usize], |
| ) -> Result<String, Error> { |
| use pg::visit::{EdgeRef, NodeRef}; |
| |
| let mut graph = pg::Graph::new(); |
| let mut node_map = BTreeMap::new(); |
| |
| let mut node_stack = Vec::with_capacity(dup_ids.len()); |
| |
| let duplicates: Vec<_> = dup_ids.iter().map(|di| krates[*di].id.clone()).collect(); |
| |
| for (index, dupid) in dup_ids.iter().zip(duplicates.iter()) { |
| let dn = DupNode { |
| kid: dupid, |
| feature: None, |
| }; |
| let nid = graph.add_node(dn); |
| node_map.insert(dn, nid); |
| node_stack.push((krates::NodeId::new(*index), nid)); |
| } |
| |
| { |
| let kg = krates.graph(); |
| let mut visited = HashSet::new(); |
| |
| if true { |
| while let Some((nid, target)) = node_stack.pop() { |
| for edge in kg.edges_directed(nid, pg::Direction::Incoming) { |
| match &kg[edge.source()] { |
| krates::Node::Krate { krate, .. } => { |
| if let krates::Edge::Dep { kind, .. } |
| | krates::Edge::DepFeature { kind, .. } = edge.weight() |
| { |
| let dn = DupNode { |
| kid: &krate.id, |
| feature: None, |
| }; |
| |
| if let Some(pindex) = node_map.get(&dn) { |
| graph.update_edge(*pindex, target, *kind); |
| } else { |
| let pindex = graph.add_node(DupNode { |
| kid: &krate.id, |
| feature: None, |
| }); |
| |
| graph.update_edge(pindex, target, *kind); |
| |
| node_map.insert(dn, pindex); |
| node_stack.push((edge.source(), pindex)); |
| } |
| } |
| } |
| krates::Node::Feature { krate_index, .. } => { |
| if *krate_index == nid && visited.insert(edge.source()) { |
| node_stack.push((edge.source(), target)); |
| } |
| } |
| } |
| } |
| } |
| } else { |
| while let Some((src_id, tar_id)) = node_stack.pop() { |
| let target = tar_id; |
| |
| for edge in kg.edges_directed(src_id, pg::Direction::Incoming) { |
| let source = edge.source(); |
| match &kg[source] { |
| krates::Node::Krate { krate, .. } => { |
| if let krates::Edge::Dep { kind, .. } |
| | krates::Edge::DepFeature { kind, .. } = edge.weight() |
| { |
| let node = DupNode { |
| kid: &krate.id, |
| feature: None, |
| }; |
| |
| if let Some(pindex) = node_map.get(&node) { |
| graph.update_edge(*pindex, target, *kind); |
| } else { |
| let pindex = graph.add_node(node); |
| |
| graph.update_edge(pindex, target, *kind); |
| |
| node_map.insert(node, pindex); |
| node_stack.push((source, pindex)); |
| } |
| } |
| } |
| krates::Node::Feature { krate_index, name } => { |
| let kid = &krates[*krate_index].id; |
| |
| let node = DupNode { |
| kid, |
| feature: Some(name.as_str()), |
| }; |
| |
| if let Some(pindex) = node_map.get(&node) { |
| graph.update_edge(*pindex, target, DepKind::Normal); |
| } else { |
| let pindex = graph.add_node(node); |
| |
| graph.update_edge(pindex, target, DepKind::Normal); |
| |
| node_map.insert(node, pindex); |
| node_stack.push((source, pindex)); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| let mut node_stack = Vec::new(); |
| let mut dupe_nodes = BTreeMap::<_, Vec<_>>::new(); |
| |
| let mut edge_sets = Vec::with_capacity(duplicates.len()); |
| |
| // Find all of the edges that lead to each duplicate, and also keep track of |
| // any additional crate duplicates, to make them stand out more in the dotgraph |
| for id in &duplicates { |
| let dup_node = node_map[&DupNode { |
| kid: id, |
| feature: None, |
| }]; |
| let mut set = HashSet::new(); |
| |
| node_stack.push(dup_node); |
| |
| while let Some(nid) = node_stack.pop() { |
| let node = &graph[nid]; |
| let name = node.kid.name(); |
| |
| match dupe_nodes.entry(name) { |
| Entry::Occupied(it) => { |
| let it = it.into_mut(); |
| if !it.contains(&nid) { |
| it.push(nid); |
| } |
| } |
| Entry::Vacant(it) => { |
| it.insert(vec![nid]); |
| } |
| } |
| |
| for edge in graph.edges_directed(nid, pg::Direction::Incoming) { |
| if set.insert(edge.id()) { |
| node_stack.push(edge.source()); |
| } |
| } |
| } |
| |
| edge_sets.push(set); |
| } |
| |
| dupe_nodes.retain(|_, v| { |
| v.sort(); |
| // Only keep the actual duplicates |
| v.len() > 1 |
| }); |
| |
| // Find the version with the least number of total edges to the least common ancestor, |
| // this will presumably be the easiest version to "fix" |
| // This just returns the first lowest one, there can be multiple with the |
| // same number of edges |
| let smollest = edge_sets |
| .iter() |
| .min_by(|a, b| a.len().cmp(&b.len())) |
| .context("expected shortest edge path")?; |
| |
| // The krates are ordered lexicographically by id, so the first duplicate |
| // is the one with the lowest version (or at least the lowest source...) |
| let lowest = &edge_sets[0]; |
| |
| print_graph( |
| &graph, |
| |node| { |
| let node_weight = node.weight(); |
| |
| if let Some(feat) = node_weight.feature { |
| NodeAttributes { |
| label: Some(feat.into()), |
| shape: Some(Shape::diamond), |
| ..Default::default() |
| } |
| } else { |
| let kid = node_weight.kid; |
| |
| let name = kid.name(); |
| let version = kid.version(); |
| let source = kid.source(); |
| |
| if dupe_nodes.contains_key(name) { |
| // Add the source only if it is not crates.io |
| let label = if source != "registry+https://github.com/rust-lang/crates.io-index" |
| { |
| format!("{version} {source}").into() |
| } else { |
| version.into() |
| }; |
| |
| NodeAttributes { |
| label: Some(label), |
| shape: Some(Shape::r#box), |
| color: Some("red"), |
| style: Some(Style::rounded), |
| ..Default::default() |
| } |
| } else { |
| NodeAttributes { |
| label: Some(format!("{name} {version}").into()), |
| shape: Some(Shape::r#box), |
| style: Some(Style::rounded), |
| ..Default::default() |
| } |
| } |
| } |
| }, |
| |edge| { |
| // Color edges if they are part of the lowest or smollest path, |
| // based on the graph highlighting configuration |
| let label = match edge.weight() { |
| DepKind::Normal => None, |
| DepKind::Dev => Some("dev"), |
| DepKind::Build => Some("build"), |
| }; |
| if highlight.simplest() && smollest.contains(&edge.id()) { |
| EdgeAttributes { |
| color: Some("red"), |
| label, |
| } |
| } else if highlight.lowest_version() && lowest.contains(&edge.id()) { |
| EdgeAttributes { |
| color: Some("blue"), |
| label, |
| } |
| } else { |
| EdgeAttributes { color: None, label } |
| } |
| }, |
| |output| { |
| use std::fmt::Write; |
| |
| for (i, (name, ids)) in dupe_nodes.iter().enumerate() { |
| writeln!(output, "{INDENT}subgraph cluster_{i} {{")?; |
| |
| write!(output, "{0}{0}{{rank=same ", INDENT)?; |
| |
| for nid in ids { |
| write!(output, "{} ", nid.index())?; |
| } |
| |
| writeln!( |
| output, |
| "}}\n{0}{0}style=\"rounded{1}\";\n{0}{0}label=\"{2}\"\n{0}}}", |
| INDENT, |
| if name == &dup_name { ",filled" } else { "" }, |
| name, |
| )?; |
| } |
| |
| Ok(()) |
| }, |
| ) |
| } |
| |
| fn print_graph<'a: 'b, 'b, NP, EP, SG>( |
| graph: &'a pg::Graph<DupNode<'a>, DepKind>, |
| node_print: NP, |
| edge_print: EP, |
| subgraphs: SG, |
| ) -> Result<String, Error> |
| where |
| NP: Fn((Id, &'b DupNode<'a>)) -> NodeAttributes<'b>, |
| EP: Fn(&pg::graph::EdgeReference<'_, DepKind, u32>) -> EdgeAttributes<'b>, |
| SG: Fn(&mut String) -> Result<(), Error>, |
| { |
| use pg::visit::{EdgeRef, IntoNodeReferences, NodeIndexable, NodeRef}; |
| use std::fmt::Write; |
| let mut output = String::with_capacity(1024); |
| writeln!(output, "digraph {{",)?; |
| |
| // output all nodes |
| for node in graph.node_references() { |
| write!(output, "{INDENT}{}", graph.to_index(node.id()))?; |
| |
| let attrs = node_print(node); |
| |
| if !attrs.has_attrs() { |
| writeln!(output)?; |
| continue; |
| } |
| |
| let mut append = false; |
| write!(output, " [")?; |
| |
| if let Some(label) = attrs.label { |
| write!(output, "label=\"{label}\"")?; |
| append = true; |
| } |
| |
| if let Some(shape) = attrs.shape { |
| write!(output, "{}shape={shape:?}", if append { ", " } else { "" },)?; |
| append = true; |
| } |
| |
| if let Some(style) = attrs.style { |
| write!(output, "{}style={style:?}", if append { ", " } else { "" },)?; |
| append = true; |
| } |
| |
| if let Some(color) = attrs.color { |
| write!(output, "{}color={color}", if append { ", " } else { "" })?; |
| append = true; |
| } |
| |
| if let Some(color) = attrs.fill_color { |
| write!( |
| output, |
| "{}fillcolor={color}", |
| if append { ", " } else { "" }, |
| )?; |
| } |
| |
| writeln!(output, "]")?; |
| } |
| |
| // output all edges |
| for edge in graph.edge_references() { |
| write!( |
| output, |
| "{INDENT}{} -> {}", |
| graph.to_index(edge.source()), |
| graph.to_index(edge.target()), |
| )?; |
| |
| let attrs = edge_print(&edge); |
| |
| write!(output, " [")?; |
| |
| let mut append = false; |
| |
| if let Some(label) = attrs.label { |
| write!(output, "label=\"{label}\"")?; |
| append = true; |
| } |
| |
| if let Some(color) = attrs.color { |
| write!(output, "{}color={color}", if append { ", " } else { "" })?; |
| //append = true; |
| } |
| |
| writeln!(output, "]")?; |
| } |
| |
| subgraphs(&mut output)?; |
| |
| writeln!(output, "}}")?; |
| Ok(output) |
| } |