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