1use crate::metadata::{DependencyInfo, ParsedMetadata};
2use serde::Serialize;
3use std::collections::{HashSet, VecDeque};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct DependencySets {
7 pub declared: Vec<DependencyInfo>,
8 pub compiled: Vec<DependencyInfo>,
9 pub delta: Vec<DeltaEntry>,
10 pub orphaned: Vec<DependencyInfo>,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
14pub struct DeltaEntry {
15 pub name: String,
16 pub version: Option<String>,
17 pub source: Option<String>,
18 pub via: String,
19}
20
21pub fn compute_sets(parsed: &ParsedMetadata) -> DependencySets {
22 let declared_names = parsed
23 .declared_deps
24 .iter()
25 .map(|dep| dep.name.as_str())
26 .collect::<HashSet<_>>();
27 let compiled_names = parsed
28 .compiled_deps
29 .iter()
30 .map(|dep| dep.name.as_str())
31 .collect::<HashSet<_>>();
32
33 let delta = parsed
34 .compiled_deps
35 .iter()
36 .filter(|dep| !declared_names.contains(dep.name.as_str()))
37 .map(|dep| DeltaEntry {
38 name: dep.name.clone(),
39 version: dep.version.clone(),
40 source: dep.source.clone(),
41 via: via_dependency(parsed, &dep.name),
42 })
43 .collect();
44
45 let orphaned = parsed
46 .declared_deps
47 .iter()
48 .filter(|dep| !compiled_names.contains(dep.name.as_str()))
49 .cloned()
50 .collect();
51
52 DependencySets {
53 declared: parsed.declared_deps.clone(),
54 compiled: parsed.compiled_deps.clone(),
55 delta,
56 orphaned,
57 }
58}
59
60fn via_dependency(parsed: &ParsedMetadata, target: &str) -> String {
61 for dep in &parsed.declared_deps {
62 if dep.name == target {
63 return dep.name.clone();
64 }
65
66 if reaches_target(parsed, &dep.name, target) {
67 return dep.name.clone();
68 }
69 }
70
71 "unknown".to_string()
72}
73
74fn reaches_target(parsed: &ParsedMetadata, start: &str, target: &str) -> bool {
75 let mut queue = VecDeque::from([start.to_string()]);
76 let mut visited = HashSet::new();
77
78 while let Some(current) = queue.pop_front() {
79 if !visited.insert(current.clone()) {
80 continue;
81 }
82
83 let Some(children) = parsed.package_graph.get(¤t) else {
84 continue;
85 };
86
87 if children.iter().any(|child| child == target) {
88 return true;
89 }
90
91 queue.extend(children.iter().cloned());
92 }
93
94 false
95}
96
97pub fn format_human(sets: &DependencySets) -> String {
98 let mut output = String::new();
99
100 output.push_str("cargo-declared v0.1.0\n\n");
101 output.push_str(&format!("declared: {}\n", sets.declared.len()));
102 output.push_str(&format!("compiled: {}\n", sets.compiled.len()));
103 output.push_str(&format!("delta: {}\n", sets.delta.len()));
104
105 if !sets.delta.is_empty() {
106 output.push_str(&format!("\n+ transitive ({})\n", sets.delta.len()));
107 for entry in &sets.delta {
108 output.push_str(&format!(
109 " {} {} via: {}\n",
110 entry.name,
111 entry.version.as_deref().unwrap_or("unknown"),
112 entry.via
113 ));
114 }
115 }
116
117 if !sets.orphaned.is_empty() {
118 output.push_str(&format!("\n~ orphaned ({})\n", sets.orphaned.len()));
119 for dep in &sets.orphaned {
120 output.push_str(&format!(" {}\n", dep.name));
121 }
122 }
123
124 output
125}
126
127pub fn format_json(sets: &DependencySets) -> Result<String, serde_json::Error> {
128 let json = serde_json::json!({
129 "declared": sets.declared,
130 "compiled": sets.compiled,
131 "delta": sets.delta,
132 "orphaned": sets.orphaned,
133 "summary": {
134 "declared_count": sets.declared.len(),
135 "compiled_count": sets.compiled.len(),
136 "delta_count": sets.delta.len(),
137 "orphaned_count": sets.orphaned.len()
138 }
139 });
140
141 serde_json::to_string_pretty(&json)
142}