cargo_autodd/dependency_manager/
reporter.rs1use 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 let is_workspace = doc.get("workspace").is_some();
41
42 let deps_path = if is_workspace {
44 "workspace.dependencies"
45 } else {
46 "dependencies"
47 };
48
49 let deps = if deps_path.contains('.') {
51 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 let is_workspace = doc.get("workspace").is_some();
137
138 let deps_path = if is_workspace {
140 "workspace.dependencies"
141 } else {
142 "dependencies"
143 };
144
145 let deps = if deps_path.contains('.') {
147 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}