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                    && let Ok(latest) = self.updater.get_latest_version(name)
161                    && let Ok(true) = self.check_version(&version, &latest)
162                {
163                    outdated.push((name.to_string(), format!("{} -> {}", version, latest)));
164                }
165            }
166        }
167
168        Ok(outdated)
169    }
170
171    pub fn check_version(&self, version: &str, latest: &str) -> Result<bool> {
172        let current = Version::parse(Self::strip_version_prefix(version))?;
173        let latest_ver = Version::parse(Self::strip_version_prefix(latest))?;
174        Ok(latest_ver > current)
175    }
176
177    /// Strip version requirement prefixes (^, ~, =, >=, <=, >, <)
178    fn strip_version_prefix(version: &str) -> &str {
179        let version = version.trim();
180        if version.starts_with(">=") || version.starts_with("<=") {
181            &version[2..]
182        } else if version.starts_with('^')
183            || version.starts_with('~')
184            || version.starts_with('=')
185            || version.starts_with('>')
186            || version.starts_with('<')
187        {
188            &version[1..]
189        } else {
190            version
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use std::fs::File;
199    use std::io::Write;
200    use tempfile::TempDir;
201
202    fn create_test_environment() -> Result<(TempDir, PathBuf)> {
203        let temp_dir = TempDir::new()?;
204        let cargo_toml = temp_dir.path().join("Cargo.toml");
205
206        let content = r#"
207[package]
208name = "test-package"
209version = "0.1.0"
210edition = "2021"
211
212[dependencies]
213serde = "1.0"
214tokio = "1.0"
215"#;
216        let mut file = File::create(&cargo_toml)?;
217        writeln!(file, "{}", content)?;
218
219        Ok((temp_dir, cargo_toml))
220    }
221
222    fn create_workspace_test_environment() -> Result<(TempDir, PathBuf)> {
223        let temp_dir = TempDir::new()?;
224        let cargo_toml = temp_dir.path().join("Cargo.toml");
225
226        let content = r#"
227[workspace]
228members = ["crate1", "crate2"]
229
230[workspace.dependencies]
231serde = "1.0"
232tokio = "1.0"
233"#;
234        let mut file = File::create(&cargo_toml)?;
235        writeln!(file, "{}", content)?;
236
237        Ok((temp_dir, cargo_toml))
238    }
239
240    #[test]
241    fn test_generate_dependency_report() -> Result<()> {
242        let (temp_dir, _) = create_test_environment()?;
243        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
244
245        let mut crate_refs = HashMap::new();
246        let mut serde_ref = CrateReference::new("serde".to_string());
247        serde_ref.add_usage(temp_dir.path().join("src/main.rs"));
248        crate_refs.insert("serde".to_string(), serde_ref);
249
250        reporter.generate_dependency_report(&crate_refs)?;
251        Ok(())
252    }
253
254    #[test]
255    fn test_generate_workspace_dependency_report() -> Result<()> {
256        let (temp_dir, _) = create_workspace_test_environment()?;
257        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
258
259        let mut crate_refs = HashMap::new();
260        let mut serde_ref = CrateReference::new("serde".to_string());
261        serde_ref.add_usage(temp_dir.path().join("crate1/src/main.rs"));
262        crate_refs.insert("serde".to_string(), serde_ref);
263
264        reporter.generate_dependency_report(&crate_refs)?;
265        Ok(())
266    }
267
268    #[test]
269    fn test_generate_security_report() -> Result<()> {
270        let (temp_dir, _) = create_test_environment()?;
271        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
272        reporter.generate_security_report()?;
273        Ok(())
274    }
275
276    #[test]
277    fn test_generate_workspace_security_report() -> Result<()> {
278        let (temp_dir, _) = create_workspace_test_environment()?;
279        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
280        reporter.generate_security_report()?;
281        Ok(())
282    }
283
284    #[test]
285    fn test_check_version_update_available() -> Result<()> {
286        let (temp_dir, _) = create_test_environment()?;
287        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
288
289        // Newer version available
290        assert!(reporter.check_version("1.0.0", "1.1.0")?);
291        assert!(reporter.check_version("1.0.0", "2.0.0")?);
292        assert!(reporter.check_version("1.0.0", "1.0.1")?);
293
294        Ok(())
295    }
296
297    #[test]
298    fn test_check_version_up_to_date() -> Result<()> {
299        let (temp_dir, _) = create_test_environment()?;
300        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
301
302        // Same version
303        assert!(!reporter.check_version("1.0.0", "1.0.0")?);
304
305        // Current is newer (shouldn't happen in practice but test the logic)
306        assert!(!reporter.check_version("2.0.0", "1.0.0")?);
307        assert!(!reporter.check_version("1.1.0", "1.0.0")?);
308
309        Ok(())
310    }
311
312    #[test]
313    fn test_check_version_with_caret_prefix() -> Result<()> {
314        let (temp_dir, _) = create_test_environment()?;
315        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
316
317        // Caret prefix should be stripped
318        assert!(reporter.check_version("^1.0.0", "1.1.0")?);
319        assert!(reporter.check_version("^1.0.0", "^1.1.0")?);
320        assert!(!reporter.check_version("^1.0.0", "^1.0.0")?);
321
322        Ok(())
323    }
324
325    #[test]
326    fn test_check_version_with_tilde_prefix() -> Result<()> {
327        let (temp_dir, _) = create_test_environment()?;
328        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
329
330        // Tilde prefix should be stripped
331        assert!(reporter.check_version("~1.0.0", "1.1.0")?);
332        assert!(!reporter.check_version("~1.0.0", "~1.0.0")?);
333
334        Ok(())
335    }
336
337    #[test]
338    fn test_check_version_with_comparison_prefixes() -> Result<()> {
339        let (temp_dir, _) = create_test_environment()?;
340        let reporter = DependencyReporter::new(temp_dir.path().to_path_buf());
341
342        // Various comparison prefixes should be stripped
343        assert!(reporter.check_version("=1.0.0", "1.1.0")?);
344        assert!(reporter.check_version(">=1.0.0", "1.1.0")?);
345        assert!(reporter.check_version("<=1.0.0", "1.1.0")?);
346        assert!(reporter.check_version(">1.0.0", "1.1.0")?);
347        assert!(reporter.check_version("<1.0.0", "1.1.0")?);
348
349        Ok(())
350    }
351
352    #[test]
353    fn test_strip_version_prefix() {
354        // Test the private helper function behavior through check_version
355        // This test verifies that all prefixes are correctly handled
356
357        // The function is private, so we test it indirectly
358        // through the check_version method which uses it
359    }
360}