cargo_autodd/dependency_manager/
analyzer.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use regex::Regex;
7use toml_edit::{DocumentMut, Item};
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
38        // 既存のCargo.tomlから内部クレート情報を読み取る
39        self.load_existing_dependencies(&mut crate_refs)?;
40
41        // Walk through all Rust files in the project
42        for entry in WalkDir::new(&self.project_root) {
43            let entry = entry?;
44            let path = entry.path();
45
46            // Skip test files and build scripts
47            if path.to_string_lossy().contains("tests/")
48                || path.file_name().is_some_and(|f| f == "build.rs")
49            {
50                continue;
51            }
52
53            if path.extension().is_some_and(|ext| ext == "rs") {
54                let content = fs::read_to_string(path)?;
55                let file_path = path.to_path_buf();
56
57                self.analyze_file(FileAnalysisContext {
58                    content: content.trim().to_string(),
59                    file_path: &file_path,
60                    use_regex: &use_regex,
61                    extern_regex: &extern_regex,
62                    crate_refs: &mut crate_refs,
63                })?;
64            }
65        }
66
67        // Filter out dev-dependencies and test-only crates
68        crate_refs.retain(|name, _| {
69            !name.ends_with("_test")
70                && !name.ends_with("_tests")
71                && name != "test"
72                && name != "tempfile"
73                && !name.starts_with("crate")
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                if crate_ref.is_path_dependency {
81                    println!(
82                        "  Path dependency: {}",
83                        crate_ref.path.as_ref().unwrap_or(&"unknown".to_string())
84                    );
85                }
86                if let Some(publish) = crate_ref.publish {
87                    println!("  Publish: {}", publish);
88                }
89                println!("  Used in:");
90                for path in &crate_ref.used_in {
91                    println!("    - {:?}", path);
92                }
93            }
94        }
95
96        Ok(crate_refs)
97    }
98
99    /// Load existing dependency information from Cargo.toml
100    fn load_existing_dependencies(
101        &self,
102        crate_refs: &mut HashMap<String, CrateReference>,
103    ) -> Result<()> {
104        let cargo_toml_path = self.project_root.join("Cargo.toml");
105        if !cargo_toml_path.exists() {
106            return Ok(());
107        }
108
109        if self.debug {
110            println!("Loading dependencies from {:?}", cargo_toml_path);
111        }
112
113        let content = fs::read_to_string(&cargo_toml_path)
114            .with_context(|| format!("Failed to read Cargo.toml at {:?}", cargo_toml_path))?;
115        let doc = content
116            .parse::<DocumentMut>()
117            .with_context(|| format!("Failed to parse Cargo.toml at {:?}", cargo_toml_path))?;
118
119        // Check package publish settings
120        let publish = if let Some(package) = doc.get("package") {
121            if let Some(publish_value) = package.get("publish") {
122                publish_value.as_bool()
123            } else {
124                None
125            }
126        } else {
127            None
128        };
129
130        if self.debug {
131            println!("Package publish setting: {:?}", publish);
132        }
133
134        // Load dependencies
135        if let Some(dependencies) = doc.get("dependencies").and_then(|d| d.as_table()) {
136            for (name, value) in dependencies.iter() {
137                let crate_name = name.to_string();
138
139                if self.debug {
140                    println!("Found dependency: {}", crate_name);
141                    println!("Dependency value type: {:?}", value);
142                }
143
144                // Skip if already exists
145                if crate_refs.contains_key(&crate_name) {
146                    continue;
147                }
148
149                match value {
150                    // Path dependency (standard table format)
151                    Item::Table(table) => {
152                        if self.debug {
153                            println!("Dependency {} is a table: {:?}", crate_name, table);
154                        }
155                        if let Some(path_value) = table.get("path") {
156                            if self.debug {
157                                println!("Path value for {}: {:?}", crate_name, path_value);
158                            }
159                            if let Some(path_str) = path_value.as_str() {
160                                let mut crate_ref = CrateReference::with_path(
161                                    crate_name.clone(),
162                                    path_str.to_string(),
163                                );
164                                if let Some(publish_value) = publish {
165                                    crate_ref.set_publish(publish_value);
166                                }
167
168                                if self.debug {
169                                    println!(
170                                        "Adding path dependency: {} at {}",
171                                        crate_name, path_str
172                                    );
173                                    println!("With publish setting: {:?}", crate_ref.publish);
174                                }
175
176                                crate_refs.insert(crate_name, crate_ref);
177                            }
178                        }
179                    }
180                    // Path dependency (inline table format)
181                    Item::Value(val) if val.is_inline_table() => {
182                        if self.debug {
183                            println!("Dependency {} is an inline table: {:?}", crate_name, val);
184                        }
185                        if let Some(inline_table) = val.as_inline_table() {
186                            if let Some(path_value) = inline_table.get("path") {
187                                if self.debug {
188                                    println!("Path value for {}: {:?}", crate_name, path_value);
189                                }
190                                if let Some(path_str) = path_value.as_str() {
191                                    let mut crate_ref = CrateReference::with_path(
192                                        crate_name.clone(),
193                                        path_str.to_string(),
194                                    );
195                                    if let Some(publish_value) = publish {
196                                        crate_ref.set_publish(publish_value);
197                                    }
198
199                                    if self.debug {
200                                        println!(
201                                            "Adding path dependency (inline): {} at {}",
202                                            crate_name, path_str
203                                        );
204                                        println!("With publish setting: {:?}", crate_ref.publish);
205                                    }
206
207                                    crate_refs.insert(crate_name, crate_ref);
208                                }
209                            }
210                        }
211                    }
212                    // Regular dependency
213                    _ => {
214                        // Regular dependencies are detected during analysis, so nothing to do here
215                        if self.debug {
216                            println!("Skipping regular dependency: {}", crate_name);
217                        }
218                    }
219                }
220            }
221        } else if self.debug {
222            println!("No dependencies section found in Cargo.toml");
223        }
224
225        Ok(())
226    }
227
228    fn analyze_file(&self, ctx: FileAnalysisContext) -> Result<()> {
229        let FileAnalysisContext {
230            content,
231            file_path,
232            use_regex,
233            extern_regex,
234            crate_refs,
235        } = ctx;
236
237        for line in content.lines() {
238            let line = line.trim();
239            if line.is_empty() || line.starts_with("//") {
240                continue;
241            }
242
243            // Handle use statements
244            if let Some(cap) = use_regex.captures(line) {
245                let full_path = cap[1].to_string();
246                let parts: Vec<&str> = full_path.split("::").collect();
247
248                // Skip empty parts and special keywords
249                if parts.is_empty()
250                    || parts[0] == "self"
251                    || parts[0] == "super"
252                    || parts[0] == "crate"
253                {
254                    continue;
255                }
256
257                let base_crate = parts[0].to_string();
258
259                // Skip standard library types and modules
260                if !is_std_crate(&base_crate)
261                    && !base_crate.starts_with("std::")
262                    && !base_crate.starts_with("core::")
263                    && !base_crate.starts_with("alloc::")
264                {
265                    // 既に内部クレートとして登録されている場合は、使用情報のみ追加
266                    if let Some(crate_ref) = crate_refs.get_mut(&base_crate) {
267                        crate_ref.add_usage(file_path.to_path_buf());
268                    } else {
269                        let crate_ref = crate_refs
270                            .entry(base_crate.clone())
271                            .or_insert_with(|| CrateReference::new(base_crate));
272                        crate_ref.add_usage(file_path.to_path_buf());
273                    }
274                }
275            }
276
277            // Handle extern crate statements
278            if let Some(cap) = extern_regex.captures(line) {
279                let crate_name = cap[1].to_string();
280                if !is_std_crate(&crate_name) {
281                    // 既に内部クレートとして登録されている場合は、使用情報のみ追加
282                    if let Some(crate_ref) = crate_refs.get_mut(&crate_name) {
283                        crate_ref.add_usage(file_path.to_path_buf());
284                    } else {
285                        let crate_ref = crate_refs
286                            .entry(crate_name.clone())
287                            .or_insert_with(|| CrateReference::new(crate_name));
288                        crate_ref.add_usage(file_path.to_path_buf());
289                    }
290                }
291            }
292        }
293
294        Ok(())
295    }
296}
297
298struct FileAnalysisContext<'a> {
299    content: String,
300    file_path: &'a PathBuf,
301    use_regex: &'a Regex,
302    extern_regex: &'a Regex,
303    crate_refs: &'a mut HashMap<String, CrateReference>,
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use std::fs::File;
310    use std::io::Write;
311    use tempfile::TempDir;
312
313    fn create_test_file(dir: &TempDir, name: &str, content: &str) -> Result<PathBuf> {
314        let path = dir.path().join(name);
315        let mut file = File::create(&path)?;
316        writeln!(file, "{}", content.trim())?;
317        Ok(path)
318    }
319
320    #[test]
321    fn test_analyze_dependencies() -> Result<()> {
322        let temp_dir = TempDir::new()?;
323
324        // Create test files with various import styles
325        let main_rs = create_test_file(
326            &temp_dir,
327            "main.rs",
328            r#"use serde::Serialize;
329               use tokio::runtime::Runtime;
330               use anyhow::Result;
331               use std::fs;"#,
332        )?;
333
334        let lib_rs = create_test_file(
335            &temp_dir,
336            "lib.rs",
337            r#"use serde::{Deserialize, Serialize};
338               use regex::Regex;
339               extern crate serde;"#,
340        )?;
341
342        // Debug output
343        println!("\nTest files created:");
344        println!("main.rs content:\n{}", fs::read_to_string(&main_rs)?);
345        println!("lib.rs content:\n{}", fs::read_to_string(&lib_rs)?);
346        println!("\nStarting analysis...\n");
347
348        let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
349        let crate_refs = analyzer.analyze_dependencies()?;
350
351        // Debug output
352        println!("\nAnalysis complete. Found crates:");
353        for (name, crate_ref) in &crate_refs {
354            println!("- {} (used in {} files)", name, crate_ref.usage_count());
355            println!("  Used in:");
356            for path in &crate_ref.used_in {
357                if let Ok(relative) = path.strip_prefix(temp_dir.path()) {
358                    println!("    - {}", relative.display());
359                }
360            }
361        }
362
363        assert!(
364            crate_refs.contains_key("serde"),
365            "serde dependency not found"
366        );
367        assert!(
368            crate_refs.contains_key("tokio"),
369            "tokio dependency not found"
370        );
371        assert!(
372            crate_refs.contains_key("anyhow"),
373            "anyhow dependency not found"
374        );
375        assert!(
376            crate_refs.contains_key("regex"),
377            "regex dependency not found"
378        );
379
380        let serde_ref = crate_refs.get("serde").unwrap();
381        assert_eq!(
382            serde_ref.usage_count(),
383            2,
384            "serde should be used in two files"
385        );
386
387        Ok(())
388    }
389
390    #[test]
391    fn test_load_existing_dependencies() -> Result<()> {
392        let temp_dir = TempDir::new()?;
393
394        // Create Cargo.toml with path dependencies
395        let cargo_toml_content = r#"
396[package]
397name = "test-package"
398version = "0.1.0"
399edition = "2021"
400publish = false
401
402[dependencies]
403serde = "1.0"
404internal-crate = { path = "../internal-crate" }
405"#;
406
407        let cargo_toml_path = temp_dir.path().join("Cargo.toml");
408        let mut file = File::create(&cargo_toml_path)?;
409        writeln!(file, "{}", cargo_toml_content)?;
410
411        // Create a simple source file to ensure the analyzer has something to work with
412        fs::create_dir_all(temp_dir.path().join("src"))?;
413        let main_rs_path = temp_dir.path().join("src/main.rs");
414        let main_rs_content = r#"
415fn main() {
416    println!("Hello, world!");
417}
418"#;
419        let mut file = File::create(main_rs_path)?;
420        writeln!(file, "{}", main_rs_content)?;
421
422        // Run the analyzer with debug mode to see what's happening
423        let analyzer = DependencyAnalyzer::with_debug(temp_dir.path().to_path_buf(), true);
424
425        // Analyze dependencies (this will call load_existing_dependencies internally)
426        let crate_refs = analyzer.analyze_dependencies()?;
427
428        // Check that internal-crate was detected as a path dependency
429        assert!(
430            crate_refs.contains_key("internal-crate"),
431            "internal-crate dependency not found"
432        );
433
434        if let Some(internal_crate) = crate_refs.get("internal-crate") {
435            assert!(
436                internal_crate.is_path_dependency,
437                "internal-crate should be a path dependency"
438            );
439            assert_eq!(
440                internal_crate.path,
441                Some("../internal-crate".to_string()),
442                "internal-crate path should be ../internal-crate"
443            );
444            assert_eq!(
445                internal_crate.publish,
446                Some(false),
447                "publish should be false"
448            );
449        }
450
451        Ok(())
452    }
453
454    #[test]
455    fn test_analyze_file() -> Result<()> {
456        let temp_dir = TempDir::new()?;
457        let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
458        let file_path = temp_dir.path().join("test.rs");
459        let content = r#"use serde::Serialize;
460                       use tokio::runtime::Runtime;
461                       extern crate anyhow;
462                       use std::fs;"#;
463
464        println!("\nTest file content:\n{}", content);
465        println!("\nStarting analysis...\n");
466
467        let mut crate_refs = HashMap::new();
468        let use_regex = Regex::new(r"^\s*use\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)*)")?;
469        let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
470
471        analyzer.analyze_file(FileAnalysisContext {
472            content: content.trim().to_string(),
473            file_path: &file_path,
474            use_regex: &use_regex,
475            extern_regex: &extern_regex,
476            crate_refs: &mut crate_refs,
477        })?;
478
479        println!("\nAnalysis complete. Found crates:");
480        for (name, crate_ref) in &crate_refs {
481            println!("- {} (used in {} files)", name, crate_ref.usage_count());
482        }
483
484        assert!(
485            crate_refs.contains_key("serde"),
486            "serde dependency not found"
487        );
488        assert!(
489            crate_refs.contains_key("tokio"),
490            "tokio dependency not found"
491        );
492        assert!(
493            crate_refs.contains_key("anyhow"),
494            "anyhow dependency not found"
495        );
496
497        Ok(())
498    }
499}