cargo_autodd/dependency_manager/
analyzer.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use anyhow::Result;
6use regex::Regex;
7use walkdir::WalkDir;
8
9use crate::models::CrateReference;
10use crate::utils::is_std_crate;
11
12pub struct DependencyAnalyzer {
13    project_root: PathBuf,
14    debug: bool,
15}
16
17impl DependencyAnalyzer {
18    pub fn new(project_root: PathBuf) -> Self {
19        Self {
20            project_root,
21            debug: false,
22        }
23    }
24
25    pub fn with_debug(project_root: PathBuf, debug: bool) -> Self {
26        Self {
27            project_root,
28            debug,
29        }
30    }
31
32    pub fn analyze_dependencies(&self) -> Result<HashMap<String, CrateReference>> {
33        let mut crate_refs = HashMap::new();
34        let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
35        let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
36
37        // Walk through all Rust files in the project
38        for entry in WalkDir::new(&self.project_root) {
39            let entry = entry?;
40            let path = entry.path();
41
42            // Skip test files and build scripts
43            if path.to_string_lossy().contains("tests/")
44                || path.file_name().map_or(false, |f| f == "build.rs")
45            {
46                continue;
47            }
48
49            if path.extension().is_some_and(|ext| ext == "rs") {
50                let content = fs::read_to_string(path)?;
51                let file_path = path.to_path_buf();
52
53                self.analyze_file(FileAnalysisContext {
54                    content: &content,
55                    file_path: &file_path,
56                    use_regex: &use_regex,
57                    extern_regex: &extern_regex,
58                    crate_refs: &mut crate_refs,
59                })?;
60            }
61        }
62
63        // Filter out dev-dependencies and test-only crates
64        crate_refs.retain(|name, _| {
65            !name.ends_with("_test")
66                && !name.ends_with("_tests")
67                && name != "test"
68                && name != "tempfile"
69                && !name.starts_with("crate")
70        });
71
72        if self.debug {
73            println!("\nFinal crate references:");
74            for (name, crate_ref) in &crate_refs {
75                println!("- {} (used in {} files)", name, crate_ref.usage_count());
76                println!("  Used in:");
77                for path in &crate_ref.used_in {
78                    println!("    - {:?}", path);
79                }
80            }
81        }
82
83        Ok(crate_refs)
84    }
85
86    fn analyze_file(&self, ctx: FileAnalysisContext) -> Result<()> {
87        let FileAnalysisContext {
88            content,
89            file_path,
90            use_regex,
91            extern_regex,
92            crate_refs,
93        } = ctx;
94
95        for line in content.lines() {
96            let line = line.trim();
97            if line.is_empty() || line.starts_with("//") {
98                continue;
99            }
100
101            // Handle use statements
102            if let Some(cap) = use_regex.captures(line) {
103                let full_path = cap[1].to_string();
104                let parts: Vec<&str> = full_path.split("::").collect();
105
106                // Skip empty parts and special keywords
107                if parts.is_empty()
108                    || parts[0] == "self"
109                    || parts[0] == "super"
110                    || parts[0] == "crate"
111                {
112                    continue;
113                }
114
115                let base_crate = parts[0].to_string();
116
117                // Skip standard library types and modules
118                if !is_std_crate(&base_crate)
119                    && !base_crate.starts_with("std::")
120                    && !base_crate.starts_with("core::")
121                    && !base_crate.starts_with("alloc::")
122                {
123                    let crate_ref = crate_refs
124                        .entry(base_crate.clone())
125                        .or_insert_with(|| CrateReference::new(base_crate));
126                    crate_ref.add_usage(file_path.to_path_buf());
127                }
128            }
129
130            // Handle extern crate statements
131            if let Some(cap) = extern_regex.captures(line) {
132                let crate_name = cap[1].to_string();
133                if !is_std_crate(&crate_name) {
134                    let crate_ref = crate_refs
135                        .entry(crate_name.clone())
136                        .or_insert_with(|| CrateReference::new(crate_name));
137                    crate_ref.add_usage(file_path.to_path_buf());
138                }
139            }
140        }
141
142        Ok(())
143    }
144}
145
146struct FileAnalysisContext<'a> {
147    content: &'a str,
148    file_path: &'a PathBuf,
149    use_regex: &'a Regex,
150    extern_regex: &'a Regex,
151    crate_refs: &'a mut HashMap<String, CrateReference>,
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use std::fs::File;
158    use std::io::Write;
159    use tempfile::TempDir;
160
161    fn create_test_file(dir: &TempDir, name: &str, content: &str) -> Result<PathBuf> {
162        let path = dir.path().join(name);
163        let mut file = File::create(&path)?;
164        writeln!(file, "{}", content.trim())?;
165        Ok(path)
166    }
167
168    #[test]
169    fn test_analyze_dependencies() -> Result<()> {
170        let temp_dir = TempDir::new()?;
171
172        // Create test files with various import styles
173        let main_rs = create_test_file(
174            &temp_dir,
175            "main.rs",
176            r#"use serde::Serialize;
177               use tokio::runtime::Runtime;
178               use anyhow::Result;
179               use std::fs;"#,
180        )?;
181
182        let lib_rs = create_test_file(
183            &temp_dir,
184            "lib.rs",
185            r#"use serde::{Deserialize, Serialize};
186               use regex::Regex;
187               extern crate serde;"#,
188        )?;
189
190        // Debug output
191        println!("\nTest files created:");
192        println!("main.rs content:\n{}", fs::read_to_string(&main_rs)?);
193        println!("lib.rs content:\n{}", fs::read_to_string(&lib_rs)?);
194        println!("\nStarting analysis...\n");
195
196        let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
197        let crate_refs = analyzer.analyze_dependencies()?;
198
199        // Debug output
200        println!("\nAnalysis complete. Found crates:");
201        for (name, crate_ref) in &crate_refs {
202            println!("- {} (used in {} files)", name, crate_ref.usage_count());
203            println!("  Used in:");
204            for path in &crate_ref.used_in {
205                if let Ok(relative) = path.strip_prefix(temp_dir.path()) {
206                    println!("    - {}", relative.display());
207                }
208            }
209        }
210
211        assert!(
212            crate_refs.contains_key("serde"),
213            "serde dependency not found"
214        );
215        assert!(
216            crate_refs.contains_key("tokio"),
217            "tokio dependency not found"
218        );
219        assert!(
220            crate_refs.contains_key("anyhow"),
221            "anyhow dependency not found"
222        );
223        assert!(
224            crate_refs.contains_key("regex"),
225            "regex dependency not found"
226        );
227
228        let serde_ref = crate_refs.get("serde").unwrap();
229        assert_eq!(
230            serde_ref.usage_count(),
231            2,
232            "serde should be used in two files"
233        );
234
235        Ok(())
236    }
237
238    #[test]
239    fn test_analyze_file() -> Result<()> {
240        let temp_dir = TempDir::new()?;
241        let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
242        let file_path = temp_dir.path().join("test.rs");
243        let content = r#"use serde::Serialize;
244                       use tokio::runtime::Runtime;
245                       extern crate anyhow;
246                       use std::fs;"#;
247
248        println!("\nTest file content:\n{}", content);
249        println!("\nStarting analysis...\n");
250
251        let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
252        let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
253        let nested_regex = Regex::new(r"\{([^}]*)\}")?;
254        let item_regex = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)")?;
255        let mut crate_refs = HashMap::new();
256
257        analyzer.analyze_file(FileAnalysisContext {
258            content,
259            file_path: &file_path,
260            use_regex: &use_regex,
261            extern_regex: &extern_regex,
262            crate_refs: &mut crate_refs,
263        })?;
264
265        println!("\nAnalysis complete. Found crates:");
266        for (name, crate_ref) in &crate_refs {
267            println!("- {} (used in {} files)", name, crate_ref.usage_count());
268        }
269
270        assert!(
271            crate_refs.contains_key("serde"),
272            "serde dependency not found"
273        );
274        assert!(
275            crate_refs.contains_key("tokio"),
276            "tokio dependency not found"
277        );
278        assert!(
279            crate_refs.contains_key("anyhow"),
280            "anyhow dependency not found"
281        );
282
283        Ok(())
284    }
285}