cargo_autodd/dependency_manager/
analyzer.rs1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4use std::process::Command;
5
6use anyhow::{Context, Result};
7use regex::Regex;
8use walkdir::WalkDir;
9
10use crate::models::CrateReference;
11use crate::utils::is_std_crate;
12
13pub struct DependencyAnalyzer {
14 project_root: PathBuf,
15 debug: bool,
16}
17
18impl DependencyAnalyzer {
19 pub fn new(project_root: PathBuf) -> Self {
20 Self {
21 project_root,
22 debug: false,
23 }
24 }
25
26 pub fn with_debug(project_root: PathBuf, debug: bool) -> Self {
27 Self {
28 project_root,
29 debug,
30 }
31 }
32
33 pub fn analyze_dependencies(&self) -> Result<HashMap<String, CrateReference>> {
34 let mut crate_refs = HashMap::new();
35 let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
36 let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
37 let nested_regex = Regex::new(r"\{([^}]*)\}")?;
38 let item_regex = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)")?;
39
40 let output = Command::new("rust-analyzer")
42 .arg("analysis")
43 .arg("--workspace")
44 .current_dir(&self.project_root)
45 .output()
46 .context("Failed to run rust-analyzer. Is it installed?")?;
47
48 if !output.status.success() {
49 println!("Warning: rust-analyzer analysis returned non-zero status. Falling back to regex-based analysis.");
50 }
51
52 for entry in WalkDir::new(&self.project_root) {
54 let entry = entry?;
55 let path = entry.path();
56
57 if path.extension().is_some_and(|ext| ext == "rs") {
58 if self.debug {
59 println!("Found Rust file: {:?}", path);
60 }
61 let content = fs::read_to_string(path)?;
62 let file_path = path.to_path_buf();
63
64 self.analyze_file(FileAnalysisContext {
65 content: &content,
66 file_path: &file_path,
67 use_regex: &use_regex,
68 extern_regex: &extern_regex,
69 nested_regex: &nested_regex,
70 item_regex: &item_regex,
71 crate_refs: &mut crate_refs,
72 })?;
73 }
74 }
75
76 if self.debug {
77 println!("\nFinal crate references:");
78 for (name, crate_ref) in &crate_refs {
79 println!("- {} (used in {} files)", name, crate_ref.usage_count());
80 println!(" Used in:");
81 for path in &crate_ref.used_in {
82 println!(" - {:?}", path);
83 }
84 }
85 }
86
87 Ok(crate_refs)
88 }
89
90 fn analyze_file(&self, ctx: FileAnalysisContext) -> Result<()> {
91 if self.debug {
92 println!("Analyzing file: {:?}", ctx.file_path);
93 }
94
95 let FileAnalysisContext {
96 content,
97 file_path,
98 use_regex,
99 extern_regex,
100 nested_regex,
101 item_regex,
102 crate_refs,
103 } = ctx;
104
105 for line in content.lines() {
106 let line = line.trim();
107 if line.is_empty() || line.starts_with("//") {
108 continue;
109 }
110
111 if self.debug {
112 println!("Processing line: {}", line);
113 }
114
115 if let Some(cap) = use_regex.captures(line) {
117 let full_path = cap[1].to_string();
118 let base_crate = full_path
119 .split("::")
120 .next()
121 .unwrap_or(&full_path)
122 .to_string();
123
124 if self.debug {
125 println!(
126 "Found use statement: {} -> base crate: {}",
127 full_path, base_crate
128 );
129 }
130
131 if !is_std_crate(&base_crate) &&
132 !base_crate.starts_with("std::") &&
133 !base_crate.starts_with("core::") &&
134 !base_crate.starts_with("alloc::") {
135 let crate_ref = crate_refs
136 .entry(base_crate.clone())
137 .or_insert_with(|| CrateReference::new(base_crate.clone()));
138 crate_ref.add_usage(file_path.clone());
139
140 if self.debug {
141 println!("Added crate reference: {}", crate_ref.name);
142 }
143 }
144
145 if let Some(nested) = nested_regex.captures(line) {
147 if let Some(items) = nested.get(1) {
148 for item in item_regex.captures_iter(items.as_str()) {
149 let item_name = item[1].to_string();
150 if self.debug {
151 println!("Processing nested item: {}", item_name);
152 }
153
154 if !is_std_crate(&item_name) &&
155 !item_name.starts_with("std::") {
156 let crate_ref = crate_refs
157 .entry(item_name.clone())
158 .or_insert_with(|| CrateReference::new(item_name.clone()));
159 crate_ref.add_usage(file_path.clone());
160
161 if self.debug {
162 println!("Added nested crate reference: {}", crate_ref.name);
163 }
164 }
165 }
166 }
167 }
168 }
169
170 if let Some(cap) = extern_regex.captures(line) {
172 let crate_name = cap[1].to_string();
173 if self.debug {
174 println!("Found extern crate: {}", crate_name);
175 }
176
177 if !is_std_crate(&crate_name) {
178 let crate_ref = crate_refs
179 .entry(crate_name.clone())
180 .or_insert_with(|| CrateReference::new(crate_name.clone()));
181 crate_ref.add_usage(file_path.clone());
182
183 if self.debug {
184 println!("Added extern crate reference: {}", crate_ref.name);
185 }
186 }
187 }
188 }
189
190 if self.debug {
191 println!("Current crate references:");
192 for (name, crate_ref) in crate_refs {
193 println!("- {} (used in {} files)", name, crate_ref.usage_count());
194 }
195 }
196
197 Ok(())
198 }
199}
200
201struct FileAnalysisContext<'a> {
202 content: &'a str,
203 file_path: &'a PathBuf,
204 use_regex: &'a Regex,
205 extern_regex: &'a Regex,
206 nested_regex: &'a Regex,
207 item_regex: &'a Regex,
208 crate_refs: &'a mut HashMap<String, CrateReference>,
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use std::fs::File;
215 use std::io::Write;
216 use tempfile::TempDir;
217
218 fn create_test_file(dir: &TempDir, name: &str, content: &str) -> Result<PathBuf> {
219 let path = dir.path().join(name);
220 let mut file = File::create(&path)?;
221 writeln!(file, "{}", content.trim())?;
222 Ok(path)
223 }
224
225 #[test]
226 fn test_analyze_dependencies() -> Result<()> {
227 let temp_dir = TempDir::new()?;
228
229 let main_rs = create_test_file(
231 &temp_dir,
232 "main.rs",
233 r#"use serde::Serialize;
234 use tokio::runtime::Runtime;
235 use anyhow::Result;
236 use std::fs;"#,
237 )?;
238
239 let lib_rs = create_test_file(
240 &temp_dir,
241 "lib.rs",
242 r#"use serde::{Deserialize, Serialize};
243 use regex::Regex;
244 extern crate serde;"#,
245 )?;
246
247 println!("\nTest files created:");
249 println!("main.rs content:\n{}", fs::read_to_string(&main_rs)?);
250 println!("lib.rs content:\n{}", fs::read_to_string(&lib_rs)?);
251 println!("\nStarting analysis...\n");
252
253 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
254 let crate_refs = analyzer.analyze_dependencies()?;
255
256 println!("\nAnalysis complete. Found crates:");
258 for (name, crate_ref) in &crate_refs {
259 println!("- {} (used in {} files)", name, crate_ref.usage_count());
260 println!(" Used in:");
261 for path in &crate_ref.used_in {
262 if let Ok(relative) = path.strip_prefix(temp_dir.path()) {
263 println!(" - {}", relative.display());
264 }
265 }
266 }
267
268 assert!(
269 crate_refs.contains_key("serde"),
270 "serde dependency not found"
271 );
272 assert!(
273 crate_refs.contains_key("tokio"),
274 "tokio dependency not found"
275 );
276 assert!(
277 crate_refs.contains_key("anyhow"),
278 "anyhow dependency not found"
279 );
280 assert!(
281 crate_refs.contains_key("regex"),
282 "regex dependency not found"
283 );
284
285 let serde_ref = crate_refs.get("serde").unwrap();
286 assert_eq!(
287 serde_ref.usage_count(),
288 2,
289 "serde should be used in two files"
290 );
291
292 Ok(())
293 }
294
295 #[test]
296 fn test_analyze_file() -> Result<()> {
297 let temp_dir = TempDir::new()?;
298 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
299 let file_path = temp_dir.path().join("test.rs");
300 let content = r#"use serde::Serialize;
301 use tokio::runtime::Runtime;
302 extern crate anyhow;
303 use std::fs;"#;
304
305 println!("\nTest file content:\n{}", content);
306 println!("\nStarting analysis...\n");
307
308 let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
309 let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
310 let nested_regex = Regex::new(r"\{([^}]*)\}")?;
311 let item_regex = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)")?;
312 let mut crate_refs = HashMap::new();
313
314 analyzer.analyze_file(FileAnalysisContext {
315 content,
316 file_path: &file_path,
317 use_regex: &use_regex,
318 extern_regex: &extern_regex,
319 nested_regex: &nested_regex,
320 item_regex: &item_regex,
321 crate_refs: &mut crate_refs,
322 })?;
323
324 println!("\nAnalysis complete. Found crates:");
325 for (name, crate_ref) in &crate_refs {
326 println!("- {} (used in {} files)", name, crate_ref.usage_count());
327 }
328
329 assert!(
330 crate_refs.contains_key("serde"),
331 "serde dependency not found"
332 );
333 assert!(
334 crate_refs.contains_key("tokio"),
335 "tokio dependency not found"
336 );
337 assert!(
338 crate_refs.contains_key("anyhow"),
339 "anyhow dependency not found"
340 );
341
342 Ok(())
343 }
344}