cargo_autodd/dependency_manager/
analyzer.rs

1use 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        // Use rust-analyzer CLI to analyze the project
41        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        // Walk through all Rust files in the project
53        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            // Handle use statements
116            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                // Handle nested imports
146                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            // Handle extern crate statements
171            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        // Create test files with various import styles
230        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        // Debug output
248        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        // Debug output
257        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}