1use crate::cli::Config;
7use crate::core::cache::FileCache;
8use crate::core::semantic::function_call_index::FunctionCallIndex;
9use crate::core::semantic::path_validator::validate_import_path;
10use crate::core::semantic::type_resolver::{ResolutionLimits, TypeResolver};
11use crate::core::walker::{perform_semantic_analysis, walk_directory, FileInfo};
12use crate::utils::error::ContextCreatorError;
13use ignore::gitignore::{Gitignore, GitignoreBuilder};
14use std::collections::{HashMap, HashSet, VecDeque};
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17
18fn build_ignore_matcher(
20 walk_options: &crate::core::walker::WalkOptions,
21 base_path: &Path,
22) -> Option<Gitignore> {
23 if walk_options.ignore_patterns.is_empty() {
24 return None;
25 }
26
27 let mut builder = GitignoreBuilder::new(base_path);
28
29 for pattern in &walk_options.ignore_patterns {
31 let _ = builder.add_line(None, pattern);
33 }
34
35 builder.build().ok()
36}
37
38pub fn detect_project_root(start_path: &Path) -> PathBuf {
40 let start_dir = if start_path.is_file() {
42 start_path.parent().unwrap_or(start_path)
43 } else {
44 start_path
45 };
46
47 let mut current = start_dir;
49 loop {
50 if current.join(".git").exists() {
51 return current.to_path_buf();
52 }
53 if let Some(parent) = current.parent() {
54 current = parent;
55 } else {
56 break;
57 }
58 }
59
60 current = start_dir;
62 loop {
63 if current.join("Cargo.toml").exists() {
65 return current.to_path_buf();
66 }
67 if current.join("package.json").exists() {
69 return current.to_path_buf();
70 }
71 if current.join("pyproject.toml").exists() || current.join("setup.py").exists() {
73 return current.to_path_buf();
74 }
75 if current.join("README.md").exists() || current.join("readme.md").exists() {
77 return current.to_path_buf();
78 }
79
80 if let Some(parent) = current.parent() {
81 current = parent;
82 } else {
83 break;
84 }
85 }
86
87 start_dir.to_path_buf()
89}
90
91pub fn expand_file_list_with_context(
97 files_map: HashMap<PathBuf, FileInfo>,
98 config: &Config,
99 cache: &Arc<FileCache>,
100 walk_options: &crate::core::walker::WalkOptions,
101 all_files_context: &HashMap<PathBuf, FileInfo>,
102) -> Result<HashMap<PathBuf, FileInfo>, ContextCreatorError> {
103 expand_file_list_internal(
104 files_map,
105 config,
106 cache,
107 walk_options,
108 Some(all_files_context),
109 )
110}
111
112pub fn expand_file_list(
117 files_map: HashMap<PathBuf, FileInfo>,
118 config: &Config,
119 cache: &Arc<FileCache>,
120 walk_options: &crate::core::walker::WalkOptions,
121) -> Result<HashMap<PathBuf, FileInfo>, ContextCreatorError> {
122 expand_file_list_internal(files_map, config, cache, walk_options, None)
123}
124
125fn expand_file_list_internal(
127 files_map: HashMap<PathBuf, FileInfo>,
128 config: &Config,
129 cache: &Arc<FileCache>,
130 walk_options: &crate::core::walker::WalkOptions,
131 all_files_context: Option<&HashMap<PathBuf, FileInfo>>,
132) -> Result<HashMap<PathBuf, FileInfo>, ContextCreatorError> {
133 if !config.trace_imports && !config.include_callers && !config.include_types {
135 return Ok(files_map);
136 }
137
138 let mut files_map = files_map;
139
140 let project_root = if let Some((first_path, _)) = files_map.iter().next() {
142 detect_project_root(first_path)
143 } else {
144 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
146 };
147
148 if config.trace_imports || config.include_types {
150 use crate::core::semantic::analyzer::SemanticContext;
151 use crate::core::semantic::get_analyzer_for_file;
152
153 for (path, file_info) in files_map.iter_mut() {
154 if !file_info.imports.is_empty() || !file_info.type_references.is_empty() {
156 continue;
157 }
158
159 if let Ok(content) = cache.get_or_load(path) {
161 if let Ok(Some(analyzer)) = get_analyzer_for_file(path) {
162 let context = SemanticContext {
163 current_file: path.clone(),
164 base_dir: project_root.clone(),
165 current_depth: 0,
166 max_depth: config.semantic_depth,
167 visited_files: HashSet::new(),
168 };
169
170 if let Ok(analysis) = analyzer.analyze_file(path, &content, &context) {
171 file_info.imports = analysis
173 .imports
174 .iter()
175 .filter_map(|imp| {
176 resolve_import_to_path(&imp.module, path, &project_root)
178 })
179 .collect();
180 file_info.function_calls = analysis.function_calls;
181 file_info.type_references = analysis.type_references;
182
183 tracing::debug!(
184 "Initial file {} analyzed: {} imports, {} types, {} calls",
185 path.display(),
186 file_info.imports.len(),
187 file_info.type_references.len(),
188 file_info.function_calls.len()
189 );
190 }
191 }
192 }
193 }
194 }
195
196 let mut work_queue = VecDeque::new();
198 let mut visited_paths = HashSet::new();
199 let mut files_to_add = Vec::new();
200
201 for (path, file_info) in &files_map {
203 visited_paths.insert(path.clone());
204
205 if config.include_types && !file_info.type_references.is_empty() {
207 work_queue.push_back((path.clone(), file_info.clone(), ExpansionReason::Types, 0));
208 }
209 if config.trace_imports && !file_info.imports.is_empty() {
210 tracing::debug!(
211 "Queuing {} for import expansion (has {} imports)",
212 path.display(),
213 file_info.imports.len()
214 );
215 work_queue.push_back((path.clone(), file_info.clone(), ExpansionReason::Imports, 0));
216 }
217 }
218
219 if config.include_callers {
221 let project_files = if let Some(context) = all_files_context {
222 context.values().cloned().collect()
224 } else {
225 let mut project_walk_options = walk_options.clone();
228 project_walk_options.include_patterns.clear(); let mut all_project_files = walk_directory(&project_root, project_walk_options)
231 .map_err(|e| ContextCreatorError::ContextGenerationError(e.to_string()))?;
232
233 perform_semantic_analysis(&mut all_project_files, config, cache)
235 .map_err(|e| ContextCreatorError::ContextGenerationError(e.to_string()))?;
236
237 all_project_files
238 };
239
240 let function_call_index = FunctionCallIndex::build(&project_files);
242
243 let initial_files: Vec<PathBuf> = files_map.keys().cloned().collect();
245 let caller_paths = function_call_index.find_callers_of_files(&initial_files);
246
247 for caller_path in caller_paths {
249 if !visited_paths.contains(&caller_path) {
250 let should_ignore = walk_options.ignore_patterns.iter().any(|pattern| {
254 glob::Pattern::new(pattern)
255 .ok()
256 .is_some_and(|p| p.matches_path(&caller_path))
257 });
258
259 if !should_ignore {
260 let caller_info = if let Some(context) = all_files_context {
262 context.get(&caller_path).cloned()
263 } else {
264 project_files
266 .iter()
267 .find(|f| f.path == caller_path)
268 .cloned()
269 };
270
271 if let Some(caller_info) = caller_info {
272 visited_paths.insert(caller_path.clone());
273 files_to_add.push((caller_path, caller_info));
274 }
275 }
276 }
277 }
278 }
279
280 let resolution_limits = ResolutionLimits {
282 max_depth: config.semantic_depth,
283 max_visited_types: 1000, max_resolution_time: std::time::Duration::from_secs(30),
285 };
286 let mut type_resolver = TypeResolver::with_limits(resolution_limits);
287
288 while let Some((source_path, source_file, reason, depth)) = work_queue.pop_front() {
292 if depth > config.semantic_depth {
294 tracing::debug!(
295 "Skipping {} (depth {} > limit {})",
296 source_path.display(),
297 depth,
298 config.semantic_depth
299 );
300 continue;
301 }
302
303 match reason {
304 ExpansionReason::Types => {
305 tracing::debug!(
307 "Processing type references from {} (depth {})",
308 source_path.display(),
309 depth
310 );
311 for type_ref in &source_file.type_references {
312 if type_ref.is_external {
314 continue;
315 }
316
317 let mut type_ref_copy = type_ref.clone();
319
320 if let Some(ref module) = type_ref_copy.module {
322 if module.ends_with(&format!("::{}", type_ref_copy.name)) {
323 let corrected_module = module
325 .strip_suffix(&format!("::{}", type_ref_copy.name))
326 .unwrap_or(module);
327 type_ref_copy.module = Some(corrected_module.to_string());
328 }
329 }
330
331 let type_ref = &type_ref_copy;
332
333 tracing::debug!(
334 " Type reference: {} (module: {:?}, definition_path: {:?}, is_external: {})",
335 type_ref.name,
336 type_ref.module,
337 type_ref.definition_path,
338 type_ref.is_external
339 );
340
341 match type_resolver.resolve_with_limits(type_ref, depth) {
343 Err(e) => {
344 if config.verbose > 0 {
346 eprintln!("⚠️ Type resolution limited: {e}");
347 }
348 continue;
349 }
350 Ok(_) => {
351 }
353 }
354
355 if let Some(ref def_path) = type_ref.definition_path {
357 tracing::debug!(" Type has definition_path: {}", def_path.display());
358 if !visited_paths.contains(def_path) && def_path.exists() {
359 match validate_import_path(&project_root, def_path) {
361 Ok(validated_path) => {
362 tracing::debug!(
363 " Adding type definition file: {}",
364 validated_path.display()
365 );
366 visited_paths.insert(validated_path.clone());
367
368 let mut file_info =
370 create_file_info_for_path(&validated_path, &source_path)?;
371
372 if depth + 1 < config.semantic_depth {
374 if let Ok(content) = cache.get_or_load(&validated_path) {
375 use crate::core::semantic::analyzer::SemanticContext;
376 use crate::core::semantic::get_analyzer_for_file;
377
378 if let Ok(Some(analyzer)) =
379 get_analyzer_for_file(&validated_path)
380 {
381 let context = SemanticContext::new(
382 validated_path.clone(),
383 project_root.clone(),
384 config.semantic_depth,
385 );
386
387 if let Ok(analysis) = analyzer.analyze_file(
388 &validated_path,
389 &content,
390 &context,
391 ) {
392 file_info.type_references =
394 analysis.type_references;
395
396 if !file_info.type_references.is_empty() {
398 work_queue.push_back((
399 validated_path.clone(),
400 file_info.clone(),
401 ExpansionReason::Types,
402 depth + 1,
403 ));
404 }
405 }
406 }
407 }
408 }
409
410 files_to_add.push((validated_path.clone(), file_info));
411 }
412 Err(_) => {
413 tracing::debug!(
415 " Path validation failed for: {}",
416 def_path.display()
417 );
418 }
419 }
420 }
421 } else {
422 tracing::debug!(
423 " No definition_path, attempting to find type definition file"
424 );
425 let module_name = type_ref.module.as_deref();
428
429 tracing::debug!(
430 " Looking for type {} with module {:?}",
431 type_ref.name,
432 module_name
433 );
434 if let Some(def_path) = find_type_definition_file(
435 &type_ref.name,
436 module_name,
437 &source_path,
438 cache,
439 ) {
440 tracing::debug!(
441 " Found type definition file: {}",
442 def_path.display()
443 );
444 if !visited_paths.contains(&def_path) {
445 match validate_import_path(&project_root, &def_path) {
447 Ok(validated_path) => {
448 tracing::debug!(
449 " Adding found type definition file: {}",
450 validated_path.display()
451 );
452 visited_paths.insert(validated_path.clone());
453
454 let mut file_info = create_file_info_for_path(
456 &validated_path,
457 &source_path,
458 )?;
459
460 if depth + 1 < config.semantic_depth {
462 if let Ok(content) = cache.get_or_load(&validated_path)
463 {
464 use crate::core::semantic::analyzer::SemanticContext;
465 use crate::core::semantic::get_analyzer_for_file;
466
467 if let Ok(Some(analyzer)) =
468 get_analyzer_for_file(&validated_path)
469 {
470 let context = SemanticContext::new(
471 validated_path.clone(),
472 project_root.clone(),
473 config.semantic_depth,
474 );
475
476 if let Ok(analysis) = analyzer.analyze_file(
477 &validated_path,
478 &content,
479 &context,
480 ) {
481 file_info.type_references =
483 analysis.type_references;
484
485 if !file_info.type_references.is_empty() {
487 work_queue.push_back((
488 validated_path.clone(),
489 file_info.clone(),
490 ExpansionReason::Types,
491 depth + 1,
492 ));
493 }
494 }
495 }
496 }
497 }
498
499 files_to_add.push((validated_path, file_info));
500 }
501 Err(_) => {
502 tracing::debug!(
504 " Path validation failed for found file: {}",
505 def_path.display()
506 );
507 }
508 }
509 }
510 } else {
511 tracing::debug!(
512 " Could not find type definition file for: {}",
513 type_ref.name
514 );
515 }
516 }
517 }
518 }
519 ExpansionReason::Imports => {
520 for import_path in &source_file.imports {
522 if !import_path.exists() {
524 continue;
525 }
526
527 let canonical_import = import_path
529 .canonicalize()
530 .unwrap_or_else(|_| import_path.clone());
531 if visited_paths.contains(import_path)
532 || visited_paths.contains(&canonical_import)
533 {
534 continue;
535 }
536
537 match validate_import_path(&project_root, import_path) {
539 Ok(validated_path) => {
540 visited_paths.insert(validated_path.clone());
541
542 if source_path.extension() == Some(std::ffi::OsStr::new("rs")) {
544 if let Some(parent) = validated_path.parent() {
546 let lib_rs = parent.join("lib.rs");
547 if lib_rs.exists()
548 && lib_rs != validated_path
549 && !visited_paths.contains(&lib_rs)
550 {
551 if let Ok(lib_content) = cache.get_or_load(&lib_rs) {
553 let module_name = validated_path
554 .file_stem()
555 .and_then(|s| s.to_str())
556 .unwrap_or("");
557 if lib_content.contains(&format!("mod {module_name};"))
558 || lib_content
559 .contains(&format!("pub mod {module_name};"))
560 {
561 visited_paths.insert(lib_rs.clone());
563 if let Some(context) = all_files_context {
564 if let Some(lib_file) = context.get(&lib_rs) {
565 files_to_add.push((
566 lib_rs.clone(),
567 lib_file.clone(),
568 ));
569 }
570 } else {
571 let lib_info = create_file_info_for_path(
572 &lib_rs,
573 &source_path,
574 )?;
575 files_to_add.push((lib_rs, lib_info));
576 }
577 }
578 }
579 }
580 }
581 }
582
583 let mut file_info = if let Some(context) = all_files_context {
585 let lookup_path = validated_path
587 .canonicalize()
588 .unwrap_or_else(|_| validated_path.clone());
589
590 let context_file = context
592 .get(&lookup_path)
593 .or_else(|| context.get(&validated_path));
594
595 if let Some(context_file) = context_file {
596 let mut file = context_file.clone();
598 file.imported_by.push(source_path.clone());
600 file
601 } else {
602 let mut file =
604 create_file_info_for_path(&validated_path, &source_path)?;
605 file.imported_by.push(source_path.clone());
606 file
607 }
608 } else {
609 let mut file =
611 create_file_info_for_path(&validated_path, &source_path)?;
612 file.imported_by.push(source_path.clone());
613 file
614 };
615
616 if depth + 1 < config.semantic_depth {
618 if !file_info.imports.is_empty()
620 || !file_info.type_references.is_empty()
621 || !file_info.function_calls.is_empty()
622 {
623 if !file_info.imports.is_empty() {
625 work_queue.push_back((
626 validated_path.clone(),
627 file_info.clone(),
628 ExpansionReason::Imports,
629 depth + 1,
630 ));
631 }
632 if config.include_types && !file_info.type_references.is_empty()
633 {
634 work_queue.push_back((
635 validated_path.clone(),
636 file_info.clone(),
637 ExpansionReason::Types,
638 depth + 1,
639 ));
640 }
641 } else if let Ok(content) = cache.get_or_load(&validated_path) {
642 use crate::core::semantic::analyzer::SemanticContext;
644 use crate::core::semantic::get_analyzer_for_file;
645
646 if let Ok(Some(analyzer)) =
647 get_analyzer_for_file(&validated_path)
648 {
649 let context = SemanticContext::new(
650 validated_path.clone(),
651 project_root.clone(),
652 config.semantic_depth,
653 );
654
655 if let Ok(analysis) = analyzer.analyze_file(
656 &validated_path,
657 &content,
658 &context,
659 ) {
660 file_info.imports = analysis
662 .imports
663 .iter()
664 .filter_map(|imp| {
665 resolve_import_to_path(
667 &imp.module,
668 &validated_path,
669 &project_root,
670 )
671 })
672 .collect();
673 file_info.function_calls = analysis.function_calls;
674 file_info.type_references = analysis.type_references;
675
676 if !file_info.imports.is_empty() {
678 work_queue.push_back((
679 validated_path.clone(),
680 file_info.clone(),
681 ExpansionReason::Imports,
682 depth + 1,
683 ));
684 }
685 }
686 }
687 }
688 }
689
690 files_to_add.push((validated_path, file_info));
691 }
692 Err(_) => {
693 if config.verbose > 0 {
695 eprintln!(
696 "⚠️ Skipping invalid import path: {}",
697 import_path.display()
698 );
699 }
700 }
701 }
702 }
703 }
704 }
705 }
706
707 for (path, file_info) in files_to_add {
709 files_map.insert(path, file_info);
710 }
711
712 update_import_relationships(&mut files_map);
714
715 if let Some(ignore_matcher) = build_ignore_matcher(walk_options, &project_root) {
717 let ignored_files: Vec<PathBuf> = files_map
720 .keys()
721 .filter(|path| {
722 ignore_matcher.matched(path, path.is_dir()).is_ignore()
724 })
725 .cloned()
726 .collect();
727
728 for ignored_path in ignored_files {
729 files_map.remove(&ignored_path);
730 }
731 }
732
733 Ok(files_map)
734}
735
736#[derive(Debug, Clone, Copy)]
738enum ExpansionReason {
739 Types,
740 Imports,
741}
742
743fn create_file_info_for_path(
745 path: &PathBuf,
746 source_path: &Path,
747) -> Result<FileInfo, ContextCreatorError> {
748 use crate::utils::file_ext::FileType;
749 use std::fs;
750
751 let metadata = fs::metadata(path)?;
752 let file_type = FileType::from_path(path);
753
754 let relative_path = path
756 .strip_prefix(common_ancestor(path, source_path))
757 .unwrap_or(path)
758 .to_path_buf();
759
760 Ok(FileInfo {
761 path: path.clone(),
762 relative_path,
763 size: metadata.len(),
764 file_type,
765 priority: 1.0, imports: Vec::new(),
767 imported_by: vec![source_path.to_path_buf()], function_calls: Vec::new(),
769 type_references: Vec::new(),
770 exported_functions: Vec::new(),
771 })
772}
773
774fn common_ancestor(path1: &Path, path2: &Path) -> PathBuf {
776 use std::collections::HashSet;
777
778 let ancestors1: HashSet<&Path> = path1.ancestors().collect();
780
781 for ancestor in path2.ancestors() {
784 if ancestors1.contains(ancestor) {
785 return ancestor.to_path_buf();
786 }
787 }
788
789 #[cfg(windows)]
792 {
793 if let Some(root) = path1.ancestors().last() {
795 root.to_path_buf()
796 } else {
797 PathBuf::from("C:\\")
798 }
799 }
800 #[cfg(not(windows))]
801 {
802 PathBuf::from("/")
803 }
804}
805
806fn file_contains_definition(path: &Path, content: &str, type_name: &str) -> bool {
808 let language = match path.extension().and_then(|s| s.to_str()) {
810 Some("rs") => Some(tree_sitter_rust::language()),
811 Some("py") => Some(tree_sitter_python::language()),
812 Some("ts") | Some("tsx") => Some(tree_sitter_typescript::language_typescript()),
813 Some("js") | Some("jsx") => Some(tree_sitter_javascript::language()),
814 _ => None,
815 };
816
817 if let Some(language) = language {
818 let mut parser = tree_sitter::Parser::new();
819 if parser.set_language(language).is_err() {
820 return false;
821 }
822
823 if let Some(tree) = parser.parse(content, None) {
824 let query_text = match path.extension().and_then(|s| s.to_str()) {
826 Some("rs") => {
827 r#"
828 [
829 (struct_item name: (type_identifier) @name)
830 (enum_item name: (type_identifier) @name)
831 (trait_item name: (type_identifier) @name)
832 (type_item name: (type_identifier) @name)
833 (union_item name: (type_identifier) @name)
834 ]
835 "#
836 }
837 Some("py") => {
838 r#"
839 [
840 (class_definition name: (identifier) @name)
841 (function_definition name: (identifier) @name)
842 ]
843 "#
844 }
845 Some("ts") | Some("tsx") => {
846 r#"
847 [
848 (interface_declaration name: (type_identifier) @name)
849 (type_alias_declaration name: (type_identifier) @name)
850 (class_declaration name: (type_identifier) @name)
851 (enum_declaration name: (identifier) @name)
852 ]
853 "#
854 }
855 Some("js") | Some("jsx") => {
856 r#"
857 [
858 (class_declaration name: (identifier) @name)
859 (function_declaration name: (identifier) @name)
860 ]
861 "#
862 }
863 _ => return false,
864 };
865
866 if let Ok(query) = tree_sitter::Query::new(language, query_text) {
867 let mut cursor = tree_sitter::QueryCursor::new();
868 let matches = cursor.matches(&query, tree.root_node(), content.as_bytes());
869
870 for m in matches {
872 for capture in m.captures {
873 if let Ok(captured_text) = capture.node.utf8_text(content.as_bytes()) {
874 if captured_text == type_name {
875 return true;
876 }
877 }
878 }
879 }
880 return false;
881 }
882 }
883 }
884 false
885}
886
887fn find_type_definition_file(
889 type_name: &str,
890 module_name: Option<&str>,
891 source_file: &Path,
892 cache: &FileCache,
893) -> Option<PathBuf> {
894 tracing::debug!(
895 "find_type_definition_file: type_name={}, module_name={:?}, source_file={}",
896 type_name,
897 module_name,
898 source_file.display()
899 );
900 let source_dir = source_file.parent()?;
902
903 let mut project_root = source_dir;
905 while let Some(parent) = project_root.parent() {
906 if parent.join("Cargo.toml").exists() || parent.join("src").exists() {
908 project_root = parent;
909 break;
910 }
911 if project_root.file_name() == Some(std::ffi::OsStr::new("src")) {
913 project_root = parent;
914 break;
915 }
916 project_root = parent;
917 }
918
919 let type_name_lower = type_name.to_lowercase();
921
922 let mut patterns = vec![
924 format!("{type_name_lower}.rs"),
926 format!("{type_name_lower}.py"),
927 format!("{type_name_lower}.ts"),
928 format!("{type_name_lower}.js"),
929 format!("{type_name_lower}.tsx"),
930 format!("{type_name_lower}.jsx"),
931 "types.rs".to_string(),
933 "types.py".to_string(),
934 "types.ts".to_string(),
935 "types.js".to_string(),
936 "mod.rs".to_string(),
938 "index.ts".to_string(),
939 "index.js".to_string(),
940 "__init__.py".to_string(),
941 format!("{type_name_lower}_types.rs"),
943 format!("{type_name_lower}_type.rs"),
944 format!("{type_name_lower}s.rs"), ];
946
947 if let Some(module) = module_name {
949 if module.starts_with("crate::") {
951 let relative_path = module.strip_prefix("crate::").unwrap();
952 let module_path = relative_path.replace("::", "/");
954
955 patterns.insert(0, format!("src/{module_path}.rs"));
957 patterns.insert(1, format!("src/{module_path}/mod.rs"));
958 patterns.insert(2, format!("{module_path}.rs"));
959 patterns.insert(3, format!("{module_path}/mod.rs"));
960 } else if module.contains("::") {
961 let module_path = module.replace("::", "/");
963 patterns.insert(0, format!("{module_path}.rs"));
964 patterns.insert(1, format!("{module_path}/mod.rs"));
965 } else {
966 let module_lower = module.to_lowercase();
968 patterns.insert(0, format!("{module_lower}.rs"));
969 patterns.insert(1, format!("{module_lower}.py"));
970 patterns.insert(2, format!("{module_lower}.ts"));
971 patterns.insert(3, format!("{module_lower}.js"));
972 patterns.insert(4, format!("{module_lower}.tsx"));
973 patterns.insert(5, format!("{module_lower}.jsx"));
974 patterns.insert(6, format!("{module}.rs")); patterns.insert(7, format!("{module}.py"));
976 patterns.insert(8, format!("{module}.ts"));
977 patterns.insert(9, format!("{module}.js"));
978 }
979 }
980
981 for pattern in &patterns {
983 let candidate = source_dir.join(pattern);
984 if candidate.exists() {
985 if let Ok(content) = cache.get_or_load(&candidate) {
987 if file_contains_definition(&candidate, &content, type_name) {
989 return Some(candidate);
990 }
991 }
992 }
993 }
994
995 if let Some(parent_dir) = source_dir.parent() {
997 for pattern in &patterns {
998 let candidate = parent_dir.join(pattern);
999 if candidate.exists() {
1000 if let Ok(content) = cache.get_or_load(&candidate) {
1001 if file_contains_definition(&candidate, &content, type_name) {
1002 return Some(candidate);
1003 }
1004 }
1005 }
1006 }
1007 }
1008
1009 let search_dirs = vec![
1011 project_root.to_path_buf(),
1012 project_root.join("src"),
1013 project_root.join("src/models"),
1014 project_root.join("src/types"),
1015 project_root.join("shared"),
1016 project_root.join("shared/types"),
1017 project_root.join("lib"),
1018 project_root.join("domain"),
1019 source_dir.join("models"),
1020 source_dir.join("types"),
1021 ];
1022
1023 for search_dir in search_dirs {
1024 if search_dir.exists() {
1025 for pattern in &patterns {
1026 let candidate = search_dir.join(pattern);
1027 if candidate.exists() {
1028 if let Ok(content) = cache.get_or_load(&candidate) {
1029 if file_contains_definition(&candidate, &content, type_name) {
1030 return Some(candidate);
1031 }
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 None
1039}
1040
1041fn resolve_import_to_path(
1043 module_name: &str,
1044 importing_file: &Path,
1045 project_root: &Path,
1046) -> Option<PathBuf> {
1047 use crate::core::semantic::get_module_resolver_for_file;
1049
1050 let resolver = match get_module_resolver_for_file(importing_file) {
1052 Ok(Some(r)) => r,
1053 _ => {
1054 let source_dir = importing_file.parent()?;
1056
1057 if module_name.starts_with('.') {
1059 return resolve_relative_import(module_name, source_dir, project_root);
1060 }
1061
1062 return match importing_file.extension().and_then(|s| s.to_str()) {
1064 Some("rs") => resolve_rust_import(module_name, source_dir, project_root),
1065 Some("py") => resolve_python_import(module_name, source_dir, project_root),
1066 Some("js") | Some("jsx") => {
1067 resolve_javascript_import(module_name, source_dir, project_root)
1068 }
1069 Some("ts") | Some("tsx") => {
1070 resolve_typescript_import(module_name, source_dir, project_root)
1071 }
1072 Some("go") => resolve_go_import(module_name, source_dir, project_root),
1073 _ => None,
1074 };
1075 }
1076 };
1077
1078 match resolver.resolve_import(module_name, importing_file, project_root) {
1080 Ok(resolved) => {
1081 if resolved.is_external {
1082 None
1084 } else {
1085 Some(resolved.path)
1086 }
1087 }
1088 Err(_) => {
1089 let source_dir = importing_file.parent()?;
1091
1092 if module_name.starts_with('.') {
1094 return resolve_relative_import(module_name, source_dir, project_root);
1095 }
1096
1097 match importing_file.extension().and_then(|s| s.to_str()) {
1099 Some("rs") => resolve_rust_import(module_name, source_dir, project_root),
1100 Some("py") => resolve_python_import(module_name, source_dir, project_root),
1101 Some("js") | Some("jsx") => {
1102 resolve_javascript_import(module_name, source_dir, project_root)
1103 }
1104 Some("ts") | Some("tsx") => {
1105 resolve_typescript_import(module_name, source_dir, project_root)
1106 }
1107 Some("go") => resolve_go_import(module_name, source_dir, project_root),
1108 _ => None,
1109 }
1110 }
1111 }
1112}
1113
1114fn resolve_relative_import(
1116 module_name: &str,
1117 source_dir: &Path,
1118 _project_root: &Path,
1119) -> Option<PathBuf> {
1120 let mut path = source_dir.to_path_buf();
1121
1122 let dots: Vec<&str> = module_name.split('/').collect();
1124 if dots.is_empty() {
1125 return None;
1126 }
1127
1128 for part in &dots {
1130 if *part == ".." {
1131 path = path.parent()?.to_path_buf();
1132 } else if *part == "." {
1133 } else {
1135 path = path.join(part);
1137 break;
1138 }
1139 }
1140
1141 for ext in &["py", "js", "ts", "rs"] {
1143 let file_path = path.with_extension(ext);
1144 if file_path.exists() {
1145 return Some(file_path);
1146 }
1147 }
1148
1149 if path.is_dir() {
1151 for index_file in &["__init__.py", "index.js", "index.ts", "mod.rs"] {
1152 let index_path = path.join(index_file);
1153 if index_path.exists() {
1154 return Some(index_path);
1155 }
1156 }
1157 }
1158
1159 None
1160}
1161
1162fn resolve_rust_import(
1164 module_name: &str,
1165 source_dir: &Path,
1166 project_root: &Path,
1167) -> Option<PathBuf> {
1168 let module_path = if module_name.starts_with("crate::") {
1170 module_name.strip_prefix("crate::").unwrap()
1171 } else {
1172 module_name
1173 };
1174
1175 let parts: Vec<&str> = module_path.split("::").collect();
1177
1178 let mut path = source_dir.to_path_buf();
1180 for part in &parts {
1181 path = path.join(part);
1182 }
1183
1184 let rs_file = path.with_extension("rs");
1186 if rs_file.exists() {
1187 return Some(rs_file);
1188 }
1189
1190 let mod_file = path.join("mod.rs");
1192 if mod_file.exists() {
1193 return Some(mod_file);
1194 }
1195
1196 let src_path = project_root.join("src");
1198 if src_path.exists() {
1199 let mut path = src_path;
1200 for part in &parts {
1201 path = path.join(part);
1202 }
1203
1204 let rs_file = path.with_extension("rs");
1205 if rs_file.exists() {
1206 return Some(rs_file);
1207 }
1208
1209 let mod_file = path.join("mod.rs");
1210 if mod_file.exists() {
1211 return Some(mod_file);
1212 }
1213 }
1214
1215 None
1216}
1217
1218fn resolve_python_import(
1220 module_name: &str,
1221 source_dir: &Path,
1222 project_root: &Path,
1223) -> Option<PathBuf> {
1224 let parts: Vec<&str> = module_name.split('.').collect();
1226
1227 let mut path = source_dir.to_path_buf();
1229 for part in &parts {
1230 path = path.join(part);
1231 }
1232
1233 let py_file = path.with_extension("py");
1235 if py_file.exists() {
1236 return Some(py_file);
1237 }
1238
1239 let init_file = path.join("__init__.py");
1241 if init_file.exists() {
1242 return Some(init_file);
1243 }
1244
1245 let mut path = project_root.to_path_buf();
1247 for part in &parts {
1248 path = path.join(part);
1249 }
1250
1251 let py_file = path.with_extension("py");
1252 if py_file.exists() {
1253 return Some(py_file);
1254 }
1255
1256 let init_file = path.join("__init__.py");
1257 if init_file.exists() {
1258 return Some(init_file);
1259 }
1260
1261 None
1262}
1263
1264fn resolve_javascript_import(
1266 module_name: &str,
1267 source_dir: &Path,
1268 _project_root: &Path,
1269) -> Option<PathBuf> {
1270 if module_name.starts_with("./") || module_name.starts_with("../") {
1272 let path = source_dir.join(module_name);
1273
1274 if path.exists() {
1276 return Some(path);
1277 }
1278
1279 let js_file = path.with_extension("js");
1281 if js_file.exists() {
1282 return Some(js_file);
1283 }
1284
1285 let jsx_file = path.with_extension("jsx");
1287 if jsx_file.exists() {
1288 return Some(jsx_file);
1289 }
1290
1291 let index_file = path.join("index.js");
1293 if index_file.exists() {
1294 return Some(index_file);
1295 }
1296 }
1297
1298 None
1300}
1301
1302fn resolve_typescript_import(
1304 module_name: &str,
1305 source_dir: &Path,
1306 _project_root: &Path,
1307) -> Option<PathBuf> {
1308 if module_name.starts_with("./") || module_name.starts_with("../") {
1310 let path = source_dir.join(module_name);
1311
1312 if path.exists() {
1314 return Some(path);
1315 }
1316
1317 let ts_file = path.with_extension("ts");
1319 if ts_file.exists() {
1320 return Some(ts_file);
1321 }
1322
1323 let tsx_file = path.with_extension("tsx");
1325 if tsx_file.exists() {
1326 return Some(tsx_file);
1327 }
1328
1329 let index_file = path.join("index.ts");
1331 if index_file.exists() {
1332 return Some(index_file);
1333 }
1334 }
1335
1336 None
1338}
1339
1340fn resolve_go_import(
1342 module_name: &str,
1343 _source_dir: &Path,
1344 project_root: &Path,
1345) -> Option<PathBuf> {
1346 if module_name.contains('/') && module_name.split('/').next()?.contains('.') {
1349 return None; }
1351
1352 let parts: Vec<&str> = module_name.split('/').collect();
1354 let mut path = project_root.to_path_buf();
1355
1356 for part in parts {
1357 path = path.join(part);
1358 }
1359
1360 if path.is_dir() {
1362 if let Ok(entries) = std::fs::read_dir(&path) {
1364 for entry in entries.flatten() {
1365 let file_path = entry.path();
1366 if file_path.extension() == Some(std::ffi::OsStr::new("go")) {
1367 let file_name = file_path.file_name()?.to_string_lossy();
1368 if !file_name.ends_with("_test.go") {
1369 return Some(file_path);
1370 }
1371 }
1372 }
1373 }
1374 }
1375
1376 None
1377}
1378
1379fn update_import_relationships(files_map: &mut HashMap<PathBuf, FileInfo>) {
1381 let mut import_map: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
1383
1384 for (path, file_info) in files_map.iter() {
1385 for import_path in &file_info.imports {
1386 import_map
1388 .entry(import_path.clone())
1389 .or_default()
1390 .push(path.clone());
1391 }
1392 }
1393
1394 for (imported_path, importers) in import_map {
1396 if let Some(file_info) = files_map.get_mut(&imported_path) {
1397 file_info.imported_by.extend(importers);
1398 file_info.imported_by.sort();
1399 file_info.imported_by.dedup();
1400 }
1401 }
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use super::*;
1407 use crate::utils::file_ext::FileType;
1408
1409 #[test]
1410 fn test_no_expansion_when_disabled() {
1411 let mut files_map = HashMap::new();
1412 files_map.insert(
1413 PathBuf::from("test.rs"),
1414 FileInfo {
1415 path: PathBuf::from("test.rs"),
1416 relative_path: PathBuf::from("test.rs"),
1417 size: 100,
1418 file_type: FileType::Rust,
1419 priority: 1.0,
1420 imports: Vec::new(),
1421 imported_by: Vec::new(),
1422 function_calls: Vec::new(),
1423 type_references: Vec::new(),
1424 exported_functions: Vec::new(),
1425 },
1426 );
1427
1428 let config = Config {
1429 trace_imports: false,
1430 include_callers: false,
1431 include_types: false,
1432 ..Default::default()
1433 };
1434
1435 let cache = Arc::new(FileCache::new());
1436 let walk_options = crate::core::walker::WalkOptions {
1437 max_file_size: None,
1438 follow_links: false,
1439 include_hidden: false,
1440 parallel: false,
1441 ignore_file: ".context-creator-ignore".to_string(),
1442 ignore_patterns: vec![],
1443 include_patterns: vec![],
1444 custom_priorities: vec![],
1445 filter_binary_files: false,
1446 };
1447 let result = expand_file_list(files_map.clone(), &config, &cache, &walk_options).unwrap();
1448
1449 assert_eq!(result.len(), 1);
1450 }
1451
1452 #[test]
1453 fn test_common_ancestor() {
1454 #[cfg(windows)]
1455 {
1456 let path1 = PathBuf::from("C:\\Users\\user\\project\\src\\main.rs");
1457 let path2 = PathBuf::from("C:\\Users\\user\\project\\lib\\util.rs");
1458 let ancestor = common_ancestor(&path1, &path2);
1459 assert_eq!(ancestor, PathBuf::from("C:\\Users\\user\\project"));
1460
1461 let path3 = PathBuf::from("C:\\Users\\user\\project\\main.rs");
1463 let path4 = PathBuf::from("C:\\Users\\user\\project\\util.rs");
1464 let ancestor2 = common_ancestor(&path3, &path4);
1465 assert_eq!(ancestor2, PathBuf::from("C:\\Users\\user\\project"));
1466
1467 let path5 = PathBuf::from("C:\\Users\\user\\project\\src\\deep\\nested\\file.rs");
1469 let path6 = PathBuf::from("C:\\Users\\user\\project\\src\\main.rs");
1470 let ancestor3 = common_ancestor(&path5, &path6);
1471 assert_eq!(ancestor3, PathBuf::from("C:\\Users\\user\\project\\src"));
1472
1473 let path7 = PathBuf::from("C:\\Program Files\\tool");
1475 let path8 = PathBuf::from("D:\\Users\\user\\file");
1476 let ancestor4 = common_ancestor(&path7, &path8);
1477 assert_eq!(ancestor4, PathBuf::from("C:\\"));
1479 }
1480
1481 #[cfg(not(windows))]
1482 {
1483 let path1 = PathBuf::from("/home/user/project/src/main.rs");
1484 let path2 = PathBuf::from("/home/user/project/lib/util.rs");
1485 let ancestor = common_ancestor(&path1, &path2);
1486 assert_eq!(ancestor, PathBuf::from("/home/user/project"));
1487
1488 let path3 = PathBuf::from("/home/user/project/main.rs");
1490 let path4 = PathBuf::from("/home/user/project/util.rs");
1491 let ancestor2 = common_ancestor(&path3, &path4);
1492 assert_eq!(ancestor2, PathBuf::from("/home/user/project"));
1493
1494 let path5 = PathBuf::from("/home/user/project/src/deep/nested/file.rs");
1496 let path6 = PathBuf::from("/home/user/project/src/main.rs");
1497 let ancestor3 = common_ancestor(&path5, &path6);
1498 assert_eq!(ancestor3, PathBuf::from("/home/user/project/src"));
1499
1500 let path7 = PathBuf::from("/usr/local/bin/tool");
1502 let path8 = PathBuf::from("/home/user/file");
1503 let ancestor4 = common_ancestor(&path7, &path8);
1504 assert_eq!(ancestor4, PathBuf::from("/"));
1505
1506 let path9 = PathBuf::from("/home/user/project");
1508 let path10 = PathBuf::from("/home/user/project/src/main.rs");
1509 let ancestor5 = common_ancestor(&path9, &path10);
1510 assert_eq!(ancestor5, PathBuf::from("/home/user/project"));
1511 }
1512 }
1513
1514 #[test]
1515 fn test_file_contains_definition() {
1516 let rust_content = r#"
1518 pub struct MyStruct {
1519 field1: String,
1520 field2: i32,
1521 }
1522
1523 pub enum MyEnum {
1524 Variant1,
1525 Variant2(String),
1526 }
1527
1528 pub trait MyTrait {
1529 fn method(&self);
1530 }
1531 "#;
1532
1533 let mut parser = tree_sitter::Parser::new();
1534 parser.set_language(tree_sitter_rust::language()).unwrap();
1535 let tree = parser.parse(rust_content, None).unwrap();
1536
1537 let simple_query = r#"
1539 [
1540 (struct_item name: (type_identifier) @name)
1541 (enum_item name: (type_identifier) @name)
1542 (trait_item name: (type_identifier) @name)
1543 ]
1544 "#;
1545
1546 let query = tree_sitter::Query::new(tree_sitter_rust::language(), simple_query).unwrap();
1547 let mut cursor = tree_sitter::QueryCursor::new();
1548 let matches: Vec<_> = cursor
1549 .matches(&query, tree.root_node(), rust_content.as_bytes())
1550 .collect();
1551
1552 assert_eq!(matches.len(), 3, "Should find exactly 3 type definitions");
1554
1555 let rust_path = PathBuf::from("test.rs");
1557 assert!(file_contains_definition(
1558 &rust_path,
1559 rust_content,
1560 "MyStruct"
1561 ));
1562 assert!(file_contains_definition(&rust_path, rust_content, "MyEnum"));
1563 assert!(file_contains_definition(
1564 &rust_path,
1565 rust_content,
1566 "MyTrait"
1567 ));
1568 assert!(!file_contains_definition(
1569 &rust_path,
1570 rust_content,
1571 "NonExistent"
1572 ));
1573
1574 let python_content = r#"
1576 class MyClass:
1577 def __init__(self):
1578 pass
1579
1580 def my_function():
1581 pass
1582 "#;
1583
1584 let python_path = PathBuf::from("test.py");
1585 assert!(file_contains_definition(
1586 &python_path,
1587 python_content,
1588 "MyClass"
1589 ));
1590 assert!(file_contains_definition(
1591 &python_path,
1592 python_content,
1593 "my_function"
1594 ));
1595 assert!(!file_contains_definition(
1596 &python_path,
1597 python_content,
1598 "NonExistent"
1599 ));
1600
1601 let typescript_content = r#"
1603 export interface MyInterface {
1604 prop1: string;
1605 prop2: number;
1606 }
1607
1608 export type MyType = string | number;
1609
1610 export class MyClass {
1611 constructor() {}
1612 }
1613 "#;
1614
1615 let typescript_path = PathBuf::from("test.ts");
1616 assert!(file_contains_definition(
1617 &typescript_path,
1618 typescript_content,
1619 "MyInterface"
1620 ));
1621 assert!(file_contains_definition(
1622 &typescript_path,
1623 typescript_content,
1624 "MyType"
1625 ));
1626 assert!(file_contains_definition(
1627 &typescript_path,
1628 typescript_content,
1629 "MyClass"
1630 ));
1631 assert!(!file_contains_definition(
1632 &typescript_path,
1633 typescript_content,
1634 "NonExistent"
1635 ));
1636 }
1637
1638 #[test]
1639 fn test_query_engine_rust() {
1640 use crate::core::semantic::query_engine::QueryEngine;
1641 use tree_sitter::Parser;
1642
1643 let rust_content = r#"
1644 use model::{Account, DatabaseFactory, Rule, RuleLevel, RuleName};
1645
1646 pub fn create(
1647 database: &mut dyn DatabaseFactory,
1648 account: &Account,
1649 rule_name: &RuleName,
1650 ) -> Result<Rule, Box<dyn std::error::Error>> {
1651 Ok(Rule::new())
1652 }
1653 "#;
1654
1655 let language = tree_sitter_rust::language();
1656 let query_engine = QueryEngine::new(language, "rust").unwrap();
1657
1658 let mut parser = Parser::new();
1659 parser.set_language(tree_sitter_rust::language()).unwrap();
1660
1661 let result = query_engine
1662 .analyze_with_parser(&mut parser, rust_content)
1663 .unwrap();
1664
1665 println!("Imports found: {:?}", result.imports);
1666 println!("Type references found: {:?}", result.type_references);
1667
1668 assert!(!result.imports.is_empty(), "Should find imports");
1670
1671 assert!(
1673 !result.type_references.is_empty(),
1674 "Should find type references"
1675 );
1676
1677 let type_names: Vec<&str> = result
1679 .type_references
1680 .iter()
1681 .map(|t| t.name.as_str())
1682 .collect();
1683 assert!(
1684 type_names.contains(&"DatabaseFactory"),
1685 "Should find DatabaseFactory type"
1686 );
1687 assert!(type_names.contains(&"Account"), "Should find Account type");
1688 assert!(
1689 type_names.contains(&"RuleName"),
1690 "Should find RuleName type"
1691 );
1692 }
1693}