1use anyhow::Result;
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10use walkdir::WalkDir;
11
12#[derive(Debug, Clone)]
13pub struct ImportStatement {
14 pub file_path: String,
15 pub line_number: usize,
16 pub line_content: String,
17 pub imported_path: String,
18}
19
20#[derive(Debug, Clone)]
22enum ImportPattern {
23 Rust,
25 JavaScript,
27 Python,
29 Go,
31 JavaLike,
33 CStyle,
35 Ruby,
37 Php,
39 Shell,
41 Css,
43}
44
45fn get_import_pattern(extension: &str) -> Option<ImportPattern> {
47 match extension {
48 "rs" => Some(ImportPattern::Rust),
49 "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => Some(ImportPattern::JavaScript),
50 "py" => Some(ImportPattern::Python),
51 "go" => Some(ImportPattern::Go),
52 "java" | "kt" | "scala" => Some(ImportPattern::JavaLike),
53 "cs" => Some(ImportPattern::JavaLike),
54 "c" | "cpp" | "cc" | "cxx" | "h" | "hpp" => Some(ImportPattern::CStyle),
55 "rb" => Some(ImportPattern::Ruby),
56 "php" => Some(ImportPattern::Php),
57 "sh" | "bash" => Some(ImportPattern::Shell),
58 "css" | "scss" | "less" => Some(ImportPattern::Css),
59 _ => None,
60 }
61}
62
63fn extract_rust_import(line: &str) -> Option<String> {
65 let trimmed = line.trim();
66
67 let import_part = trimmed.strip_prefix("use ")?.trim_end_matches(';').trim();
69
70 if import_part.starts_with("crate::")
72 || import_part.starts_with("super::")
73 || import_part.starts_with("self::")
74 {
75 let path = if let Some(brace_pos) = import_part.find('{') {
77 import_part[..brace_pos].trim()
78 } else if let Some(as_pos) = import_part.find(" as ") {
79 import_part[..as_pos].trim()
80 } else {
81 import_part
82 };
83
84 return Some(path.to_string());
85 }
86
87 None
88}
89
90fn extract_javascript_import(line: &str) -> Option<String> {
92 let trimmed = line.trim();
93
94 if trimmed.starts_with("import ") {
96 if let Some(from_pos) = trimmed.find(" from ") {
97 let after_from = &trimmed[from_pos + 6..].trim();
98 return extract_quoted_string(after_from);
99 }
100 if let Some(quote_pos) = trimmed.find(['"', '\'']) {
102 return extract_quoted_string(&trimmed[quote_pos..]);
103 }
104 }
105
106 if trimmed.starts_with("export ")
108 && trimmed.contains(" from ")
109 && let Some(from_pos) = trimmed.find(" from ")
110 {
111 let after_from = &trimmed[from_pos + 6..].trim();
112 return extract_quoted_string(after_from);
113 }
114
115 if trimmed.contains("require(")
117 && let Some(paren_pos) = trimmed.find("require(")
118 {
119 let after_paren = &trimmed[paren_pos + 8..];
120 return extract_quoted_string(after_paren);
121 }
122
123 None
124}
125
126fn extract_python_import(line: &str) -> Option<String> {
128 let trimmed = line.trim();
129
130 if trimmed.starts_with("from ")
132 && let Some(import_pos) = trimmed.find(" import ")
133 {
134 let module_path = trimmed[5..import_pos].trim();
135 if module_path.starts_with('.') {
137 return Some(module_path.to_string());
138 }
139 }
140
141 if let Some(import_part) = trimmed.strip_prefix("import ") {
143 let import_part = import_part.trim();
144 let module_path = if let Some(as_pos) = import_part.find(" as ") {
145 &import_part[..as_pos]
146 } else {
147 import_part
148 };
149
150 if module_path.starts_with('.') {
152 return Some(module_path.trim().to_string());
153 }
154 }
155
156 None
157}
158
159fn extract_go_import(line: &str) -> Option<String> {
161 let trimmed = line.trim();
162 let after_import = trimmed.strip_prefix("import ")?.trim();
163 extract_quoted_string(after_import)
164}
165
166fn extract_javalike_import(line: &str) -> Option<String> {
168 let trimmed = line.trim();
169
170 if let Some(import_part) = trimmed.strip_prefix("import ") {
172 let import_part = import_part.trim().trim_end_matches(';');
173 if import_part.starts_with("static ") {
175 return None;
176 }
177 return Some(import_part.to_string());
178 }
179
180 if !trimmed.contains('=')
182 && let Some(import_part) = trimmed.strip_prefix("using ")
183 {
184 let import_part = import_part.trim().trim_end_matches(';');
185 return Some(import_part.to_string());
186 }
187
188 None
189}
190
191fn extract_c_include(line: &str) -> Option<String> {
193 let trimmed = line.trim();
194 let after_include = trimmed.strip_prefix("#include ")?.trim();
195
196 if let Some(path) = extract_quoted_string(after_include) {
198 return Some(path);
199 }
200
201 if after_include.starts_with('<')
204 && after_include.contains('/')
205 && let Some(end) = after_include.find('>')
206 {
207 return Some(after_include[1..end].to_string());
208 }
209
210 None
211}
212
213fn extract_ruby_require(line: &str) -> Option<String> {
215 let trimmed = line.trim();
216
217 if let Some(after_require) = trimmed.strip_prefix("require_relative ") {
219 return extract_quoted_string(after_require.trim());
220 }
221
222 if let Some(after_require) = trimmed.strip_prefix("require ")
224 && let Some(path) = extract_quoted_string(after_require.trim())
225 && path.starts_with('.')
226 {
227 return Some(path);
228 }
229
230 None
231}
232
233fn extract_php_include(line: &str) -> Option<String> {
235 let trimmed = line.trim();
236
237 for keyword in ["require", "require_once", "include", "include_once"] {
238 if let Some(after_keyword) = trimmed.strip_prefix(keyword) {
239 let after_keyword = after_keyword.trim();
240 if let Some(path) = extract_quoted_string(after_keyword) {
241 return Some(path);
242 }
243 }
244 }
245
246 None
247}
248
249fn extract_shell_source(line: &str) -> Option<String> {
251 let trimmed = line.trim();
252
253 if let Some(path) = trimmed.strip_prefix("source ") {
255 let path = path.trim();
256 return extract_quoted_string(path).or_else(|| Some(path.to_string()));
257 }
258
259 if let Some(path) = trimmed.strip_prefix(". ")
260 && !trimmed.starts_with("..")
261 {
262 let path = path.trim();
263 return extract_quoted_string(path).or_else(|| Some(path.to_string()));
264 }
265
266 None
267}
268
269fn extract_css_import(line: &str) -> Option<String> {
271 let trimmed = line.trim();
272 let after_import = trimmed.strip_prefix("@import ")?.trim();
273 extract_quoted_string(after_import)
274}
275
276fn extract_quoted_string(s: &str) -> Option<String> {
278 let trimmed = s.trim();
279 let first_char = trimmed.chars().next()?;
280
281 if ['"', '\'', '`'].contains(&first_char)
283 && let Some(end) = trimmed[1..].find(first_char)
284 {
285 return Some(trimmed[1..end + 1].to_string());
286 }
287
288 None
289}
290
291fn extract_import(line: &str, pattern: &ImportPattern) -> Option<String> {
293 match pattern {
294 ImportPattern::Rust => extract_rust_import(line),
295 ImportPattern::JavaScript => extract_javascript_import(line),
296 ImportPattern::Python => extract_python_import(line),
297 ImportPattern::Go => extract_go_import(line),
298 ImportPattern::JavaLike => extract_javalike_import(line),
299 ImportPattern::CStyle => extract_c_include(line),
300 ImportPattern::Ruby => extract_ruby_require(line),
301 ImportPattern::Php => extract_php_include(line),
302 ImportPattern::Shell => extract_shell_source(line),
303 ImportPattern::Css => extract_css_import(line),
304 }
305}
306
307pub fn scan_file_for_imports(file_path: &Path) -> Result<Vec<ImportStatement>> {
309 let mut imports = Vec::new();
310
311 let extension = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
312 let pattern = match get_import_pattern(extension) {
313 Some(p) => p,
314 None => return Ok(imports), };
316
317 let content = fs::read_to_string(file_path)?;
318
319 for (line_number, line) in content.lines().enumerate() {
320 if let Some(imported_path) = extract_import(line, &pattern) {
321 imports.push(ImportStatement {
322 file_path: file_path.to_string_lossy().to_string(),
323 line_number: line_number + 1, line_content: line.trim().to_string(),
325 imported_path,
326 });
327 }
328 }
329
330 Ok(imports)
331}
332
333pub fn build_file_map(base_path: &Path) -> HashMap<String, PathBuf> {
335 let mut file_map = HashMap::new();
336
337 let skip_dirs = [
338 "node_modules",
339 "target",
340 "dist",
341 "build",
342 ".git",
343 ".svn",
344 ".hg",
345 "vendor",
346 "__pycache__",
347 ".next",
348 ".nuxt",
349 "coverage",
350 ];
351
352 for entry in WalkDir::new(base_path)
353 .into_iter()
354 .filter_entry(|e| {
355 if e.file_type().is_dir() {
356 let dir_name = e.file_name().to_string_lossy();
357 !skip_dirs.contains(&dir_name.as_ref())
358 } else {
359 true
360 }
361 })
362 .filter_map(|e| e.ok())
363 {
364 if entry.file_type().is_file() {
365 let path = entry.path();
366 if let Ok(relative_path) = path.strip_prefix(base_path) {
367 let key = relative_path.to_string_lossy().to_string();
368 file_map.insert(key, path.to_path_buf());
369 }
370 }
371 }
372
373 file_map
374}
375
376pub fn resolve_import_path(
378 import_path: &str,
379 source_file: &Path,
380 base_path: &Path,
381 file_map: &HashMap<String, PathBuf>,
382) -> Option<PathBuf> {
383 let source_dir = source_file.parent()?;
384
385 if import_path.starts_with('.') {
387 resolve_relative_import(import_path, source_dir, base_path)
389 } else if import_path.contains("::") {
390 resolve_rust_module_path(import_path, source_file, base_path, file_map)
392 } else if import_path.contains('/') {
393 resolve_path_import(import_path, base_path, file_map)
395 } else {
396 None
398 }
399}
400
401fn resolve_relative_import(
403 import_path: &str,
404 source_dir: &Path,
405 base_path: &Path,
406) -> Option<PathBuf> {
407 let _import_path_clean = import_path
408 .trim_start_matches("./")
409 .trim_start_matches("../");
410
411 let candidate = source_dir.join(import_path);
413
414 let extensions = [
416 "", ".ts", ".tsx", ".js", ".jsx", ".rs", ".py", ".go", ".java", ".rb", ".php",
417 ];
418
419 for ext in extensions {
420 let path_with_ext = if ext.is_empty() {
421 candidate.clone()
422 } else {
423 let mut path_str = candidate.to_string_lossy().to_string();
425 path_str.push_str(ext);
426 PathBuf::from(path_str)
427 };
428
429 if path_with_ext.exists() && path_with_ext.starts_with(base_path) {
430 if let Ok(canonical) = path_with_ext.canonicalize() {
432 return Some(canonical);
433 }
434 return Some(path_with_ext);
435 }
436
437 let candidate_as_dir = candidate.clone();
439 if candidate_as_dir.is_dir() {
440 for index_name in ["index", "mod", "__init__"] {
441 for idx_ext in ["ts", "tsx", "js", "jsx", "rs", "py"] {
442 let index_path = candidate_as_dir.join(format!("{}.{}", index_name, idx_ext));
443 if index_path.exists() {
444 return Some(index_path);
445 }
446 }
447 }
448 }
449 }
450
451 None
452}
453
454fn resolve_rust_module_path(
456 import_path: &str,
457 source_file: &Path,
458 base_path: &Path,
459 _file_map: &HashMap<String, PathBuf>,
460) -> Option<PathBuf> {
461 let path_str = if let Some(stripped) = import_path.strip_prefix("crate::") {
463 stripped
464 } else if let Some(stripped) = import_path.strip_prefix("super::") {
465 return resolve_super_path(stripped, source_file, base_path);
467 } else {
468 import_path.strip_prefix("self::")?
469 };
470
471 let path_parts: Vec<&str> = path_str.split("::").collect();
473
474 let src_dir = find_src_directory(base_path)?;
476
477 let mut module_path = src_dir.clone();
479 for part in &path_parts {
480 module_path = module_path.join(part);
481 }
482
483 if module_path.with_extension("rs").exists() {
484 return Some(module_path.with_extension("rs"));
485 }
486
487 let mod_path = module_path.join("mod.rs");
489 if mod_path.exists() {
490 return Some(mod_path);
491 }
492
493 None
494}
495
496fn resolve_super_path(
498 remaining_path: &str,
499 source_file: &Path,
500 base_path: &Path,
501) -> Option<PathBuf> {
502 let source_dir = source_file.parent()?;
503 let parent_dir = source_dir.parent()?;
504
505 if !parent_dir.starts_with(base_path) {
506 return None;
507 }
508
509 let path_parts: Vec<&str> = remaining_path.split("::").collect();
510 let mut module_path = parent_dir.to_path_buf();
511
512 for part in &path_parts {
513 module_path = module_path.join(part);
514 }
515
516 if module_path.with_extension("rs").exists() {
517 return Some(module_path.with_extension("rs"));
518 }
519
520 let mod_path = module_path.join("mod.rs");
521 if mod_path.exists() {
522 return Some(mod_path);
523 }
524
525 None
526}
527
528fn find_src_directory(base_path: &Path) -> Option<PathBuf> {
530 let src_dir = base_path.join("src");
531 if src_dir.is_dir() {
532 return Some(src_dir);
533 }
534
535 for entry in (fs::read_dir(base_path).ok()?).flatten() {
537 let path = entry.path();
538 if path.is_dir() {
539 let nested_src = path.join("src");
540 if nested_src.is_dir() {
541 return Some(nested_src);
542 }
543 }
544 }
545
546 None
547}
548
549fn resolve_path_import(
551 import_path: &str,
552 _base_path: &Path,
553 file_map: &HashMap<String, PathBuf>,
554) -> Option<PathBuf> {
555 if let Some(path) = file_map.get(import_path) {
557 return Some(path.clone());
558 }
559
560 for ext in [
562 "ts", "tsx", "js", "jsx", "rs", "py", "go", "java", "rb", "php",
563 ] {
564 let with_ext = format!("{}.{}", import_path, ext);
565 if let Some(path) = file_map.get(&with_ext) {
566 return Some(path.clone());
567 }
568 }
569
570 for index_name in ["index", "mod", "__init__"] {
572 for ext in ["ts", "tsx", "js", "jsx", "rs", "py"] {
573 let index_path = format!("{}/{}.{}", import_path, index_name, ext);
574 if let Some(path) = file_map.get(&index_path) {
575 return Some(path.clone());
576 }
577 }
578 }
579
580 None
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn test_extract_rust_import() {
589 assert_eq!(
590 extract_rust_import("use crate::models::Feature;"),
591 Some("crate::models::Feature".to_string())
592 );
593 assert_eq!(
594 extract_rust_import("use super::helper;"),
595 Some("super::helper".to_string())
596 );
597 assert_eq!(
598 extract_rust_import("use self::utils;"),
599 Some("self::utils".to_string())
600 );
601 }
602
603 #[test]
604 fn test_extract_javascript_import() {
605 assert_eq!(
606 extract_javascript_import("import { Feature } from './models';"),
607 Some("./models".to_string())
608 );
609 assert_eq!(
610 extract_javascript_import("const x = require('../utils');"),
611 Some("../utils".to_string())
612 );
613 assert_eq!(
614 extract_javascript_import("export { Feature } from './models';"),
615 Some("./models".to_string())
616 );
617 }
618
619 #[test]
620 fn test_extract_python_import() {
621 assert_eq!(
622 extract_python_import("from .models import Feature"),
623 Some(".models".to_string())
624 );
625 assert_eq!(
626 extract_python_import("from ..utils import helper"),
627 Some("..utils".to_string())
628 );
629 }
630
631 #[test]
632 fn test_extract_quoted_string() {
633 assert_eq!(
634 extract_quoted_string("\"./path/to/file\""),
635 Some("./path/to/file".to_string())
636 );
637 assert_eq!(
638 extract_quoted_string("'./path/to/file'"),
639 Some("./path/to/file".to_string())
640 );
641 }
642}