cargo_autodd/dependency_manager/
reporter.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use semver::Version;
7use toml_edit::{DocumentMut, Item};
8
9use crate::dependency_manager::updater::DependencyUpdater;
10use crate::models::CrateReference;
11
12pub struct DependencyReporter {
13    project_root: PathBuf,
14    cargo_toml: PathBuf,
15    updater: DependencyUpdater,
16}
17
18impl DependencyReporter {
19    pub fn new(project_root: PathBuf) -> Self {
20        let cargo_toml = project_root.join("Cargo.toml");
21        let updater = DependencyUpdater::new(project_root.clone());
22        Self {
23            project_root,
24            cargo_toml,
25            updater,
26        }
27    }
28
29    pub fn generate_dependency_report(
30        &self,
31        crate_refs: &HashMap<String, CrateReference>,
32    ) -> Result<()> {
33        let content = fs::read_to_string(&self.cargo_toml)
34            .context("Failed to read Cargo.toml")?;
35        let doc = content.parse::<DocumentMut>()
36            .context("Failed to parse Cargo.toml")?;
37
38        println!("\nDependency Usage Report");
39        println!("=====================\n");
40
41        if let Some(Item::Table(deps)) = doc.get("dependencies") {
42            for (name, dep) in deps.iter() {
43                println!("📦 {}", name);
44
45                if let Some(version) = self.updater.get_dependency_version(dep) {
46                    println!("  Version: {}", version);
47
48                    if let Ok(latest) = self.updater.get_latest_version(name) {
49                        if let (Ok(current), Ok(latest_ver)) = (
50                            Version::parse(version.trim_start_matches('^')),
51                            Version::parse(latest.trim_start_matches('^')),
52                        ) {
53                            if latest_ver > current {
54                                println!("  Update available: {} -> {}", version, latest);
55                            }
56                        }
57                    }
58                }
59
60                if let Some(crate_ref) = crate_refs.get(name) {
61                    println!("  Used in {} file(s)", crate_ref.usage_count());
62                    println!("  Usage locations:");
63                    for path in &crate_ref.used_in {
64                        if let Ok(relative) = path.strip_prefix(&self.project_root) {
65                            println!("    - {}", relative.display());
66                        }
67                    }
68                } else {
69                    println!("  ⚠️ Warning: No usage detected in the project");
70                }
71                println!();
72            }
73        }
74
75        Ok(())
76    }
77
78    pub fn generate_security_report(&self) -> Result<()> {
79        println!("\nDependency Security Report");
80        println!("========================\n");
81
82        let outdated = self.check_security()?;
83
84        if outdated.is_empty() {
85            println!("✅ All dependencies are up to date.");
86            return Ok(());
87        }
88
89        println!("⚠️ The following dependencies have updates available:\n");
90
91        for (name, version_info) in outdated {
92            println!("📦 {}", name);
93            println!("  Version update available: {}", version_info);
94            println!("  Please check the changelog for security fixes.");
95            println!();
96        }
97
98        println!("Note: For a complete security audit, please use:");
99        println!("  cargo audit");
100        println!("  https://github.com/rustsec/rustsec\n");
101
102        Ok(())
103    }
104
105    fn check_security(&self) -> Result<Vec<(String, String)>> {
106        let content = fs::read_to_string(&self.cargo_toml)
107            .context("Failed to read Cargo.toml")?;
108        let doc = content.parse::<DocumentMut>()
109            .context("Failed to parse Cargo.toml")?;
110        let mut outdated = Vec::new();
111
112        if let Some(Item::Table(deps)) = doc.get("dependencies") {
113            for (name, dep) in deps.iter() {
114                if let Some(version) = self.updater.get_dependency_version(dep) {
115                    if let Ok(latest) = self.updater.get_latest_version(name) {
116                        let current = Version::parse(version.trim_start_matches('^'))
117                            .unwrap_or_else(|_| Version::new(0, 0, 0));
118                        let latest = Version::parse(latest.trim_start_matches('^'))
119                            .unwrap_or_else(|_| Version::new(0, 0, 0));
120
121                        if latest > current {
122                            outdated.push((name.to_string(), format!("{} -> {}", version, latest)));
123                        }
124                    }
125                }
126            }
127        }
128
129        Ok(outdated)
130    }
131
132    pub fn check_version(&self, version: &str, latest: &str) -> Result<bool> {
133        Ok(
134            match (
135                Version::parse(version.trim_start_matches('^')),
136                Version::parse(latest.trim_start_matches('^')),
137            ) {
138                (Ok(current), Ok(latest_ver)) => latest_ver > current,
139                _ => false,
140            },
141        )
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use std::fs::File;
149    use std::io::Write;
150    use tempfile::TempDir;
151
152    fn create_test_environment() -> Result<(TempDir, PathBuf)> {
153        let temp_dir = TempDir::new()?;
154        let cargo_toml = temp_dir.path().join("Cargo.toml");
155
156        let content = r#"
157[package]
158name = "test-package"
159version = "0.1.0"
160edition = "2021"
161
162[dependencies]
163serde = "1.0"
164tokio = "1.0"
165"#;
166        let mut file = File::create(&cargo_toml)?;
167        writeln!(file, "{}", content)?;
168
169        Ok((temp_dir, cargo_toml))
170    }
171
172    #[test]
173    fn test_generate_dependency_report() -> Result<()> {
174        let (temp_dir, _) = create_test_environment()?;
175        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
176
177        let mut crate_refs = HashMap::new();
178        let mut serde_ref = CrateReference::new("serde".to_string());
179        serde_ref.add_usage(temp_dir.path().join("src/main.rs"));
180        crate_refs.insert("serde".to_string(), serde_ref);
181
182        reporter.generate_dependency_report(&crate_refs)?;
183        Ok(())
184    }
185
186    #[test]
187    fn test_generate_security_report() -> Result<()> {
188        let (temp_dir, _) = create_test_environment()?;
189        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
190        reporter.generate_security_report()?;
191        Ok(())
192    }
193}