cargo_autodd/dependency_manager/
reporter.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use anyhow::Result;
6use semver::Version;
7use toml_edit::DocumentMut;
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        let doc = content.parse::<DocumentMut>()?;
35
36        println!("\nDependency Usage Report");
37        println!("=====================\n");
38
39        // Check if this is a workspace or a package
40        let is_workspace = doc.get("workspace").is_some();
41
42        // Determine the correct dependencies table (workspace or package)
43        let deps_path = if is_workspace {
44            "workspace.dependencies"
45        } else {
46            "dependencies"
47        };
48
49        // Get dependencies from the correct table
50        let deps = if deps_path.contains('.') {
51            // Handle nested table path like "workspace.dependencies"
52            let parts: Vec<&str> = deps_path.split('.').collect();
53            doc.get(parts[0])
54                .and_then(|t| t.as_table())
55                .and_then(|t| t.get(parts[1]))
56                .and_then(|t| t.as_table())
57        } else {
58            doc.get(deps_path).and_then(|t| t.as_table())
59        };
60
61        if let Some(deps) = deps {
62            for (name, dep) in deps.iter() {
63                println!("📦 {}", name);
64
65                if let Some(version) = self.updater.get_dependency_version(dep) {
66                    println!("  Version: {}", version);
67
68                    match self.updater.get_latest_version(name) {
69                        Ok(latest) => {
70                            if let Ok(needs_update) = self.check_version(&version, &latest) {
71                                if needs_update {
72                                    println!("  ⚠️ Update available: {} -> {}", version, latest);
73                                } else {
74                                    println!("  ✅ Up to date");
75                                }
76                            }
77                        }
78                        Err(e) => {
79                            println!("  ⚠️ Failed to check latest version: {}", e);
80                        }
81                    }
82                }
83
84                if let Some(crate_ref) = crate_refs.get(name) {
85                    println!("  Used in {} file(s)", crate_ref.usage_count());
86                    println!("  Usage locations:");
87                    for path in &crate_ref.used_in {
88                        if let Ok(relative) = path.strip_prefix(&self.project_root) {
89                            println!("    - {}", relative.display());
90                        }
91                    }
92                } else {
93                    println!("  ⚠️ Warning: No usage detected in the project");
94                }
95                println!();
96            }
97        } else {
98            println!("⚠️ No dependencies found in the {} table", deps_path);
99        }
100
101        Ok(())
102    }
103
104    pub fn generate_security_report(&self) -> Result<()> {
105        println!("\nDependency Security Report");
106        println!("========================\n");
107
108        let outdated = self.check_security()?;
109
110        if outdated.is_empty() {
111            println!("✅ All dependencies are up to date.");
112            return Ok(());
113        }
114
115        println!("⚠️ The following dependencies have updates available:\n");
116
117        for (name, version_info) in outdated {
118            println!("📦 {}", name);
119            println!("  Version update available: {}", version_info);
120            println!();
121        }
122
123        println!("Note: For a complete security audit, please use:");
124        println!("  cargo audit");
125        println!("  https://github.com/rustsec/rustsec\n");
126
127        Ok(())
128    }
129
130    fn check_security(&self) -> Result<Vec<(String, String)>> {
131        let content = fs::read_to_string(&self.cargo_toml)?;
132        let doc = content.parse::<DocumentMut>()?;
133        let mut outdated = Vec::new();
134
135        // Check if this is a workspace or a package
136        let is_workspace = doc.get("workspace").is_some();
137
138        // Determine the correct dependencies table (workspace or package)
139        let deps_path = if is_workspace {
140            "workspace.dependencies"
141        } else {
142            "dependencies"
143        };
144
145        // Get dependencies from the correct table
146        let deps = if deps_path.contains('.') {
147            // Handle nested table path like "workspace.dependencies"
148            let parts: Vec<&str> = deps_path.split('.').collect();
149            doc.get(parts[0])
150                .and_then(|t| t.as_table())
151                .and_then(|t| t.get(parts[1]))
152                .and_then(|t| t.as_table())
153        } else {
154            doc.get(deps_path).and_then(|t| t.as_table())
155        };
156
157        if let Some(deps) = deps {
158            for (name, dep) in deps.iter() {
159                if let Some(version) = self.updater.get_dependency_version(dep) {
160                    if let Ok(latest) = self.updater.get_latest_version(name) {
161                        if let Ok(true) = self.check_version(&version, &latest) {
162                            outdated.push((name.to_string(), format!("{} -> {}", version, latest)));
163                        }
164                    }
165                }
166            }
167        }
168
169        Ok(outdated)
170    }
171
172    pub fn check_version(&self, version: &str, latest: &str) -> Result<bool> {
173        let current = Version::parse(version.trim_start_matches('^'))?;
174        let latest_ver = Version::parse(latest.trim_start_matches('^'))?;
175        Ok(latest_ver > current)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use std::fs::File;
183    use std::io::Write;
184    use tempfile::TempDir;
185
186    fn create_test_environment() -> Result<(TempDir, PathBuf)> {
187        let temp_dir = TempDir::new()?;
188        let cargo_toml = temp_dir.path().join("Cargo.toml");
189
190        let content = r#"
191[package]
192name = "test-package"
193version = "0.1.0"
194edition = "2021"
195
196[dependencies]
197serde = "1.0"
198tokio = "1.0"
199"#;
200        let mut file = File::create(&cargo_toml)?;
201        writeln!(file, "{}", content)?;
202
203        Ok((temp_dir, cargo_toml))
204    }
205
206    fn create_workspace_test_environment() -> Result<(TempDir, PathBuf)> {
207        let temp_dir = TempDir::new()?;
208        let cargo_toml = temp_dir.path().join("Cargo.toml");
209
210        let content = r#"
211[workspace]
212members = ["crate1", "crate2"]
213
214[workspace.dependencies]
215serde = "1.0"
216tokio = "1.0"
217"#;
218        let mut file = File::create(&cargo_toml)?;
219        writeln!(file, "{}", content)?;
220
221        Ok((temp_dir, cargo_toml))
222    }
223
224    #[test]
225    fn test_generate_dependency_report() -> Result<()> {
226        let (temp_dir, _) = create_test_environment()?;
227        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
228
229        let mut crate_refs = HashMap::new();
230        let mut serde_ref = CrateReference::new("serde".to_string());
231        serde_ref.add_usage(temp_dir.path().join("src/main.rs"));
232        crate_refs.insert("serde".to_string(), serde_ref);
233
234        reporter.generate_dependency_report(&crate_refs)?;
235        Ok(())
236    }
237
238    #[test]
239    fn test_generate_workspace_dependency_report() -> Result<()> {
240        let (temp_dir, _) = create_workspace_test_environment()?;
241        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
242
243        let mut crate_refs = HashMap::new();
244        let mut serde_ref = CrateReference::new("serde".to_string());
245        serde_ref.add_usage(temp_dir.path().join("crate1/src/main.rs"));
246        crate_refs.insert("serde".to_string(), serde_ref);
247
248        reporter.generate_dependency_report(&crate_refs)?;
249        Ok(())
250    }
251
252    #[test]
253    fn test_generate_security_report() -> Result<()> {
254        let (temp_dir, _) = create_test_environment()?;
255        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
256        reporter.generate_security_report()?;
257        Ok(())
258    }
259
260    #[test]
261    fn test_generate_workspace_security_report() -> Result<()> {
262        let (temp_dir, _) = create_workspace_test_environment()?;
263        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
264        reporter.generate_security_report()?;
265        Ok(())
266    }
267}