cargo_autodd/dependency_manager/
analyzer.rs1use 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 for entry in WalkDir::new(&self.project_root) {
39 let entry = entry?;
40 let path = entry.path();
41
42 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 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 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 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 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 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 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 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 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}