1use crate::metadata::{dependency_key, DependencyInfo, ParsedMetadata};
2use serde::Serialize;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::env;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7pub struct DependencySets {
8 pub declared: Vec<DependencyInfo>,
9 pub compiled: Vec<DependencyInfo>,
10 pub delta: Vec<DeltaEntry>,
11 pub orphaned: Vec<DependencyInfo>,
12 pub summary: Summary,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct DeltaEntry {
17 pub name: String,
18 pub version: Option<String>,
19 pub source: Option<String>,
20 pub via: String,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24pub struct Summary {
25 pub declared_count: usize,
26 pub compiled_count: usize,
27 pub delta_count: usize,
28 pub orphaned_count: usize,
29}
30
31pub fn compute_sets(parsed: &ParsedMetadata) -> DependencySets {
32 let declared_ids = parsed
33 .declared_dep_ids
34 .iter()
35 .filter_map(|id| id.as_ref())
36 .collect::<HashSet<_>>();
37 let compiled_ids = parsed
38 .compiled_deps
39 .iter()
40 .filter_map(|dep| dep_package_id(parsed, dep))
41 .collect::<HashSet<_>>();
42 let predecessors = shortest_predecessors(parsed);
43
44 let mut delta = parsed
45 .compiled_deps
46 .iter()
47 .filter(|dep| dep_package_id(parsed, dep).is_some_and(|id| !declared_ids.contains(id)))
48 .map(|dep| DeltaEntry {
49 name: dep.name.clone(),
50 version: dep.version.clone(),
51 source: dep.source.clone(),
52 via: via_dependency(parsed, &predecessors, dep),
53 })
54 .collect::<Vec<_>>();
55 delta.sort_by(|a, b| {
56 a.name.cmp(&b.name).then_with(|| {
57 a.version
58 .as_deref()
59 .unwrap_or("")
60 .cmp(b.version.as_deref().unwrap_or(""))
61 .then_with(|| a.source.cmp(&b.source))
62 })
63 });
64
65 let mut orphaned = parsed
66 .declared_deps
67 .iter()
68 .zip(parsed.declared_dep_ids.iter())
69 .filter(|(_, package_id)| {
70 package_id
71 .as_ref()
72 .map(|id| !compiled_ids.contains(id))
73 .unwrap_or(true)
74 })
75 .map(|(dep, _)| dep.clone())
76 .collect::<Vec<_>>();
77 orphaned.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.version.cmp(&b.version)));
78
79 let summary = Summary {
80 declared_count: parsed.declared_deps.len(),
81 compiled_count: parsed.compiled_deps.len(),
82 delta_count: delta.len(),
83 orphaned_count: orphaned.len(),
84 };
85
86 DependencySets {
87 declared: parsed.declared_deps.clone(),
88 compiled: parsed.compiled_deps.clone(),
89 delta,
90 orphaned,
91 summary,
92 }
93}
94
95fn shortest_predecessors(parsed: &ParsedMetadata) -> HashMap<String, String> {
96 let mut queue = VecDeque::from([parsed.root_package_id.clone()]);
97 let mut visited = HashSet::new();
98 let mut predecessors = HashMap::new();
99
100 while let Some(current) = queue.pop_front() {
101 if !visited.insert(current.clone()) {
102 continue;
103 }
104
105 let Some(children) = parsed.package_graph.get(¤t) else {
106 continue;
107 };
108
109 for child in children {
110 if !predecessors.contains_key(child) {
111 predecessors.insert(child.clone(), current.clone());
112 }
113 queue.push_back(child.clone());
114 }
115 }
116
117 predecessors
118}
119
120fn via_dependency(
121 parsed: &ParsedMetadata,
122 predecessors: &HashMap<String, String>,
123 dep: &DependencyInfo,
124) -> String {
125 let Some(package_id) = dep_package_id(parsed, dep) else {
126 return "unknown".to_string();
127 };
128 let Some(predecessor_id) = predecessors.get(package_id) else {
129 return "unknown".to_string();
130 };
131
132 parsed
133 .package_names
134 .get(predecessor_id)
135 .cloned()
136 .unwrap_or_else(|| "unknown".to_string())
137}
138
139fn dep_package_id<'a>(parsed: &'a ParsedMetadata, dep: &DependencyInfo) -> Option<&'a String> {
140 parsed.compiled_dep_ids.get(&dependency_key(
141 &dep.name,
142 dep.version.as_deref(),
143 dep.source.as_deref(),
144 ))
145}
146
147pub fn format_human(sets: &DependencySets) -> String {
148 let mut output = String::new();
149
150 output.push_str(&format!(
151 "cargo-declared v{}\n\n",
152 env!("CARGO_PKG_VERSION")
153 ));
154 output.push_str(&format!("declared: {}\n", sets.declared.len()));
155 output.push_str(&format!("compiled: {}\n", sets.compiled.len()));
156 output.push_str(&format!("delta: {}\n", sets.delta.len()));
157
158 if !sets.delta.is_empty() {
159 output.push_str(&format!("\n+ transitive ({})\n", sets.delta.len()));
160 for entry in &sets.delta {
161 output.push_str(&format!(
162 " {} {} via: {}\n",
163 entry.name,
164 entry.version.as_deref().unwrap_or("unknown"),
165 entry.via
166 ));
167 }
168 }
169
170 output.push_str(&format!("\n~ orphaned ({})\n", sets.orphaned.len()));
171 if sets.orphaned.is_empty() {
172 output.push_str(" none\n");
173 } else {
174 for dep in &sets.orphaned {
175 output.push_str(&format!(
176 " {} {}\n",
177 dep.name,
178 dep.version.as_deref().unwrap_or("unknown")
179 ));
180 }
181 }
182
183 output
184}
185
186pub fn format_json(sets: &DependencySets) -> Result<String, serde_json::Error> {
187 serde_json::to_string_pretty(&sets)
188}