1use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
12pub struct DependencyMapper {
13 dependencies: HashMap<PathBuf, HashSet<PathBuf>>,
15 dependents: HashMap<PathBuf, HashSet<PathBuf>>,
17}
18
19impl DependencyMapper {
20 pub fn new() -> Self {
22 Self {
23 dependencies: HashMap::new(),
24 dependents: HashMap::new(),
25 }
26 }
27
28 pub fn add_file(&mut self, path: &Path, source: &str) {
32 self.remove_file(path);
34
35 let lang = detect_language(path);
36 let raw_imports = parse_imports(source, lang.as_deref());
37
38 let dir = path.parent().unwrap_or(Path::new(""));
39 let resolved: HashSet<PathBuf> = raw_imports
40 .into_iter()
41 .filter_map(|imp| resolve_import(&imp, dir))
42 .collect();
43
44 self.dependencies.insert(path.to_path_buf(), resolved.clone());
46
47 for dep in &resolved {
49 self.dependents
50 .entry(dep.clone())
51 .or_default()
52 .insert(path.to_path_buf());
53 }
54 }
55
56 pub fn remove_file(&mut self, path: &Path) {
58 if let Some(old_deps) = self.dependencies.remove(path) {
59 for dep in &old_deps {
60 if let Some(rev) = self.dependents.get_mut(dep) {
61 rev.remove(path);
62 if rev.is_empty() {
63 self.dependents.remove(dep);
64 }
65 }
66 }
67 }
68 if let Some(old_rev) = self.dependents.remove(path) {
70 for src in &old_rev {
71 if let Some(fwd) = self.dependencies.get_mut(src) {
72 fwd.remove(path);
73 }
74 }
75 }
76 }
77
78 pub fn dependencies_of(&self, path: &Path) -> Vec<PathBuf> {
80 let mut deps: Vec<PathBuf> = self
81 .dependencies
82 .get(path)
83 .map(|s| s.iter().cloned().collect())
84 .unwrap_or_default();
85 deps.sort();
86 deps
87 }
88
89 pub fn dependents_of(&self, path: &Path) -> Vec<PathBuf> {
91 let mut deps: Vec<PathBuf> = self
92 .dependents
93 .get(path)
94 .map(|s| s.iter().cloned().collect())
95 .unwrap_or_default();
96 deps.sort();
97 deps
98 }
99
100 pub fn summary(&self, path: &Path) -> String {
103 let deps = self.dependencies_of(path);
104 let revs = self.dependents_of(path);
105
106 if deps.is_empty() && revs.is_empty() {
107 return String::new();
108 }
109
110 let mut parts = Vec::new();
111
112 if !deps.is_empty() {
113 let names: Vec<String> = deps
114 .iter()
115 .map(|p| short_name(p))
116 .collect();
117 parts.push(format!("imports: {}", names.join(", ")));
118 }
119
120 if !revs.is_empty() {
121 let names: Vec<String> = revs
122 .iter()
123 .map(|p| short_name(p))
124 .collect();
125 parts.push(format!("imported by: {}", names.join(", ")));
126 }
127
128 format!("[deps] {}", parts.join(" | "))
129 }
130
131 pub fn file_count(&self) -> usize {
133 self.dependencies.len()
134 }
135}
136
137impl Default for DependencyMapper {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143fn short_name(path: &Path) -> String {
145 let components: Vec<&str> = path
146 .components()
147 .filter_map(|c| c.as_os_str().to_str())
148 .collect();
149 if components.len() <= 2 {
150 path.to_string_lossy().to_string()
151 } else {
152 components[components.len() - 2..].join("/")
153 }
154}
155
156fn detect_language(path: &Path) -> Option<String> {
161 let ext = path.extension()?.to_str()?;
162 let lang = match ext {
163 "rs" => "rust",
164 "py" => "python",
165 "js" | "mjs" | "cjs" => "javascript",
166 "ts" | "tsx" | "mts" => "typescript",
167 "go" => "go",
168 "java" => "java",
169 "c" | "h" => "c",
170 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
171 "rb" => "ruby",
172 "sh" | "bash" => "bash",
173 "json" => "json",
174 "html" | "htm" => "html",
175 "css" | "scss" | "less" => "css",
176 "cs" => "csharp",
177 "kt" | "kts" => "kotlin",
178 "swift" => "swift",
179 "toml" => "toml",
180 "yaml" | "yml" => "yaml",
181 _ => return None,
182 };
183 Some(lang.to_string())
184}
185
186fn parse_imports(source: &str, language: Option<&str>) -> Vec<String> {
193 let lang = match language {
194 Some(l) => l,
195 None => return Vec::new(),
196 };
197
198 let mut imports = Vec::new();
199
200 for line in source.lines() {
201 let trimmed = line.trim();
202 if trimmed.is_empty() {
203 continue;
204 }
205
206 match lang {
207 "rust" => parse_rust_import(trimmed, &mut imports),
208 "python" => parse_python_import(trimmed, &mut imports),
209 "javascript" | "typescript" => parse_js_ts_import(trimmed, &mut imports),
210 "go" => parse_go_import(trimmed, &mut imports),
211 "java" | "kotlin" => parse_java_kotlin_import(trimmed, &mut imports),
212 "c" | "cpp" => parse_c_cpp_import(trimmed, &mut imports),
213 "ruby" => parse_ruby_import(trimmed, &mut imports),
214 "csharp" => parse_csharp_import(trimmed, &mut imports),
215 "swift" => parse_swift_import(trimmed, &mut imports),
216 "css" => parse_css_import(trimmed, &mut imports),
217 "html" => parse_html_import(trimmed, &mut imports),
218 _ => {} }
220 }
221
222 imports
223}
224
225fn parse_rust_import(line: &str, imports: &mut Vec<String>) {
227 if let Some(rest) = line.strip_prefix("use ") {
228 let spec = rest.trim_end_matches(';').trim();
229 let path = spec.split('{').next().unwrap_or(spec);
231 let path = path.split(" as ").next().unwrap_or(path).trim();
232 if !path.is_empty() {
233 imports.push(path.to_string());
234 }
235 } else if let Some(rest) = line.strip_prefix("mod ") {
236 let name = rest.trim_end_matches(';').trim();
237 if !name.is_empty() && !line.contains('{') {
238 imports.push(name.to_string());
239 }
240 }
241}
242
243fn parse_python_import(line: &str, imports: &mut Vec<String>) {
245 if let Some(rest) = line.strip_prefix("from ") {
246 if let Some(module) = rest.split(" import").next() {
248 let module = module.trim();
249 if !module.is_empty() {
250 imports.push(module.to_string());
251 }
252 }
253 } else if let Some(rest) = line.strip_prefix("import ") {
254 for part in rest.split(',') {
256 let module = part.split(" as ").next().unwrap_or(part).trim();
257 if !module.is_empty() {
258 imports.push(module.to_string());
259 }
260 }
261 }
262}
263
264fn parse_js_ts_import(line: &str, imports: &mut Vec<String>) {
266 if line.starts_with("import ") || line.starts_with("export ") {
268 if let Some(path) = extract_quoted_string(line, "from ") {
269 imports.push(path);
270 } else if line.starts_with("import ") {
271 if let Some(path) = extract_first_quoted(line) {
273 imports.push(path);
274 }
275 }
276 }
277 if line.contains("require(") {
279 if let Some(path) = extract_require_path(line) {
280 imports.push(path);
281 }
282 }
283}
284
285fn parse_go_import(line: &str, imports: &mut Vec<String>) {
287 let trimmed = line.trim();
288 if trimmed.starts_with("import ") || trimmed.starts_with("\"") {
289 if let Some(path) = extract_first_quoted(trimmed) {
290 imports.push(path);
291 }
292 }
293}
294
295fn parse_java_kotlin_import(line: &str, imports: &mut Vec<String>) {
297 if let Some(rest) = line.strip_prefix("import ") {
298 let rest = rest.strip_prefix("static ").unwrap_or(rest);
299 let path = rest.trim_end_matches(';').trim();
300 if !path.is_empty() {
301 imports.push(path.to_string());
302 }
303 }
304}
305
306fn parse_c_cpp_import(line: &str, imports: &mut Vec<String>) {
308 if let Some(rest) = line.strip_prefix("#include") {
309 let rest = rest.trim();
310 if let Some(path) = rest.strip_prefix('"').and_then(|r| r.strip_suffix('"')) {
311 imports.push(path.to_string());
312 } else if let Some(path) = rest.strip_prefix('<').and_then(|r| r.strip_suffix('>')) {
313 imports.push(path.to_string());
314 }
315 }
316}
317
318fn parse_ruby_import(line: &str, imports: &mut Vec<String>) {
320 if line.starts_with("require ") || line.starts_with("require_relative ") {
321 if let Some(path) = extract_first_quoted(line) {
322 imports.push(path);
323 }
324 }
325}
326
327fn parse_csharp_import(line: &str, imports: &mut Vec<String>) {
329 if let Some(rest) = line.strip_prefix("using ") {
330 if rest.starts_with("var ") || rest.contains('=') {
332 return;
333 }
334 let rest = rest.strip_prefix("static ").unwrap_or(rest);
335 let ns = rest.trim_end_matches(';').trim();
336 if !ns.is_empty() {
337 imports.push(ns.to_string());
338 }
339 }
340}
341
342fn parse_swift_import(line: &str, imports: &mut Vec<String>) {
344 if let Some(rest) = line.strip_prefix("import ") {
345 let module = rest.trim();
346 if !module.is_empty() {
347 imports.push(module.to_string());
348 }
349 }
350}
351
352fn parse_css_import(line: &str, imports: &mut Vec<String>) {
354 if line.starts_with("@import") {
355 if let Some(path) = extract_first_quoted(line) {
356 imports.push(path);
357 }
358 }
359}
360
361fn parse_html_import(line: &str, imports: &mut Vec<String>) {
363 for attr in &["src=\"", "href=\""] {
364 if let Some(idx) = line.find(attr) {
365 let start = idx + attr.len();
366 if let Some(end) = line[start..].find('"') {
367 let path = &line[start..start + end];
368 if !path.is_empty() {
369 imports.push(path.to_string());
370 }
371 }
372 }
373 }
374}
375
376fn extract_quoted_string(line: &str, keyword: &str) -> Option<String> {
382 let idx = line.find(keyword)?;
383 let rest = &line[idx + keyword.len()..];
384 extract_first_quoted(rest)
385}
386
387fn extract_first_quoted(text: &str) -> Option<String> {
389 for quote in &['\'', '"'] {
390 if let Some(start) = text.find(*quote) {
391 let rest = &text[start + 1..];
392 if let Some(end) = rest.find(*quote) {
393 let val = &rest[..end];
394 if !val.is_empty() {
395 return Some(val.to_string());
396 }
397 }
398 }
399 }
400 None
401}
402
403fn extract_require_path(line: &str) -> Option<String> {
405 let idx = line.find("require(")?;
406 let rest = &line[idx + "require(".len()..];
407 let paren_end = rest.find(')')?;
409 let inner = &rest[..paren_end];
410 extract_first_quoted(inner)
411}
412
413fn resolve_import(raw: &str, _base_dir: &Path) -> Option<PathBuf> {
422 let trimmed = raw.trim();
423 if trimmed.is_empty() {
424 return None;
425 }
426
427 if trimmed.starts_with('.')
430 || trimmed.starts_with('/')
431 || trimmed.contains(".rs")
432 || trimmed.contains(".py")
433 || trimmed.contains(".js")
434 || trimmed.contains(".ts")
435 || trimmed.contains(".go")
436 || trimmed.contains(".rb")
437 || trimmed.contains(".h")
438 || trimmed.contains(".c")
439 || trimmed.contains(".css")
440 || trimmed.contains(".html")
441 {
442 let path = PathBuf::from(trimmed);
443 return Some(path);
444 }
445
446 if trimmed.starts_with("crate::") || trimmed.starts_with("super::") {
448 let converted = trimmed
449 .replace("crate::", "src/")
450 .replace("super::", "../")
451 .replace("::", "/");
452 return Some(PathBuf::from(converted));
453 }
454
455 if trimmed.contains('.') && !trimmed.contains('/') {
457 let converted = trimmed.replace('.', "/");
458 return Some(PathBuf::from(converted));
459 }
460
461 if trimmed.contains('/') {
463 return Some(PathBuf::from(trimmed));
464 }
465
466 Some(PathBuf::from(trimmed))
468}
469
470#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_new_mapper_is_empty() {
480 let mapper = DependencyMapper::new();
481 assert_eq!(mapper.file_count(), 0);
482 assert!(mapper.dependencies_of(Path::new("foo.rs")).is_empty());
483 assert!(mapper.dependents_of(Path::new("foo.rs")).is_empty());
484 }
485
486 #[test]
487 fn test_add_rust_file() {
488 let mut mapper = DependencyMapper::new();
489 let source = r#"
490use crate::engine;
491use crate::types;
492use std::collections::HashMap;
493
494pub fn main() {}
495"#;
496 mapper.add_file(Path::new("src/lib.rs"), source);
497 let deps = mapper.dependencies_of(Path::new("src/lib.rs"));
498 assert!(deps.iter().any(|p| p.to_string_lossy().contains("engine")));
499 assert!(deps.iter().any(|p| p.to_string_lossy().contains("types")));
500 }
501
502 #[test]
503 fn test_add_python_file() {
504 let mut mapper = DependencyMapper::new();
505 let source = r#"
506import os
507from typing import List
508from .utils import helper
509"#;
510 mapper.add_file(Path::new("app/main.py"), source);
511 let deps = mapper.dependencies_of(Path::new("app/main.py"));
512 assert!(!deps.is_empty());
513 assert!(deps.iter().any(|p| p.to_string_lossy() == "os"));
514 }
515
516 #[test]
517 fn test_add_javascript_file() {
518 let mut mapper = DependencyMapper::new();
519 let source = r#"
520import React from 'react';
521import { useState } from 'react';
522const fs = require('fs');
523import './styles.css';
524"#;
525 mapper.add_file(Path::new("src/app.js"), source);
526 let deps = mapper.dependencies_of(Path::new("src/app.js"));
527 assert!(deps.iter().any(|p| p.to_string_lossy() == "react"));
528 assert!(deps.iter().any(|p| p.to_string_lossy() == "fs"));
529 assert!(deps.iter().any(|p| p.to_string_lossy() == "./styles.css"));
530 }
531
532 #[test]
533 fn test_add_typescript_file() {
534 let mut mapper = DependencyMapper::new();
535 let source = r#"
536import { Component } from '@angular/core';
537import { MyService } from './my-service';
538export { default } from './utils';
539"#;
540 mapper.add_file(Path::new("src/app.ts"), source);
541 let deps = mapper.dependencies_of(Path::new("src/app.ts"));
542 assert!(deps.iter().any(|p| p.to_string_lossy() == "@angular/core"));
543 assert!(deps.iter().any(|p| p.to_string_lossy() == "./my-service"));
544 assert!(deps.iter().any(|p| p.to_string_lossy() == "./utils"));
545 }
546
547 #[test]
548 fn test_add_go_file() {
549 let mut mapper = DependencyMapper::new();
550 let source = r#"
551package main
552
553import "fmt"
554import "os"
555"#;
556 mapper.add_file(Path::new("main.go"), source);
557 let deps = mapper.dependencies_of(Path::new("main.go"));
558 assert!(deps.iter().any(|p| p.to_string_lossy() == "fmt"));
559 assert!(deps.iter().any(|p| p.to_string_lossy() == "os"));
560 }
561
562 #[test]
563 fn test_add_java_file() {
564 let mut mapper = DependencyMapper::new();
565 let source = r#"
566import java.util.List;
567import com.example.MyClass;
568"#;
569 mapper.add_file(Path::new("src/Main.java"), source);
570 let deps = mapper.dependencies_of(Path::new("src/Main.java"));
571 assert!(deps.iter().any(|p| p.to_string_lossy().contains("java/util/List")));
572 assert!(deps.iter().any(|p| p.to_string_lossy().contains("com/example/MyClass")));
573 }
574
575 #[test]
576 fn test_add_c_file() {
577 let mut mapper = DependencyMapper::new();
578 let source = r#"
579#include <stdio.h>
580#include "myheader.h"
581"#;
582 mapper.add_file(Path::new("src/main.c"), source);
583 let deps = mapper.dependencies_of(Path::new("src/main.c"));
584 assert!(deps.iter().any(|p| p.to_string_lossy() == "stdio.h"));
585 assert!(deps.iter().any(|p| p.to_string_lossy() == "myheader.h"));
586 }
587
588 #[test]
589 fn test_add_ruby_file() {
590 let mut mapper = DependencyMapper::new();
591 let source = r#"
592require 'json'
593require_relative 'helper'
594"#;
595 mapper.add_file(Path::new("lib/app.rb"), source);
596 let deps = mapper.dependencies_of(Path::new("lib/app.rb"));
597 assert!(deps.iter().any(|p| p.to_string_lossy() == "json"));
598 assert!(deps.iter().any(|p| p.to_string_lossy() == "helper"));
599 }
600
601 #[test]
602 fn test_add_csharp_file() {
603 let mut mapper = DependencyMapper::new();
604 let source = r#"
605using System;
606using System.Collections.Generic;
607"#;
608 mapper.add_file(Path::new("src/Program.cs"), source);
609 let deps = mapper.dependencies_of(Path::new("src/Program.cs"));
610 assert!(deps.iter().any(|p| p.to_string_lossy() == "System"));
611 assert!(deps.iter().any(|p| p.to_string_lossy().contains("System/Collections/Generic")));
612 }
613
614 #[test]
615 fn test_add_swift_file() {
616 let mut mapper = DependencyMapper::new();
617 let source = "import Foundation\nimport UIKit\n";
618 mapper.add_file(Path::new("Sources/App.swift"), source);
619 let deps = mapper.dependencies_of(Path::new("Sources/App.swift"));
620 assert!(deps.iter().any(|p| p.to_string_lossy() == "Foundation"));
621 assert!(deps.iter().any(|p| p.to_string_lossy() == "UIKit"));
622 }
623
624 #[test]
625 fn test_add_kotlin_file() {
626 let mut mapper = DependencyMapper::new();
627 let source = "import com.example.MyClass\nimport kotlin.collections.List\n";
628 mapper.add_file(Path::new("src/Main.kt"), source);
629 let deps = mapper.dependencies_of(Path::new("src/Main.kt"));
630 assert!(deps.iter().any(|p| p.to_string_lossy().contains("com/example/MyClass")));
631 }
632
633 #[test]
634 fn test_add_css_file() {
635 let mut mapper = DependencyMapper::new();
636 let source = "@import 'reset.css';\n@import url('theme.css');\n";
637 mapper.add_file(Path::new("styles/main.css"), source);
638 let deps = mapper.dependencies_of(Path::new("styles/main.css"));
639 assert!(deps.iter().any(|p| p.to_string_lossy().contains("reset.css")));
640 }
641
642 #[test]
643 fn test_add_html_file() {
644 let mut mapper = DependencyMapper::new();
645 let source = r#"
646<link href="styles.css" rel="stylesheet">
647<script src="app.js"></script>
648"#;
649 mapper.add_file(Path::new("index.html"), source);
650 let deps = mapper.dependencies_of(Path::new("index.html"));
651 assert!(deps.iter().any(|p| p.to_string_lossy() == "styles.css"));
652 assert!(deps.iter().any(|p| p.to_string_lossy() == "app.js"));
653 }
654
655 #[test]
656 fn test_reverse_dependencies() {
657 let mut mapper = DependencyMapper::new();
658
659 mapper.add_file(
660 Path::new("src/a.rs"),
661 "use crate::shared;\n",
662 );
663 mapper.add_file(
664 Path::new("src/b.rs"),
665 "use crate::shared;\n",
666 );
667
668 let revs = mapper.dependents_of(Path::new("src/shared"));
669 assert_eq!(revs.len(), 2);
670 }
671
672 #[test]
673 fn test_remove_file_cleans_edges() {
674 let mut mapper = DependencyMapper::new();
675 mapper.add_file(
676 Path::new("src/a.rs"),
677 "use crate::b;\n",
678 );
679 assert!(!mapper.dependencies_of(Path::new("src/a.rs")).is_empty());
680
681 mapper.remove_file(Path::new("src/a.rs"));
682 assert!(mapper.dependencies_of(Path::new("src/a.rs")).is_empty());
683 assert_eq!(mapper.file_count(), 0);
684 }
685
686 #[test]
687 fn test_incremental_update() {
688 let mut mapper = DependencyMapper::new();
689 mapper.add_file(Path::new("src/a.rs"), "use crate::old_dep;\n");
690 assert!(mapper
691 .dependencies_of(Path::new("src/a.rs"))
692 .iter()
693 .any(|p| p.to_string_lossy().contains("old_dep")));
694
695 mapper.add_file(Path::new("src/a.rs"), "use crate::new_dep;\n");
697 let deps = mapper.dependencies_of(Path::new("src/a.rs"));
698 assert!(deps.iter().any(|p| p.to_string_lossy().contains("new_dep")));
699 assert!(!deps.iter().any(|p| p.to_string_lossy().contains("old_dep")));
700 }
701
702 #[test]
703 fn test_summary_empty() {
704 let mapper = DependencyMapper::new();
705 assert_eq!(mapper.summary(Path::new("foo.rs")), "");
706 }
707
708 #[test]
709 fn test_summary_with_deps() {
710 let mut mapper = DependencyMapper::new();
711 mapper.add_file(
712 Path::new("src/main.rs"),
713 "use crate::engine;\nuse crate::types;\n",
714 );
715 let summary = mapper.summary(Path::new("src/main.rs"));
716 assert!(summary.starts_with("[deps]"));
717 assert!(summary.contains("imports:"));
718 }
719
720 #[test]
721 fn test_summary_with_reverse_deps() {
722 let mut mapper = DependencyMapper::new();
723 mapper.add_file(Path::new("src/a.rs"), "use crate::shared;\n");
724 mapper.add_file(Path::new("src/b.rs"), "use crate::shared;\n");
725
726 let summary = mapper.summary(Path::new("src/shared"));
727 assert!(summary.contains("imported by:"));
728 }
729
730 #[test]
731 fn test_detect_language_extensions() {
732 assert_eq!(detect_language(Path::new("foo.rs")).as_deref(), Some("rust"));
733 assert_eq!(detect_language(Path::new("foo.py")).as_deref(), Some("python"));
734 assert_eq!(detect_language(Path::new("foo.js")).as_deref(), Some("javascript"));
735 assert_eq!(detect_language(Path::new("foo.ts")).as_deref(), Some("typescript"));
736 assert_eq!(detect_language(Path::new("foo.go")).as_deref(), Some("go"));
737 assert_eq!(detect_language(Path::new("foo.java")).as_deref(), Some("java"));
738 assert_eq!(detect_language(Path::new("foo.c")).as_deref(), Some("c"));
739 assert_eq!(detect_language(Path::new("foo.cpp")).as_deref(), Some("cpp"));
740 assert_eq!(detect_language(Path::new("foo.rb")).as_deref(), Some("ruby"));
741 assert_eq!(detect_language(Path::new("foo.cs")).as_deref(), Some("csharp"));
742 assert_eq!(detect_language(Path::new("foo.kt")).as_deref(), Some("kotlin"));
743 assert_eq!(detect_language(Path::new("foo.swift")).as_deref(), Some("swift"));
744 assert_eq!(detect_language(Path::new("foo.css")).as_deref(), Some("css"));
745 assert_eq!(detect_language(Path::new("foo.html")).as_deref(), Some("html"));
746 assert_eq!(detect_language(Path::new("foo.sh")).as_deref(), Some("bash"));
747 assert_eq!(detect_language(Path::new("foo.toml")).as_deref(), Some("toml"));
748 assert_eq!(detect_language(Path::new("foo.yaml")).as_deref(), Some("yaml"));
749 assert_eq!(detect_language(Path::new("foo.json")).as_deref(), Some("json"));
750 assert_eq!(detect_language(Path::new("foo.xyz")), None);
751 }
752
753 #[test]
754 fn test_unknown_extension_no_imports() {
755 let mut mapper = DependencyMapper::new();
756 mapper.add_file(Path::new("data.xyz"), "some random content\n");
757 assert!(mapper.dependencies_of(Path::new("data.xyz")).is_empty());
758 }
759
760 #[test]
761 fn test_cpp_includes() {
762 let mut mapper = DependencyMapper::new();
763 let source = "#include <iostream>\n#include \"mylib.h\"\n";
764 mapper.add_file(Path::new("src/main.cpp"), source);
765 let deps = mapper.dependencies_of(Path::new("src/main.cpp"));
766 assert!(deps.iter().any(|p| p.to_string_lossy() == "iostream"));
767 assert!(deps.iter().any(|p| p.to_string_lossy() == "mylib.h"));
768 }
769
770 #[test]
771 fn test_python_from_import() {
772 let mut mapper = DependencyMapper::new();
773 let source = "from os.path import join\nfrom . import utils\n";
774 mapper.add_file(Path::new("app/main.py"), source);
775 let deps = mapper.dependencies_of(Path::new("app/main.py"));
776 assert!(deps.iter().any(|p| p.to_string_lossy().contains("os")));
777 assert!(deps.iter().any(|p| p.to_string_lossy() == "."));
778 }
779
780 #[test]
781 fn test_default_trait() {
782 let mapper = DependencyMapper::default();
783 assert_eq!(mapper.file_count(), 0);
784 }
785
786 #[test]
787 fn test_file_count() {
788 let mut mapper = DependencyMapper::new();
789 mapper.add_file(Path::new("a.rs"), "use crate::b;\n");
790 mapper.add_file(Path::new("b.rs"), "use crate::c;\n");
791 assert_eq!(mapper.file_count(), 2);
792 mapper.remove_file(Path::new("a.rs"));
793 assert_eq!(mapper.file_count(), 1);
794 }
795
796 #[test]
797 fn test_csharp_using_var_skipped() {
798 let mut mapper = DependencyMapper::new();
799 let source = "using System;\nusing var stream = new FileStream();\n";
800 mapper.add_file(Path::new("Program.cs"), source);
801 let deps = mapper.dependencies_of(Path::new("Program.cs"));
802 assert!(deps.iter().any(|p| p.to_string_lossy() == "System"));
804 assert!(!deps.iter().any(|p| p.to_string_lossy().contains("stream")));
805 }
806
807 #[test]
808 fn test_js_require_and_import() {
809 let mut mapper = DependencyMapper::new();
810 let source = r#"
811import defaultExport from './module';
812const path = require('path');
813"#;
814 mapper.add_file(Path::new("index.js"), source);
815 let deps = mapper.dependencies_of(Path::new("index.js"));
816 assert!(deps.iter().any(|p| p.to_string_lossy() == "./module"));
817 assert!(deps.iter().any(|p| p.to_string_lossy() == "path"));
818 }
819
820 #[test]
821 fn test_short_name() {
822 assert_eq!(short_name(Path::new("a.rs")), "a.rs");
823 assert_eq!(short_name(Path::new("src/lib.rs")), "src/lib.rs");
824 assert_eq!(short_name(Path::new("foo/bar/baz.rs")), "bar/baz.rs");
825 }
826}