Skip to main content

cargo_declared/
delta.rs

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(&current) 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}