Skip to main content

cargo_declared/
delta.rs

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