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 && 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 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 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 assert!(!reporter.check_version("1.0.0", "1.0.0")?);
304
305 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 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 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 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 }
360}