Skip to main content

cargo_declared/
delta.rs

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