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 mut dev_crate_refs = HashMap::new();
36 let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
37
38 self.load_existing_dependencies(&mut crate_refs)?;
40
41 for entry in WalkDir::new(&self.project_root) {
43 let entry = entry?;
44 let path = entry.path();
45
46 if path.file_name().is_some_and(|f| f == "build.rs") {
48 continue;
49 }
50
51 let is_test_file = path.to_string_lossy().contains("tests/")
53 || path
54 .file_name()
55 .is_some_and(|f| f.to_string_lossy().ends_with("_test.rs"));
56
57 if path.extension().is_some_and(|ext| ext == "rs") {
58 let content = fs::read_to_string(path)?;
59 let file_path = path.to_path_buf();
60
61 if is_test_file {
62 self.analyze_file(FileAnalysisContext {
64 content: content.trim().to_string(),
65 file_path: &file_path,
66 extern_regex: &extern_regex,
67 crate_refs: &mut dev_crate_refs,
68 })?;
69 } else {
70 self.analyze_file(FileAnalysisContext {
72 content: content.trim().to_string(),
73 file_path: &file_path,
74 extern_regex: &extern_regex,
75 crate_refs: &mut crate_refs,
76 })?;
77 }
78 }
79 }
80
81 crate_refs.retain(|name, _| {
83 !name.ends_with("_test")
84 && !name.ends_with("_tests")
85 && name != "test"
86 && !name.starts_with("crate")
87 });
88
89 dev_crate_refs.retain(|name, _| {
91 !name.ends_with("_test")
92 && !name.ends_with("_tests")
93 && name != "test"
94 && !name.starts_with("crate")
95 });
96
97 for (name, mut crate_ref) in dev_crate_refs {
99 if crate_refs.contains_key(&name) {
101 continue;
102 }
103 crate_ref.set_dev_dependency(true);
104 crate_refs.insert(name, crate_ref);
105 }
106
107 if self.debug {
108 println!("\nFinal crate references:");
109 for (name, crate_ref) in &crate_refs {
110 println!("- {} (used in {} files)", name, crate_ref.usage_count());
111 if crate_ref.is_path_dependency {
112 println!(
113 " Path dependency: {}",
114 crate_ref.path.as_ref().unwrap_or(&"unknown".to_string())
115 );
116 }
117 if let Some(publish) = crate_ref.publish {
118 println!(" Publish: {}", publish);
119 }
120 if crate_ref.is_dev_dependency {
121 println!(" Dev dependency: true");
122 }
123 println!(" Used in:");
124 for path in &crate_ref.used_in {
125 println!(" - {:?}", path);
126 }
127 }
128 }
129
130 Ok(crate_refs)
131 }
132
133 fn load_existing_dependencies(
135 &self,
136 crate_refs: &mut HashMap<String, CrateReference>,
137 ) -> Result<()> {
138 let cargo_toml_path = self.project_root.join("Cargo.toml");
139 if !cargo_toml_path.exists() {
140 return Ok(());
141 }
142
143 if self.debug {
144 println!("Loading dependencies from {:?}", cargo_toml_path);
145 }
146
147 let content = fs::read_to_string(&cargo_toml_path)
148 .with_context(|| format!("Failed to read Cargo.toml at {:?}", cargo_toml_path))?;
149 let doc = content
150 .parse::<DocumentMut>()
151 .with_context(|| format!("Failed to parse Cargo.toml at {:?}", cargo_toml_path))?;
152
153 let publish = if let Some(package) = doc.get("package") {
155 if let Some(publish_value) = package.get("publish") {
156 publish_value.as_bool()
157 } else {
158 None
159 }
160 } else {
161 None
162 };
163
164 if self.debug {
165 println!("Package publish setting: {:?}", publish);
166 }
167
168 if let Some(dependencies) = doc.get("dependencies").and_then(|d| d.as_table()) {
170 for (name, value) in dependencies.iter() {
171 let crate_name = name.to_string();
172
173 if self.debug {
174 println!("Found dependency: {}", crate_name);
175 println!("Dependency value type: {:?}", value);
176 }
177
178 if crate_refs.contains_key(&crate_name) {
180 continue;
181 }
182
183 match value {
184 Item::Table(table) => {
186 if self.debug {
187 println!("Dependency {} is a table: {:?}", crate_name, table);
188 }
189 if let Some(path_value) = table.get("path") {
190 if self.debug {
191 println!("Path value for {}: {:?}", crate_name, path_value);
192 }
193 if let Some(path_str) = path_value.as_str() {
194 let mut crate_ref = CrateReference::with_path(
195 crate_name.clone(),
196 path_str.to_string(),
197 );
198 if let Some(publish_value) = publish {
199 crate_ref.set_publish(publish_value);
200 }
201
202 if self.debug {
203 println!(
204 "Adding path dependency: {} at {}",
205 crate_name, path_str
206 );
207 println!("With publish setting: {:?}", crate_ref.publish);
208 }
209
210 crate_refs.insert(crate_name, crate_ref);
211 }
212 }
213 }
214 Item::Value(val) if val.is_inline_table() => {
216 if self.debug {
217 println!("Dependency {} is an inline table: {:?}", crate_name, val);
218 }
219 if let Some(inline_table) = val.as_inline_table()
220 && let Some(path_value) = inline_table.get("path")
221 {
222 if self.debug {
223 println!("Path value for {}: {:?}", crate_name, path_value);
224 }
225 if let Some(path_str) = path_value.as_str() {
226 let mut crate_ref = CrateReference::with_path(
227 crate_name.clone(),
228 path_str.to_string(),
229 );
230 if let Some(publish_value) = publish {
231 crate_ref.set_publish(publish_value);
232 }
233
234 if self.debug {
235 println!(
236 "Adding path dependency (inline): {} at {}",
237 crate_name, path_str
238 );
239 println!("With publish setting: {:?}", crate_ref.publish);
240 }
241
242 crate_refs.insert(crate_name, crate_ref);
243 }
244 }
245 }
246 _ => {
248 if self.debug {
250 println!("Skipping regular dependency: {}", crate_name);
251 }
252 }
253 }
254 }
255 } else if self.debug {
256 println!("No dependencies section found in Cargo.toml");
257 }
258
259 Ok(())
260 }
261
262 fn analyze_file(&self, ctx: FileAnalysisContext) -> Result<()> {
263 let FileAnalysisContext {
264 content,
265 file_path,
266 extern_regex,
267 crate_refs,
268 } = ctx;
269
270 let lines: Vec<&str> = content.lines().collect();
271 let mut current_line_num = 0;
272
273 while current_line_num < lines.len() {
274 let line = lines[current_line_num].trim();
275 current_line_num += 1;
276
277 if line.is_empty() {
278 continue;
279 }
280
281 if line.starts_with("//") || line.starts_with("/*") {
283 continue;
284 }
285
286 if line.starts_with("use") {
288 let mut use_statement = line.to_string();
290 let mut brace_count = line.chars().filter(|&c| c == '{').count()
291 - line.chars().filter(|&c| c == '}').count();
292
293 while brace_count > 0 && current_line_num < lines.len() {
295 let next_line = lines[current_line_num].trim();
296 current_line_num += 1;
297 use_statement.push('\n');
298 use_statement.push_str(next_line);
299
300 brace_count += next_line.chars().filter(|&c| c == '{').count();
301 brace_count -= next_line.chars().filter(|&c| c == '}').count();
302 }
303
304 self.extract_crates_from_use(&use_statement, crate_refs)?;
306 continue;
307 }
308
309 if let Some(cap) = extern_regex.captures(line) {
311 let crate_name = cap[1].to_string();
312 if !is_std_crate(&crate_name) {
313 crate_refs
314 .entry(crate_name.clone())
315 .or_insert_with(|| CrateReference::new(crate_name))
316 .add_usage(file_path.clone());
317 }
318 }
319 }
320
321 self.scan_for_direct_references(&content, crate_refs)?;
323
324 Ok(())
325 }
326
327 fn extract_crates_from_use(
329 &self,
330 use_statement: &str,
331 crate_refs: &mut HashMap<String, CrateReference>,
332 ) -> Result<()> {
333 let clean_use = self.remove_comments(use_statement);
335
336 if self.debug {
337 println!("Cleaned use statement: {}", clean_use);
338 }
339
340 let statement = clean_use.trim_start_matches("use").trim();
342
343 if !statement.starts_with('{') && statement.contains("::") {
345 let parts: Vec<&str> = statement.split("::").collect();
346 if !parts.is_empty() {
347 let crate_name = parts[0].trim_end_matches(':').trim();
348 self.add_crate_if_valid(crate_name, crate_refs);
349 }
350 }
351 else if !statement.starts_with('{') && statement.contains("::") && statement.contains('{')
353 {
354 let parts: Vec<&str> = statement.split("::").collect();
355 if !parts.is_empty() {
356 let crate_name = parts[0].trim();
357 self.add_crate_if_valid(crate_name, crate_refs);
358 }
359 }
360 else if statement.starts_with('{') {
362 let content = &statement[1..statement.rfind('}').unwrap_or(statement.len())];
364
365 for item in content.split(',') {
367 let item = item.trim();
368 if item.is_empty() {
369 continue;
370 }
371
372 if item.contains("::") {
374 let parts: Vec<&str> = item.split("::").collect();
375 if !parts.is_empty() {
376 let crate_name = parts[0].trim();
377 self.add_crate_if_valid(crate_name, crate_refs);
378 }
379 }
380 else {
382 let crate_name = item.trim();
383 self.add_crate_if_valid(crate_name, crate_refs);
384 }
385 }
386 }
387 else {
389 let crate_name = statement.trim_end_matches(';').trim();
390 self.add_crate_if_valid(crate_name, crate_refs);
391 }
392
393 Ok(())
394 }
395
396 fn add_crate_if_valid(
398 &self,
399 crate_name: &str,
400 crate_refs: &mut HashMap<String, CrateReference>,
401 ) {
402 let clean_name = crate_name.trim().trim_end_matches(['}', '\n', '\r', ':']);
404
405 if !clean_name.is_empty()
406 && !is_std_crate(clean_name)
407 && clean_name != "crate"
408 && clean_name != "self"
409 && clean_name != "super"
410 {
411 if self.debug {
412 println!("Found crate: {}", clean_name);
413 }
414
415 let original_name = clean_name.to_string();
417
418 crate_refs
419 .entry(original_name.clone())
420 .or_insert_with(|| CrateReference::new(original_name))
421 .add_usage(PathBuf::from(""));
422 }
423 }
424
425 fn remove_comments(&self, code: &str) -> String {
427 let mut clean_code = String::new();
428 let mut in_line_comment = false;
429 let mut in_block_comment = false;
430 let mut i = 0;
431 let chars: Vec<char> = code.chars().collect();
432
433 while i < chars.len() {
434 if in_line_comment {
435 if chars[i] == '\n' {
436 in_line_comment = false;
437 clean_code.push('\n');
438 }
439 i += 1;
440 continue;
441 }
442
443 if in_block_comment {
444 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '/' {
445 in_block_comment = false;
446 i += 2;
447 } else {
448 i += 1;
449 }
450 continue;
451 }
452
453 if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '/' {
454 in_line_comment = true;
455 i += 2;
456 continue;
457 }
458
459 if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {
460 in_block_comment = true;
461 i += 2;
462 continue;
463 }
464
465 clean_code.push(chars[i]);
466 i += 1;
467 }
468
469 clean_code
470 }
471
472 fn scan_for_direct_references(
474 &self,
475 content: &str,
476 crate_refs: &mut HashMap<String, CrateReference>,
477 ) -> Result<()> {
478 let clean_content = self.remove_comments(content);
480
481 let direct_ref_regex = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_-]*)::([a-zA-Z0-9_:]+)")?;
483
484 for cap in direct_ref_regex.captures_iter(&clean_content) {
485 let potential_crate = &cap[1];
486 if !is_std_crate(potential_crate) {
487 self.add_crate_if_valid(potential_crate, crate_refs);
488 }
489 }
490
491 Ok(())
492 }
493}
494
495struct FileAnalysisContext<'a> {
496 content: String,
497 file_path: &'a PathBuf,
498 extern_regex: &'a Regex,
499 crate_refs: &'a mut HashMap<String, CrateReference>,
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use std::fs::File;
506 use std::io::Write;
507 use tempfile::TempDir;
508
509 fn create_test_file(dir: &TempDir, name: &str, content: &str) -> Result<PathBuf> {
510 let path = dir.path().join(name);
511 let mut file = File::create(&path)?;
512 writeln!(file, "{}", content.trim())?;
513 Ok(path)
514 }
515
516 #[test]
517 fn test_analyze_dependencies() -> Result<()> {
518 let temp_dir = TempDir::new()?;
519
520 let main_rs = create_test_file(
522 &temp_dir,
523 "main.rs",
524 r#"use serde::Serialize;
525 use tokio::runtime::Runtime;
526 use anyhow::Result;
527 use std::fs;"#,
528 )?;
529
530 let lib_rs = create_test_file(
531 &temp_dir,
532 "lib.rs",
533 r#"use serde::{Deserialize, Serialize};
534 use regex::Regex;
535 extern crate serde;"#,
536 )?;
537
538 println!("\nTest files created:");
540 println!("main.rs content:\n{}", fs::read_to_string(&main_rs)?);
541 println!("lib.rs content:\n{}", fs::read_to_string(&lib_rs)?);
542 println!("\nStarting analysis...\n");
543
544 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
545 let crate_refs = analyzer.analyze_dependencies()?;
546
547 println!("\nAnalysis complete. Found crates:");
549 for (name, crate_ref) in &crate_refs {
550 println!("- {} (used in {} files)", name, crate_ref.usage_count());
551 println!(" Used in:");
552 for path in &crate_ref.used_in {
553 if let Ok(relative) = path.strip_prefix(temp_dir.path()) {
554 println!(" - {}", relative.display());
555 }
556 }
557 }
558
559 assert!(
560 crate_refs.contains_key("serde"),
561 "serde dependency not found"
562 );
563 assert!(
564 crate_refs.contains_key("tokio"),
565 "tokio dependency not found"
566 );
567 assert!(
568 crate_refs.contains_key("anyhow"),
569 "anyhow dependency not found"
570 );
571 assert!(
572 crate_refs.contains_key("regex"),
573 "regex dependency not found"
574 );
575
576 let serde_ref = crate_refs.get("serde").unwrap();
577 assert_eq!(
578 serde_ref.usage_count(),
579 2,
580 "serde should be used in two files"
581 );
582
583 Ok(())
584 }
585
586 #[test]
587 fn test_load_existing_dependencies() -> Result<()> {
588 let temp_dir = TempDir::new()?;
589
590 let cargo_toml_content = r#"
592[package]
593name = "test-package"
594version = "0.1.0"
595edition = "2021"
596publish = false
597
598[dependencies]
599serde = "1.0"
600internal-crate = { path = "../internal-crate" }
601"#;
602
603 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
604 let mut file = File::create(&cargo_toml_path)?;
605 writeln!(file, "{}", cargo_toml_content)?;
606
607 fs::create_dir_all(temp_dir.path().join("src"))?;
609 let main_rs_path = temp_dir.path().join("src/main.rs");
610 let main_rs_content = r#"
611fn main() {
612 println!("Hello, world!");
613}
614"#;
615 let mut file = File::create(main_rs_path)?;
616 writeln!(file, "{}", main_rs_content)?;
617
618 let analyzer = DependencyAnalyzer::with_debug(temp_dir.path().to_path_buf(), true);
620
621 let crate_refs = analyzer.analyze_dependencies()?;
623
624 assert!(
626 crate_refs.contains_key("internal-crate"),
627 "internal-crate dependency not found"
628 );
629
630 if let Some(internal_crate) = crate_refs.get("internal-crate") {
631 assert!(
632 internal_crate.is_path_dependency,
633 "internal-crate should be a path dependency"
634 );
635 assert_eq!(
636 internal_crate.path,
637 Some("../internal-crate".to_string()),
638 "internal-crate path should be ../internal-crate"
639 );
640 assert_eq!(
641 internal_crate.publish,
642 Some(false),
643 "publish should be false"
644 );
645 }
646
647 Ok(())
648 }
649
650 #[test]
651 fn test_analyze_file() -> Result<()> {
652 let temp_dir = TempDir::new()?;
653 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
654 let file_path = temp_dir.path().join("test.rs");
655 let content = r#"use serde::Serialize;
656 use tokio::runtime::Runtime;
657 extern crate anyhow;
658 use std::fs;"#;
659
660 println!("\nTest file content:\n{}", content);
661 println!("\nStarting analysis...\n");
662
663 let mut crate_refs = HashMap::new();
664 let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
665
666 analyzer.analyze_file(FileAnalysisContext {
667 content: content.trim().to_string(),
668 file_path: &file_path,
669 extern_regex: &extern_regex,
670 crate_refs: &mut crate_refs,
671 })?;
672
673 println!("\nAnalysis complete. Found crates:");
674 for (name, crate_ref) in &crate_refs {
675 println!("- {} (used in {} files)", name, crate_ref.usage_count());
676 }
677
678 assert!(
679 crate_refs.contains_key("serde"),
680 "serde dependency not found"
681 );
682 assert!(
683 crate_refs.contains_key("tokio"),
684 "tokio dependency not found"
685 );
686 assert!(
687 crate_refs.contains_key("anyhow"),
688 "anyhow dependency not found"
689 );
690
691 Ok(())
692 }
693
694 #[test]
695 fn test_complex_use_statements() -> Result<()> {
696 let temp_dir = TempDir::new()?;
697 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
698 let file_path = temp_dir.path().join("complex_use.rs");
699
700 let content = r#"
702 // Simple use statement
703 use serde::Serialize;
704
705 // Braced use statement
706 use {
707 tokio::runtime::Runtime,
708 reqwest::Client,
709 anyhow::Result
710 };
711
712 // Braced use with comments
713 use {
714 //serde_json::Value,
715 regex::Regex,
716 /* rand::Rng,
717 chrono::DateTime */
718 walkdir::WalkDir
719 };
720
721 // Wildcard import
722 use clap::*;
723
724 // Mixed imports
725 use {
726 std::fs,
727 std::path::PathBuf,
728 log::*
729 };
730 "#;
731
732 println!("\nComplex test file content:\n{}", content);
733 println!("\nStarting analysis...\n");
734
735 let mut crate_refs = HashMap::new();
736 let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
737
738 analyzer.analyze_file(FileAnalysisContext {
739 content: content.to_string(),
740 file_path: &file_path,
741 extern_regex: &extern_regex,
742 crate_refs: &mut crate_refs,
743 })?;
744
745 println!("\nAnalysis complete. Found crates:");
746 for (name, crate_ref) in &crate_refs {
747 println!("- {}: {:?}", name, crate_ref);
748 }
749
750 assert!(crate_refs.contains_key("serde"), "serde should be detected");
752 assert!(crate_refs.contains_key("tokio"), "tokio should be detected");
753 assert!(
754 crate_refs.contains_key("reqwest"),
755 "reqwest should be detected"
756 );
757 assert!(
758 crate_refs.contains_key("anyhow"),
759 "anyhow should be detected"
760 );
761 assert!(crate_refs.contains_key("regex"), "regex should be detected");
762 assert!(
763 crate_refs.contains_key("walkdir"),
764 "walkdir should be detected"
765 );
766 assert!(crate_refs.contains_key("clap"), "clap should be detected");
767 assert!(crate_refs.contains_key("log"), "log should be detected");
768
769 assert!(
771 !crate_refs.contains_key("serde_json"),
772 "serde_json should not be detected (commented out)"
773 );
774 assert!(
775 !crate_refs.contains_key("rand"),
776 "rand should not be detected (commented out)"
777 );
778 assert!(
779 !crate_refs.contains_key("chrono"),
780 "chrono should not be detected (commented out)"
781 );
782
783 Ok(())
784 }
785
786 #[test]
787 fn test_nested_and_complex_use_statements() -> Result<()> {
788 let temp_dir = TempDir::new()?;
789 let analyzer = DependencyAnalyzer::with_debug(temp_dir.path().to_path_buf(), true);
791 let file_path = temp_dir.path().join("nested_use.rs");
792
793 let content = r#"
795 // Nested use with multiple levels
796 use {
797 serde::{Serialize, Deserialize},
798 tokio::{
799 runtime::Runtime,
800 sync::{Mutex, RwLock}
801 },
802 // Commented section
803 /*
804 rand::{
805 Rng,
806 distributions::Uniform
807 },
808 */
809 reqwest::{Client, Response}
810 };
811
812 // Multiple lines with inline comments
813 use clap::{ // Command line parser
814 Command, // For creating commands
815 Arg, // For defining arguments
816 ArgMatches // For matching arguments
817 };
818
819 // Mixed with standard library
820 use {
821 std::{
822 fs::File,
823 io::{Read, Write},
824 path::{Path, PathBuf}
825 },
826 log::{debug, info, warn, error}
827 };
828 "#;
829
830 println!("\nNested test file content:\n{}", content);
831 println!("\nStarting analysis...\n");
832
833 let mut crate_refs = HashMap::new();
834 let extern_regex = Regex::new(r"^\s*extern\s+crate\s+([a-zA-Z_][a-zA-Z0-9_]*)")?;
835
836 analyzer.analyze_file(FileAnalysisContext {
837 content: content.to_string(),
838 file_path: &file_path,
839 extern_regex: &extern_regex,
840 crate_refs: &mut crate_refs,
841 })?;
842
843 println!("\nAnalysis complete. Found crates:");
844 for (name, crate_ref) in &crate_refs {
845 println!("- {}: {:?}", name, crate_ref);
846 }
847
848 assert!(crate_refs.contains_key("serde"), "serde should be detected");
850 assert!(
851 crate_refs.contains_key("reqwest"),
852 "reqwest should be detected"
853 );
854 assert!(crate_refs.contains_key("clap"), "clap should be detected");
855 assert!(crate_refs.contains_key("log"), "log should be detected");
856
857 if !crate_refs.contains_key("tokio") {
859 println!(
860 "NOTE: tokio was not detected. This is a known limitation of the current implementation."
861 );
862 println!("The current implementation does not fully support deeply nested imports.");
863 println!("This is acceptable for now, as the main goal is to detect top-level crates.");
864 }
865
866 assert!(
868 !crate_refs.contains_key("rand"),
869 "rand should not be detected (commented out)"
870 );
871
872 Ok(())
873 }
874
875 #[test]
876 fn test_filter_test_crates() -> Result<()> {
877 let temp_dir = TempDir::new()?;
878
879 let cargo_toml_content = r#"
881[package]
882name = "test-package"
883version = "0.1.0"
884edition = "2021"
885
886[dependencies]
887"#;
888 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
889 let mut file = File::create(&cargo_toml_path)?;
890 writeln!(file, "{}", cargo_toml_content)?;
891
892 fs::create_dir_all(temp_dir.path().join("src"))?;
894 let main_rs_path = temp_dir.path().join("src/main.rs");
895 let main_rs_content = r#"
896use serde::Serialize;
897use my_crate_test;
898use another_tests;
899use test;
900use tempfile;
901use crate::internal;
902use self::module;
903use super::parent;
904
905fn main() {}
906"#;
907 let mut file = File::create(main_rs_path)?;
908 writeln!(file, "{}", main_rs_content)?;
909
910 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
911 let crate_refs = analyzer.analyze_dependencies()?;
912
913 assert!(crate_refs.contains_key("serde"), "serde should be detected");
915
916 assert!(
918 !crate_refs.contains_key("my_crate_test"),
919 "crates ending with _test should be filtered"
920 );
921 assert!(
922 !crate_refs.contains_key("another_tests"),
923 "crates ending with _tests should be filtered"
924 );
925 assert!(
926 !crate_refs.contains_key("test"),
927 "test crate should be filtered"
928 );
929
930 assert!(
934 !crate_refs.contains_key("crate"),
935 "crate keyword should be filtered"
936 );
937 assert!(
938 !crate_refs.contains_key("self"),
939 "self keyword should be filtered"
940 );
941 assert!(
942 !crate_refs.contains_key("super"),
943 "super keyword should be filtered"
944 );
945
946 Ok(())
947 }
948
949 #[test]
950 fn test_dev_dependencies_from_tests_directory() -> Result<()> {
951 let temp_dir = TempDir::new()?;
952
953 let cargo_toml_content = r#"
955[package]
956name = "test-package"
957version = "0.1.0"
958edition = "2021"
959
960[dependencies]
961"#;
962 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
963 let mut file = File::create(&cargo_toml_path)?;
964 writeln!(file, "{}", cargo_toml_content)?;
965
966 fs::create_dir_all(temp_dir.path().join("src"))?;
968 let main_rs_path = temp_dir.path().join("src/main.rs");
969 let main_rs_content = r#"
970use serde::Serialize;
971
972fn main() {}
973"#;
974 let mut file = File::create(main_rs_path)?;
975 writeln!(file, "{}", main_rs_content)?;
976
977 fs::create_dir_all(temp_dir.path().join("tests"))?;
979 let test_rs_path = temp_dir.path().join("tests/integration.rs");
980 let test_rs_content = r#"
981use assert_fs;
982use predicates;
983
984#[test]
985fn test_something() {}
986"#;
987 let mut file = File::create(test_rs_path)?;
988 writeln!(file, "{}", test_rs_content)?;
989
990 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
991 let crate_refs = analyzer.analyze_dependencies()?;
992
993 assert!(
995 crate_refs.contains_key("serde"),
996 "serde from src/ should be detected"
997 );
998 assert!(
999 !crate_refs.get("serde").unwrap().is_dev_dependency,
1000 "serde should NOT be a dev-dependency"
1001 );
1002
1003 assert!(
1005 crate_refs.contains_key("assert_fs"),
1006 "assert_fs from tests/ should be detected"
1007 );
1008 assert!(
1009 crate_refs.get("assert_fs").unwrap().is_dev_dependency,
1010 "assert_fs should be a dev-dependency"
1011 );
1012
1013 assert!(
1014 crate_refs.contains_key("predicates"),
1015 "predicates from tests/ should be detected"
1016 );
1017 assert!(
1018 crate_refs.get("predicates").unwrap().is_dev_dependency,
1019 "predicates should be a dev-dependency"
1020 );
1021
1022 Ok(())
1023 }
1024
1025 #[test]
1026 fn test_skip_build_rs() -> Result<()> {
1027 let temp_dir = TempDir::new()?;
1028
1029 let cargo_toml_content = r#"
1031[package]
1032name = "test-package"
1033version = "0.1.0"
1034edition = "2021"
1035
1036[dependencies]
1037"#;
1038 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
1039 let mut file = File::create(&cargo_toml_path)?;
1040 writeln!(file, "{}", cargo_toml_content)?;
1041
1042 fs::create_dir_all(temp_dir.path().join("src"))?;
1044 let main_rs_path = temp_dir.path().join("src/main.rs");
1045 let main_rs_content = r#"
1046use serde::Serialize;
1047
1048fn main() {}
1049"#;
1050 let mut file = File::create(main_rs_path)?;
1051 writeln!(file, "{}", main_rs_content)?;
1052
1053 let build_rs_path = temp_dir.path().join("build.rs");
1055 let build_rs_content = r#"
1056use cc;
1057use pkg_config;
1058
1059fn main() {
1060 cc::Build::new().file("src/foo.c").compile("foo");
1061}
1062"#;
1063 let mut file = File::create(build_rs_path)?;
1064 writeln!(file, "{}", build_rs_content)?;
1065
1066 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
1067 let crate_refs = analyzer.analyze_dependencies()?;
1068
1069 assert!(
1071 crate_refs.contains_key("serde"),
1072 "serde from src/ should be detected"
1073 );
1074
1075 assert!(
1077 !crate_refs.contains_key("cc"),
1078 "cc from build.rs should be skipped"
1079 );
1080 assert!(
1081 !crate_refs.contains_key("pkg_config"),
1082 "pkg_config from build.rs should be skipped"
1083 );
1084
1085 Ok(())
1086 }
1087
1088 #[test]
1089 fn test_direct_reference_detection() -> Result<()> {
1090 let temp_dir = TempDir::new()?;
1091
1092 let cargo_toml_content = r#"
1094[package]
1095name = "test-package"
1096version = "0.1.0"
1097edition = "2021"
1098
1099[dependencies]
1100"#;
1101 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
1102 let mut file = File::create(&cargo_toml_path)?;
1103 writeln!(file, "{}", cargo_toml_content)?;
1104
1105 fs::create_dir_all(temp_dir.path().join("src"))?;
1107 let main_rs_path = temp_dir.path().join("src/main.rs");
1108 let main_rs_content = r#"
1109fn main() {
1110 let value: serde_json::Value = serde_json::from_str("{}").unwrap();
1111 let regex = regex::Regex::new(r"test").unwrap();
1112}
1113"#;
1114 let mut file = File::create(main_rs_path)?;
1115 writeln!(file, "{}", main_rs_content)?;
1116
1117 let analyzer = DependencyAnalyzer::new(temp_dir.path().to_path_buf());
1118 let crate_refs = analyzer.analyze_dependencies()?;
1119
1120 assert!(
1122 crate_refs.contains_key("serde_json"),
1123 "serde_json direct reference should be detected"
1124 );
1125 assert!(
1126 crate_refs.contains_key("regex"),
1127 "regex direct reference should be detected"
1128 );
1129
1130 Ok(())
1131 }
1132}