1use anyhow::{Context, Result};
7use ignore::WalkBuilder;
8use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
9use rayon::prelude::*;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14use std::time::Instant;
15
16use crate::cache::CacheManager;
17use crate::content_store::{ContentReader, ContentWriter};
18use crate::dependency::DependencyIndex;
19use crate::models::{Dependency, ImportType, IndexConfig, IndexStats, Language};
20#[cfg(unix)]
21use crate::output;
22use crate::parsers::c::CDependencyExtractor;
23use crate::parsers::cpp::CppDependencyExtractor;
24use crate::parsers::csharp::CSharpDependencyExtractor;
25use crate::parsers::go::GoDependencyExtractor;
26use crate::parsers::java::JavaDependencyExtractor;
27use crate::parsers::kotlin::KotlinDependencyExtractor;
28use crate::parsers::php::PhpDependencyExtractor;
29use crate::parsers::python::PythonDependencyExtractor;
30use crate::parsers::ruby::RubyDependencyExtractor;
31use crate::parsers::rust::RustDependencyExtractor;
32use crate::parsers::svelte::SvelteDependencyExtractor;
33use crate::parsers::typescript::TypeScriptDependencyExtractor;
34use crate::parsers::vue::VueDependencyExtractor;
35use crate::parsers::zig::ZigDependencyExtractor;
36use crate::parsers::{DependencyExtractor, ExportInfo, ImportInfo};
37use crate::trigram::TrigramIndex;
38
39pub type ProgressCallback = Arc<dyn Fn(usize, usize, String) + Send + Sync>;
42
43struct FileProcessingResult {
45 path_str: String,
46 hash: String,
47 content: String,
48 language: Language,
49 line_count: usize,
50 dependencies: Vec<ImportInfo>,
51 exports: Vec<ExportInfo>,
52}
53
54fn find_nearest_tsconfig<'a>(
59 file_path: &str,
60 root: &Path,
61 tsconfigs: &'a HashMap<PathBuf, crate::parsers::tsconfig::PathAliasMap>,
62) -> Option<&'a crate::parsers::tsconfig::PathAliasMap> {
63 let abs_file_path = if Path::new(file_path).is_absolute() {
65 PathBuf::from(file_path)
66 } else {
67 root.join(file_path)
68 };
69
70 let mut current_dir = abs_file_path.parent()?;
72
73 loop {
74 if let Some(alias_map) = tsconfigs.get(current_dir) {
76 return Some(alias_map);
77 }
78
79 current_dir = current_dir.parent()?;
81
82 if current_dir == root || !current_dir.starts_with(root) {
84 break;
85 }
86 }
87
88 None
89}
90
91pub struct Indexer {
93 cache: CacheManager,
94 config: IndexConfig,
95}
96
97impl Indexer {
98 pub fn new(cache: CacheManager, config: IndexConfig) -> Self {
100 Self { cache, config }
101 }
102
103 pub fn index(&self, root: impl AsRef<Path>, show_progress: bool) -> Result<IndexStats> {
105 self.index_with_callback(root, show_progress, None)
106 }
107
108 pub fn index_with_callback(
110 &self,
111 root: impl AsRef<Path>,
112 show_progress: bool,
113 progress_callback: Option<ProgressCallback>,
114 ) -> Result<IndexStats> {
115 let root = root.as_ref();
116 log::info!("Indexing directory: {:?}", root);
117
118 let git_state = crate::git::get_git_state_optional(root)?;
120 let branch = git_state
121 .as_ref()
122 .map(|s| s.branch.clone())
123 .unwrap_or_else(|| "_default".to_string());
124
125 if let Some(ref state) = git_state {
126 log::info!(
127 "Git state: branch='{}', commit='{}', dirty={}",
128 state.branch,
129 state.commit,
130 state.dirty
131 );
132 } else {
133 log::info!("Not a git repository, using default branch");
134 }
135
136 let num_threads = if self.config.parallel_threads == 0 {
139 let available_cores = std::thread::available_parallelism()
140 .map(|n| n.get())
141 .unwrap_or(4);
142 ((available_cores as f64 * 0.8).ceil() as usize).clamp(1, 8)
145 } else {
146 self.config.parallel_threads
147 };
148
149 log::info!(
150 "Using {} threads for parallel indexing (out of {} available)",
151 num_threads,
152 std::thread::available_parallelism()
153 .map(|n| n.get())
154 .unwrap_or(4)
155 );
156
157 self.cache.init()?;
159
160 self.check_disk_space(root)?;
162
163 let existing_hashes = self.cache.load_hashes_for_branch(&branch)?;
165 log::debug!(
166 "Loaded {} existing file hashes for branch '{}'",
167 existing_hashes.len(),
168 branch
169 );
170
171 let (files, skipped_too_large, skipped_bytes_too_large) = self.discover_files(root)?;
173 let total_files = files.len();
174 log::info!(
175 "Discovered {} files to index ({} skipped: too large)",
176 total_files,
177 skipped_too_large
178 );
179
180 let tsconfigs = crate::parsers::tsconfig::parse_all_tsconfigs(root).unwrap_or_else(|e| {
183 log::warn!("Failed to parse tsconfig.json files: {}", e);
184 HashMap::new()
185 });
186 if !tsconfigs.is_empty() {
187 log::info!("Found {} tsconfig.json files", tsconfigs.len());
188 for (config_dir, alias_map) in &tsconfigs {
189 log::debug!(
190 " {} (base_url: {:?}, {} aliases)",
191 config_dir.display(),
192 alias_map.base_url,
193 alias_map.aliases.len()
194 );
195 }
196 }
197
198 if !existing_hashes.is_empty() && total_files == existing_hashes.len() {
201 let mut any_changed = false;
203
204 for file_path in &files {
205 let path_str = file_path.to_string_lossy().to_string();
210 let normalized_path = if let Ok(rel_path) = file_path.strip_prefix(root) {
211 rel_path.to_string_lossy().replace('\\', "/")
213 } else {
214 path_str.trim_start_matches("./").replace('\\', "/")
216 };
217
218 if let Some(existing_hash) = existing_hashes.get(&normalized_path) {
220 match std::fs::read_to_string(file_path) {
222 Ok(content) => {
223 let current_hash = self.hash_content(content.as_bytes());
224 if ¤t_hash != existing_hash {
225 any_changed = true;
226 log::debug!("File changed: {}", path_str);
227 break; }
229 }
230 Err(_) => {
231 any_changed = true;
232 break;
233 }
234 }
235 } else {
236 any_changed = true;
238 break;
239 }
240 }
241
242 if !any_changed {
243 let content_path = self.cache.path().join("content.bin");
244 let trigrams_path = self.cache.path().join("trigrams.bin");
245
246 let schema_ok = self.cache.check_schema_hash().unwrap_or(false);
249
250 if schema_ok && content_path.exists() && trigrams_path.exists() {
251 let trigrams_ok = {
256 use std::io::Read;
257 std::fs::File::open(&trigrams_path)
258 .and_then(|mut f| {
259 let mut h = [0u8; 4];
260 f.read_exact(&mut h).map(|_| h)
261 })
262 .map(|h| &h == b"RFTG")
263 .unwrap_or(false)
264 };
265 if !trigrams_ok {
266 log::warn!(
267 "trigrams.bin corrupted or too small despite hashes matching - forcing rebuild"
268 );
269 } else if let Ok(reader) = ContentReader::open(&content_path) {
270 if reader.file_count() > 0 {
271 log::info!("No files changed - skipping index rebuild");
272 let mut stats = self.cache.stats()?;
273 stats.unchanged_files = total_files;
274 stats.skipped_too_large = skipped_too_large;
275 stats.skipped_bytes_too_large = skipped_bytes_too_large;
276 return Ok(stats);
277 }
278 log::warn!(
279 "content.bin has no files despite hashes matching - forcing rebuild"
280 );
281 } else {
282 log::warn!("content.bin invalid despite hashes matching - forcing rebuild");
283 }
284 } else if !schema_ok {
285 log::info!("Schema hash changed - forcing full rebuild");
286 } else {
287 log::warn!("Binary index files missing - forcing rebuild");
288 }
289 }
290 } else if total_files != existing_hashes.len() {
291 log::info!(
292 "File count changed ({} -> {}) - full reindex required",
293 existing_hashes.len(),
294 total_files
295 );
296 }
297
298 let mut new_hashes = HashMap::new();
300 let mut files_indexed = 0;
301 let mut new_file_count = 0usize;
302 let mut modified_file_count = 0usize;
303 let mut unchanged_file_count = 0usize;
304 let mut file_metadata: Vec<(String, String, String, usize)> = Vec::new(); let mut all_dependencies: Vec<(String, Vec<ImportInfo>)> = Vec::new(); let mut all_exports: Vec<(String, Vec<ExportInfo>)> = Vec::new(); let mut trigram_index = TrigramIndex::new();
310 let mut content_writer = ContentWriter::new();
311
312 if total_files > 10000 {
314 let temp_dir = self.cache.path().join("trigram_temp");
315 trigram_index
316 .enable_batch_flush(temp_dir)
317 .context("Failed to enable batch-flush mode for trigram index")?;
318 log::info!("Enabled batch-flush mode for {} files", total_files);
319 }
320
321 let content_path = self.cache.path().join("content.bin");
323 content_writer
324 .init(content_path.clone())
325 .context("Failed to initialize content writer")?;
326
327 let pb = if show_progress {
329 let pb = ProgressBar::new(total_files as u64);
330 pb.set_draw_target(ProgressDrawTarget::stderr());
331 pb.set_style(
332 ProgressStyle::default_bar()
333 .template("[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} files ({percent}%) {msg}")
334 .unwrap()
335 .progress_chars("=>-")
336 );
337 pb.enable_steady_tick(std::time::Duration::from_millis(100));
339 pb
340 } else {
341 ProgressBar::hidden()
342 };
343
344 let progress_counter = Arc::new(AtomicU64::new(0));
346 let progress_status = Arc::new(Mutex::new("Indexing files...".to_string()));
348
349 let _start_time = Instant::now();
350
351 let counter_for_thread = Arc::clone(&progress_counter);
353 let status_for_thread = Arc::clone(&progress_status);
354 let pb_clone = pb.clone();
355 let callback_for_thread = progress_callback.clone();
356 let total_files_for_thread = total_files;
357 let progress_thread = if show_progress || callback_for_thread.is_some() {
358 Some(std::thread::spawn(move || {
359 loop {
360 let count = counter_for_thread.load(Ordering::Relaxed);
361 pb_clone.set_position(count);
362
363 if let Some(ref callback) = callback_for_thread {
365 let status = status_for_thread.lock().unwrap().clone();
366 callback(count as usize, total_files_for_thread, status);
367 }
368
369 if count >= total_files_for_thread as u64 {
370 break;
371 }
372 std::thread::sleep(std::time::Duration::from_millis(50));
373 }
374 }))
375 } else {
376 None
377 };
378
379 let pool = rayon::ThreadPoolBuilder::new()
381 .num_threads(num_threads)
382 .build()
383 .context("Failed to create thread pool")?;
384
385 const BATCH_SIZE: usize = 5000;
388 let num_batches = total_files.div_ceil(BATCH_SIZE);
389 log::info!(
390 "Processing {} files in {} batches of up to {} files",
391 total_files,
392 num_batches,
393 BATCH_SIZE
394 );
395
396 for (batch_idx, batch_files) in files.chunks(BATCH_SIZE).enumerate() {
397 log::info!(
398 "Processing batch {}/{} ({} files)",
399 batch_idx + 1,
400 num_batches,
401 batch_files.len()
402 );
403
404 let counter_clone = Arc::clone(&progress_counter);
406 let results: Vec<Option<FileProcessingResult>> = pool.install(|| {
407 batch_files
408 .par_iter()
409 .map(|file_path| {
410 let path_str = file_path.to_string_lossy().to_string();
413 let normalized_path = if let Ok(rel_path) = file_path.strip_prefix(root) {
414 rel_path.to_string_lossy().replace('\\', "/")
416 } else {
417 path_str.trim_start_matches("./").replace('\\', "/")
419 };
420
421 let content = match std::fs::read_to_string(file_path) {
423 Ok(c) => c,
424 Err(e) => {
425 log::warn!("Failed to read {}: {}", path_str, e);
426 counter_clone.fetch_add(1, Ordering::Relaxed);
428 return None;
429 }
430 };
431
432 let hash = self.hash_content(content.as_bytes());
434
435 let ext = file_path.extension()
437 .and_then(|e| e.to_str())
438 .unwrap_or("");
439 let language = Language::from_extension(ext);
440
441 let line_count = content.lines().count();
443
444 let dependencies = match language {
446 Language::Rust => {
447 match RustDependencyExtractor::extract_dependencies(&content) {
448 Ok(deps) => deps,
449 Err(e) => {
450 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
451 Vec::new()
452 }
453 }
454 }
455 Language::Python => {
456 match PythonDependencyExtractor::extract_dependencies(&content) {
457 Ok(deps) => deps,
458 Err(e) => {
459 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
460 Vec::new()
461 }
462 }
463 }
464 Language::TypeScript | Language::JavaScript => {
465 let alias_map = find_nearest_tsconfig(&path_str, root, &tsconfigs);
467 match TypeScriptDependencyExtractor::extract_dependencies_with_alias_map(&content, alias_map) {
468 Ok(deps) => deps,
469 Err(e) => {
470 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
471 Vec::new()
472 }
473 }
474 }
475 Language::Go => {
476 match GoDependencyExtractor::extract_dependencies(&content) {
477 Ok(deps) => deps,
478 Err(e) => {
479 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
480 Vec::new()
481 }
482 }
483 }
484 Language::Java => {
485 match JavaDependencyExtractor::extract_dependencies(&content) {
486 Ok(deps) => deps,
487 Err(e) => {
488 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
489 Vec::new()
490 }
491 }
492 }
493 Language::C => {
494 match CDependencyExtractor::extract_dependencies(&content) {
495 Ok(deps) => deps,
496 Err(e) => {
497 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
498 Vec::new()
499 }
500 }
501 }
502 Language::Cpp => {
503 match CppDependencyExtractor::extract_dependencies(&content) {
504 Ok(deps) => deps,
505 Err(e) => {
506 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
507 Vec::new()
508 }
509 }
510 }
511 Language::CSharp => {
512 match CSharpDependencyExtractor::extract_dependencies(&content) {
513 Ok(deps) => deps,
514 Err(e) => {
515 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
516 Vec::new()
517 }
518 }
519 }
520 Language::PHP => {
521 match PhpDependencyExtractor::extract_dependencies(&content) {
522 Ok(deps) => deps,
523 Err(e) => {
524 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
525 Vec::new()
526 }
527 }
528 }
529 Language::Ruby => {
530 match RubyDependencyExtractor::extract_dependencies(&content) {
531 Ok(deps) => deps,
532 Err(e) => {
533 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
534 Vec::new()
535 }
536 }
537 }
538 Language::Kotlin => {
539 match KotlinDependencyExtractor::extract_dependencies(&content) {
540 Ok(deps) => deps,
541 Err(e) => {
542 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
543 Vec::new()
544 }
545 }
546 }
547 Language::Zig => {
548 match ZigDependencyExtractor::extract_dependencies(&content) {
549 Ok(deps) => deps,
550 Err(e) => {
551 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
552 Vec::new()
553 }
554 }
555 }
556 Language::Vue => {
557 let alias_map = find_nearest_tsconfig(&path_str, root, &tsconfigs);
559 match VueDependencyExtractor::extract_dependencies_with_alias_map(&content, alias_map) {
560 Ok(deps) => deps,
561 Err(e) => {
562 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
563 Vec::new()
564 }
565 }
566 }
567 Language::Svelte => {
568 match SvelteDependencyExtractor::extract_dependencies(&content) {
569 Ok(deps) => deps,
570 Err(e) => {
571 log::warn!("Failed to extract dependencies from {}: {}", path_str, e);
572 Vec::new()
573 }
574 }
575 }
576 _ => Vec::new(),
578 };
579
580 let exports = match language {
582 Language::TypeScript | Language::JavaScript => {
583 let alias_map = find_nearest_tsconfig(&path_str, root, &tsconfigs);
585 match TypeScriptDependencyExtractor::extract_export_declarations(&content, alias_map) {
586 Ok(exports) => exports,
587 Err(e) => {
588 log::warn!("Failed to extract exports from {}: {}", path_str, e);
589 Vec::new()
590 }
591 }
592 }
593 Language::Vue => {
594 let alias_map = find_nearest_tsconfig(&path_str, root, &tsconfigs);
596 match VueDependencyExtractor::extract_export_declarations(&content, alias_map) {
597 Ok(exports) => exports,
598 Err(e) => {
599 log::warn!("Failed to extract exports from {}: {}", path_str, e);
600 Vec::new()
601 }
602 }
603 }
604 _ => Vec::new(),
606 };
607
608 counter_clone.fetch_add(1, Ordering::Relaxed);
610
611 Some(FileProcessingResult {
612 path_str: normalized_path.to_string(),
613 hash,
614 content,
615 language,
616 line_count,
617 dependencies,
618 exports,
619 })
620 })
621 .collect()
622 });
623
624 for result in results.into_iter().flatten() {
626 let normalized_pathbuf = PathBuf::from(&result.path_str);
630
631 let file_id = trigram_index.add_file(normalized_pathbuf.clone());
633
634 trigram_index.index_file(file_id, &result.content);
636
637 content_writer.add_file(normalized_pathbuf, &result.content);
639
640 files_indexed += 1;
641
642 match existing_hashes.get(&result.path_str) {
644 None => new_file_count += 1,
645 Some(old_hash) if old_hash != &result.hash => modified_file_count += 1,
646 _ => unchanged_file_count += 1,
647 }
648
649 file_metadata.push((
651 result.path_str.clone(),
652 result.hash.clone(),
653 format!("{:?}", result.language),
654 result.line_count,
655 ));
656
657 if !result.dependencies.is_empty() {
659 all_dependencies.push((result.path_str.clone(), result.dependencies));
660 }
661
662 if !result.exports.is_empty() {
664 all_exports.push((result.path_str.clone(), result.exports));
665 }
666
667 new_hashes.insert(result.path_str, result.hash);
668 }
669
670 if total_files > 10000 {
672 let flush_msg = format!("Flushing batch {}/{}...", batch_idx + 1, num_batches);
673 if show_progress {
674 pb.set_message(flush_msg.clone());
675 }
676 *progress_status.lock().unwrap() = flush_msg;
677 trigram_index
678 .flush_batch()
679 .context("Failed to flush trigram batch")?;
680 }
681 }
682
683 if let Some(thread) = progress_thread {
685 let _ = thread.join();
686 }
687
688 if show_progress {
690 let final_count = progress_counter.load(Ordering::Relaxed);
691 pb.set_position(final_count);
692 }
693
694 *progress_status.lock().unwrap() = "Finalizing trigram index...".to_string();
696 if show_progress {
697 pb.set_message("Finalizing trigram index...".to_string());
698 }
699 trigram_index.finalize();
700
701 *progress_status.lock().unwrap() = "Writing file metadata to database...".to_string();
703 if show_progress {
704 pb.set_message("Writing file metadata to database...".to_string());
705 }
706
707 if !file_metadata.is_empty() {
710 let files_without_hash: Vec<(String, String, usize)> = file_metadata
712 .iter()
713 .map(|(path, _hash, lang, lines)| (path.clone(), lang.clone(), *lines))
714 .collect();
715
716 *progress_status.lock().unwrap() = "Recording branch files...".to_string();
718 if show_progress {
719 pb.set_message("Recording branch files...".to_string());
720 }
721
722 let branch_files: Vec<(String, String)> = file_metadata
724 .iter()
725 .map(|(path, hash, _, _)| (path.clone(), hash.clone()))
726 .collect();
727
728 self.cache
730 .batch_update_files_and_branch(
731 &files_without_hash,
732 &branch_files,
733 &branch,
734 git_state.as_ref().map(|s| s.commit.as_str()),
735 )
736 .context("Failed to batch update files and branch hashes")?;
737
738 log::info!(
739 "Wrote metadata and hashes for {} files to database",
740 file_metadata.len()
741 );
742 }
743
744 self.cache.update_branch_metadata(
746 &branch,
747 git_state.as_ref().map(|s| s.commit.as_str()),
748 file_metadata.len(),
749 git_state.as_ref().map(|s| s.dirty).unwrap_or(false),
750 )?;
751
752 self.cache
755 .checkpoint_wal()
756 .context("Failed to checkpoint WAL")?;
757 log::debug!("WAL checkpoint completed - database is fully synced");
758
759 if !all_dependencies.is_empty() {
761 *progress_status.lock().unwrap() = "Extracting dependencies...".to_string();
762 if show_progress {
763 pb.set_message("Extracting dependencies...".to_string());
764 }
765
766 let go_modules = crate::parsers::go::parse_all_go_modules(root).unwrap_or_else(|e| {
768 log::warn!("Failed to parse go.mod files: {}", e);
769 Vec::new()
770 });
771 if !go_modules.is_empty() {
772 log::info!("Found {} Go modules", go_modules.len());
773 for module in &go_modules {
774 log::debug!(" {} (project: {})", module.name, module.project_root);
775 }
776 }
777
778 let java_projects =
780 crate::parsers::java::parse_all_java_projects(root).unwrap_or_else(|e| {
781 log::warn!("Failed to parse Java project configs: {}", e);
782 Vec::new()
783 });
784 if !java_projects.is_empty() {
785 log::info!("Found {} Java projects", java_projects.len());
786 for project in &java_projects {
787 log::debug!(
788 " {} (project: {})",
789 project.package_name,
790 project.project_root
791 );
792 }
793 }
794
795 let python_packages = crate::parsers::python::parse_all_python_packages(root)
797 .unwrap_or_else(|e| {
798 log::warn!("Failed to parse Python package configs: {}", e);
799 Vec::new()
800 });
801 if !python_packages.is_empty() {
802 log::info!("Found {} Python packages", python_packages.len());
803 for package in &python_packages {
804 log::debug!(" {} (project: {})", package.name, package.project_root);
805 }
806 }
807
808 let ruby_projects =
810 crate::parsers::ruby::parse_all_ruby_projects(root).unwrap_or_else(|e| {
811 log::warn!("Failed to parse Ruby project configs: {}", e);
812 Vec::new()
813 });
814 if !ruby_projects.is_empty() {
815 log::info!("Found {} Ruby projects", ruby_projects.len());
816 for project in &ruby_projects {
817 log::debug!(" {} (project: {})", project.gem_name, project.project_root);
818 }
819 }
820
821 let rust_crates =
823 crate::parsers::rust::parse_all_rust_crates(root).unwrap_or_else(|e| {
824 log::warn!("Failed to parse Cargo.toml files: {}", e);
825 Vec::new()
826 });
827 if !rust_crates.is_empty() {
828 log::info!("Found {} Rust workspace crates", rust_crates.len());
829 for krate in &rust_crates {
830 log::debug!(" {} (root: {})", krate.name, krate.root_path.display());
831 }
832 }
833
834 let php_psr4_mappings = crate::parsers::php::parse_all_composer_psr4(root)
838 .unwrap_or_else(|e| {
839 log::warn!("Failed to parse composer.json files: {}", e);
840 Vec::new()
841 });
842 if !php_psr4_mappings.is_empty() {
843 log::info!(
844 "Found {} PSR-4 mappings from composer.json files",
845 php_psr4_mappings.len()
846 );
847 for mapping in &php_psr4_mappings {
848 log::debug!(
849 " {} => {} (project: {})",
850 mapping.namespace_prefix,
851 mapping.directory,
852 mapping.project_root
853 );
854 }
855 }
856
857 let tsconfigs =
859 crate::parsers::tsconfig::parse_all_tsconfigs(root).unwrap_or_else(|e| {
860 log::warn!("Failed to parse tsconfig.json files: {}", e);
861 HashMap::new()
862 });
863 if !tsconfigs.is_empty() {
864 log::info!("Found {} tsconfig.json files", tsconfigs.len());
865 for (config_dir, alias_map) in &tsconfigs {
866 log::debug!(
867 " {} (base_url: {:?}, {} aliases)",
868 config_dir.display(),
869 alias_map.base_url,
870 alias_map.aliases.len()
871 );
872 }
873 }
874
875 let cache_for_deps = CacheManager::new(root);
877 let dep_index = DependencyIndex::new(cache_for_deps);
878
879 let mut total_deps_inserted = 0;
880
881 for (file_path, import_infos) in all_dependencies {
883 let file_id = match dep_index.get_file_id_by_path(&file_path)? {
885 Some(id) => id,
886 None => {
887 log::warn!(
888 "File not found in database (skipping dependencies): {}",
889 file_path
890 );
891 continue;
892 }
893 };
894
895 let mut resolved_deps = Vec::new();
897
898 for mut import_info in import_infos {
899 if file_path.ends_with(".go") {
901 let mut reclassified = false;
903 for module in &go_modules {
904 import_info.import_type = crate::parsers::go::reclassify_go_import(
905 &import_info.imported_path,
906 Some(&module.name),
907 );
908 if matches!(import_info.import_type, ImportType::Internal) {
910 reclassified = true;
911 break;
912 }
913 }
914 if !reclassified {
916 import_info.import_type = crate::parsers::go::reclassify_go_import(
917 &import_info.imported_path,
918 None,
919 );
920 }
921 }
922
923 if file_path.ends_with(".java") {
925 let mut reclassified = false;
927 for project in &java_projects {
928 import_info.import_type = crate::parsers::java::reclassify_java_import(
929 &import_info.imported_path,
930 Some(&project.package_name),
931 );
932 if matches!(import_info.import_type, ImportType::Internal) {
934 reclassified = true;
935 break;
936 }
937 }
938 if !reclassified {
940 import_info.import_type = crate::parsers::java::reclassify_java_import(
941 &import_info.imported_path,
942 None,
943 );
944 }
945 }
946
947 if file_path.ends_with(".py") {
949 let mut reclassified = false;
951 for package in &python_packages {
952 import_info.import_type =
953 crate::parsers::python::reclassify_python_import(
954 &import_info.imported_path,
955 Some(&package.name),
956 );
957 if matches!(import_info.import_type, ImportType::Internal) {
959 reclassified = true;
960 break;
961 }
962 }
963 if !reclassified {
965 import_info.import_type =
966 crate::parsers::python::reclassify_python_import(
967 &import_info.imported_path,
968 None,
969 );
970 }
971 }
972
973 if file_path.ends_with(".rb")
975 || file_path.ends_with(".rake")
976 || file_path.ends_with(".gemspec")
977 {
978 let mut reclassified = false;
980 for project in &ruby_projects {
981 let gem_names = vec![project.gem_name.clone()];
982 import_info.import_type = crate::parsers::ruby::reclassify_ruby_import(
983 &import_info.imported_path,
984 &gem_names,
985 );
986 if matches!(import_info.import_type, ImportType::Internal) {
988 reclassified = true;
989 break;
990 }
991 }
992 if !reclassified {
994 import_info.import_type = crate::parsers::ruby::reclassify_ruby_import(
995 &import_info.imported_path,
996 &[],
997 );
998 }
999 }
1000
1001 if file_path.ends_with(".kt") || file_path.ends_with(".kts") {
1003 let mut reclassified = false;
1005 for project in &java_projects {
1006 import_info.import_type =
1007 crate::parsers::kotlin::reclassify_kotlin_import(
1008 &import_info.imported_path,
1009 Some(&project.package_name),
1010 );
1011 if matches!(import_info.import_type, ImportType::Internal) {
1013 reclassified = true;
1014 break;
1015 }
1016 }
1017 if !reclassified {
1019 import_info.import_type =
1020 crate::parsers::kotlin::reclassify_kotlin_import(
1021 &import_info.imported_path,
1022 None,
1023 );
1024 }
1025 }
1026
1027 if file_path.ends_with(".rs") && !rust_crates.is_empty() {
1029 let new_type = crate::parsers::rust::reclassify_rust_import(
1030 &import_info.imported_path,
1031 &rust_crates,
1032 );
1033 if matches!(new_type, ImportType::Internal) {
1034 import_info.import_type = new_type;
1035 }
1036 }
1037
1038 if matches!(
1042 import_info.import_type,
1043 ImportType::External | ImportType::Stdlib
1044 ) {
1045 resolved_deps.push(Dependency {
1046 file_id,
1047 imported_path: import_info.imported_path.clone(),
1048 resolved_file_id: None,
1049 import_type: import_info.import_type.clone(),
1050 line_number: import_info.line_number,
1051 imported_symbols: import_info.imported_symbols.clone(),
1052 });
1053 continue;
1054 }
1055
1056 let resolved_file_id = if file_path.ends_with(".php")
1058 && !php_psr4_mappings.is_empty()
1059 {
1060 if let Some(resolved_path) =
1062 crate::parsers::php::resolve_php_namespace_to_path(
1063 &import_info.imported_path,
1064 &php_psr4_mappings,
1065 )
1066 {
1067 match dep_index.get_file_id_by_path(&resolved_path) {
1069 Ok(Some(id)) => {
1070 log::trace!(
1071 "Resolved PHP dependency: {} -> {} (file_id={})",
1072 import_info.imported_path,
1073 resolved_path,
1074 id
1075 );
1076 Some(id)
1077 }
1078 Ok(None) => {
1079 log::trace!(
1080 "PHP dependency resolved to path but file not in index: {} -> {}",
1081 import_info.imported_path,
1082 resolved_path
1083 );
1084 None
1085 }
1086 Err(e) => {
1087 log::debug!(
1088 "Skipping PHP dependency resolution for '{}': {}",
1089 resolved_path,
1090 e
1091 );
1092 None
1093 }
1094 }
1095 } else {
1096 log::trace!(
1097 "Could not resolve PHP namespace using PSR-4: {}",
1098 import_info.imported_path
1099 );
1100 None
1101 }
1102 } else if file_path.ends_with(".py") && !python_packages.is_empty() {
1103 if let Some(resolved_path) =
1105 crate::parsers::python::resolve_python_import_to_path(
1106 &import_info.imported_path,
1107 &python_packages,
1108 Some(&file_path),
1109 )
1110 {
1111 match dep_index.get_file_id_by_path(&resolved_path) {
1113 Ok(Some(id)) => {
1114 log::trace!(
1115 "Resolved Python dependency: {} -> {} (file_id={})",
1116 import_info.imported_path,
1117 resolved_path,
1118 id
1119 );
1120 Some(id)
1121 }
1122 Ok(None) => {
1123 log::trace!(
1124 "Python dependency resolved to path but file not in index: {} -> {}",
1125 import_info.imported_path,
1126 resolved_path
1127 );
1128 None
1129 }
1130 Err(e) => {
1131 log::debug!(
1132 "Skipping Python dependency resolution for '{}': {}",
1133 resolved_path,
1134 e
1135 );
1136 None
1137 }
1138 }
1139 } else {
1140 log::trace!(
1141 "Could not resolve Python import: {}",
1142 import_info.imported_path
1143 );
1144 None
1145 }
1146 } else if file_path.ends_with(".go") && !go_modules.is_empty() {
1147 if let Some(resolved_path) = crate::parsers::go::resolve_go_import_to_path(
1149 &import_info.imported_path,
1150 &go_modules,
1151 Some(&file_path),
1152 ) {
1153 match dep_index.get_file_id_by_path(&resolved_path) {
1155 Ok(Some(id)) => {
1156 log::trace!(
1157 "Resolved Go dependency: {} -> {} (file_id={})",
1158 import_info.imported_path,
1159 resolved_path,
1160 id
1161 );
1162 Some(id)
1163 }
1164 Ok(None) => {
1165 log::trace!(
1166 "Go dependency resolved to path but file not in index: {} -> {}",
1167 import_info.imported_path,
1168 resolved_path
1169 );
1170 None
1171 }
1172 Err(e) => {
1173 log::debug!(
1174 "Skipping Go dependency resolution for '{}': {}",
1175 resolved_path,
1176 e
1177 );
1178 None
1179 }
1180 }
1181 } else {
1182 log::trace!(
1183 "Could not resolve Go import: {}",
1184 import_info.imported_path
1185 );
1186 None
1187 }
1188 } else if file_path.ends_with(".ts")
1189 || file_path.ends_with(".tsx")
1190 || file_path.ends_with(".js")
1191 || file_path.ends_with(".jsx")
1192 || file_path.ends_with(".mts")
1193 || file_path.ends_with(".cts")
1194 || file_path.ends_with(".mjs")
1195 || file_path.ends_with(".cjs")
1196 {
1197 let alias_map = find_nearest_tsconfig(&file_path, root, &tsconfigs);
1199 if let Some(candidates_str) =
1200 crate::parsers::typescript::resolve_ts_import_to_path(
1201 &import_info.imported_path,
1202 Some(&file_path),
1203 alias_map,
1204 )
1205 {
1206 let candidates: Vec<&str> = candidates_str.split('|').collect();
1208
1209 let mut resolved_id = None;
1211 for candidate_path in candidates {
1212 let normalized_candidate = if let Ok(rel_path) =
1215 std::path::Path::new(candidate_path).strip_prefix(root)
1216 {
1217 rel_path.to_string_lossy().replace('\\', "/")
1218 } else {
1219 candidate_path.replace('\\', "/")
1222 };
1223
1224 log::debug!(
1225 "Looking up TS/JS candidate: '{}' (from '{}')",
1226 normalized_candidate,
1227 candidate_path
1228 );
1229 match dep_index.get_file_id_by_path(&normalized_candidate) {
1230 Ok(Some(id)) => {
1231 log::debug!(
1232 "Resolved TS/JS dependency: {} -> {} (file_id={})",
1233 import_info.imported_path,
1234 normalized_candidate,
1235 id
1236 );
1237 resolved_id = Some(id);
1238 break; }
1240 Ok(None) => {
1241 log::trace!(
1242 "TS/JS candidate not in index: {}",
1243 candidate_path
1244 );
1245 }
1246 Err(e) => {
1247 log::debug!(
1248 "Skipping TS/JS dependency resolution for '{}': {}",
1249 normalized_candidate,
1250 e
1251 );
1252 }
1253 }
1254 }
1255
1256 if resolved_id.is_none() {
1257 log::trace!(
1258 "TS/JS dependency: no matching file found in database for any candidate: {}",
1259 candidates_str
1260 );
1261 }
1262
1263 resolved_id
1264 } else {
1265 log::trace!(
1266 "Could not resolve TS/JS import (non-relative or external): {}",
1267 import_info.imported_path
1268 );
1269 None
1270 }
1271 } else if file_path.ends_with(".rs") {
1272 let resolved_path_opt = crate::parsers::rust::resolve_rust_use_to_path(
1275 &import_info.imported_path,
1276 Some(&file_path),
1277 Some(root.to_str().unwrap_or("")),
1278 )
1279 .or_else(|| {
1280 crate::parsers::rust::resolve_rust_workspace_path(
1281 &import_info.imported_path,
1282 &rust_crates,
1283 )
1284 });
1285
1286 if let Some(resolved_path) = resolved_path_opt {
1287 match dep_index.get_file_id_by_path(&resolved_path) {
1289 Ok(Some(id)) => {
1290 log::trace!(
1291 "Resolved Rust dependency: {} -> {} (file_id={})",
1292 import_info.imported_path,
1293 resolved_path,
1294 id
1295 );
1296 Some(id)
1297 }
1298 Ok(None) => {
1299 log::trace!(
1300 "Rust dependency resolved to path but file not in index: {} -> {}",
1301 import_info.imported_path,
1302 resolved_path
1303 );
1304 None
1305 }
1306 Err(e) => {
1307 log::debug!(
1308 "Skipping Rust dependency resolution for '{}': {}",
1309 resolved_path,
1310 e
1311 );
1312 None
1313 }
1314 }
1315 } else {
1316 log::trace!(
1317 "Could not resolve Rust import (external or stdlib): {}",
1318 import_info.imported_path
1319 );
1320 None
1321 }
1322 } else if file_path.ends_with(".java") && !java_projects.is_empty() {
1323 if let Some(resolved_path) =
1325 crate::parsers::java::resolve_java_import_to_path(
1326 &import_info.imported_path,
1327 &java_projects,
1328 Some(&file_path),
1329 )
1330 {
1331 match dep_index.get_file_id_by_path(&resolved_path) {
1333 Ok(Some(id)) => {
1334 log::trace!(
1335 "Resolved Java dependency: {} -> {} (file_id={})",
1336 import_info.imported_path,
1337 resolved_path,
1338 id
1339 );
1340 Some(id)
1341 }
1342 Ok(None) => {
1343 log::trace!(
1344 "Java dependency resolved to path but file not in index: {} -> {}",
1345 import_info.imported_path,
1346 resolved_path
1347 );
1348 None
1349 }
1350 Err(e) => {
1351 log::debug!(
1352 "Skipping Java dependency resolution for '{}': {}",
1353 resolved_path,
1354 e
1355 );
1356 None
1357 }
1358 }
1359 } else {
1360 log::trace!(
1361 "Could not resolve Java import: {}",
1362 import_info.imported_path
1363 );
1364 None
1365 }
1366 } else if (file_path.ends_with(".kt") || file_path.ends_with(".kts"))
1367 && !java_projects.is_empty()
1368 {
1369 if let Some(resolved_path) =
1371 crate::parsers::java::resolve_kotlin_import_to_path(
1372 &import_info.imported_path,
1373 &java_projects,
1374 Some(&file_path),
1375 )
1376 {
1377 match dep_index.get_file_id_by_path(&resolved_path) {
1379 Ok(Some(id)) => {
1380 log::trace!(
1381 "Resolved Kotlin dependency: {} -> {} (file_id={})",
1382 import_info.imported_path,
1383 resolved_path,
1384 id
1385 );
1386 Some(id)
1387 }
1388 Ok(None) => {
1389 log::trace!(
1390 "Kotlin dependency resolved to path but file not in index: {} -> {}",
1391 import_info.imported_path,
1392 resolved_path
1393 );
1394 None
1395 }
1396 Err(e) => {
1397 log::debug!(
1398 "Skipping Kotlin dependency resolution for '{}': {}",
1399 resolved_path,
1400 e
1401 );
1402 None
1403 }
1404 }
1405 } else {
1406 log::trace!(
1407 "Could not resolve Kotlin import: {}",
1408 import_info.imported_path
1409 );
1410 None
1411 }
1412 } else if (file_path.ends_with(".rb")
1413 || file_path.ends_with(".rake")
1414 || file_path.ends_with(".gemspec"))
1415 && !ruby_projects.is_empty()
1416 {
1417 if let Some(resolved_path) =
1419 crate::parsers::ruby::resolve_ruby_require_to_path(
1420 &import_info.imported_path,
1421 &ruby_projects,
1422 Some(&file_path),
1423 )
1424 {
1425 match dep_index.get_file_id_by_path(&resolved_path) {
1427 Ok(Some(id)) => {
1428 log::trace!(
1429 "Resolved Ruby dependency: {} -> {} (file_id={})",
1430 import_info.imported_path,
1431 resolved_path,
1432 id
1433 );
1434 Some(id)
1435 }
1436 Ok(None) => {
1437 log::trace!(
1438 "Ruby dependency resolved to path but file not in index: {} -> {}",
1439 import_info.imported_path,
1440 resolved_path
1441 );
1442 None
1443 }
1444 Err(e) => {
1445 log::debug!(
1446 "Skipping Ruby dependency resolution for '{}': {}",
1447 resolved_path,
1448 e
1449 );
1450 None
1451 }
1452 }
1453 } else {
1454 log::trace!(
1455 "Could not resolve Ruby require: {}",
1456 import_info.imported_path
1457 );
1458 None
1459 }
1460 } else if file_path.ends_with(".c") || file_path.ends_with(".h") {
1461 if let Some(resolved_path) = crate::parsers::c::resolve_c_include_to_path(
1463 &import_info.imported_path,
1464 Some(&file_path),
1465 ) {
1466 match dep_index.get_file_id_by_path(&resolved_path) {
1468 Ok(Some(id)) => {
1469 log::trace!(
1470 "Resolved C dependency: {} -> {} (file_id={})",
1471 import_info.imported_path,
1472 resolved_path,
1473 id
1474 );
1475 Some(id)
1476 }
1477 Ok(None) => {
1478 log::trace!(
1479 "C dependency resolved to path but file not in index: {} -> {}",
1480 import_info.imported_path,
1481 resolved_path
1482 );
1483 None
1484 }
1485 Err(e) => {
1486 log::debug!(
1487 "Skipping C dependency resolution for '{}': {}",
1488 resolved_path,
1489 e
1490 );
1491 None
1492 }
1493 }
1494 } else {
1495 log::trace!(
1496 "Could not resolve C include (system header): {}",
1497 import_info.imported_path
1498 );
1499 None
1500 }
1501 } else if file_path.ends_with(".cpp")
1502 || file_path.ends_with(".cc")
1503 || file_path.ends_with(".cxx")
1504 || file_path.ends_with(".hpp")
1505 || file_path.ends_with(".hxx")
1506 || file_path.ends_with(".h++")
1507 || file_path.ends_with(".C")
1508 || file_path.ends_with(".H")
1509 {
1510 if let Some(resolved_path) =
1512 crate::parsers::cpp::resolve_cpp_include_to_path(
1513 &import_info.imported_path,
1514 Some(&file_path),
1515 )
1516 {
1517 match dep_index.get_file_id_by_path(&resolved_path) {
1519 Ok(Some(id)) => {
1520 log::trace!(
1521 "Resolved C++ dependency: {} -> {} (file_id={})",
1522 import_info.imported_path,
1523 resolved_path,
1524 id
1525 );
1526 Some(id)
1527 }
1528 Ok(None) => {
1529 log::trace!(
1530 "C++ dependency resolved to path but file not in index: {} -> {}",
1531 import_info.imported_path,
1532 resolved_path
1533 );
1534 None
1535 }
1536 Err(e) => {
1537 log::debug!(
1538 "Skipping C++ dependency resolution for '{}': {}",
1539 resolved_path,
1540 e
1541 );
1542 None
1543 }
1544 }
1545 } else {
1546 log::trace!(
1547 "Could not resolve C++ include (system header): {}",
1548 import_info.imported_path
1549 );
1550 None
1551 }
1552 } else if file_path.ends_with(".cs") {
1553 if let Some(resolved_path) =
1555 crate::parsers::csharp::resolve_csharp_using_to_path(
1556 &import_info.imported_path,
1557 Some(&file_path),
1558 )
1559 {
1560 match dep_index.get_file_id_by_path(&resolved_path) {
1562 Ok(Some(id)) => {
1563 log::trace!(
1564 "Resolved C# dependency: {} -> {} (file_id={})",
1565 import_info.imported_path,
1566 resolved_path,
1567 id
1568 );
1569 Some(id)
1570 }
1571 Ok(None) => {
1572 log::trace!(
1573 "C# dependency resolved to path but file not in index: {} -> {}",
1574 import_info.imported_path,
1575 resolved_path
1576 );
1577 None
1578 }
1579 Err(e) => {
1580 log::debug!(
1581 "Skipping C# dependency resolution for '{}': {}",
1582 resolved_path,
1583 e
1584 );
1585 None
1586 }
1587 }
1588 } else {
1589 log::trace!(
1590 "Could not resolve C# using directive: {}",
1591 import_info.imported_path
1592 );
1593 None
1594 }
1595 } else if file_path.ends_with(".zig") {
1596 if let Some(resolved_path) = crate::parsers::zig::resolve_zig_import_to_path(
1598 &import_info.imported_path,
1599 Some(&file_path),
1600 ) {
1601 match dep_index.get_file_id_by_path(&resolved_path) {
1603 Ok(Some(id)) => {
1604 log::trace!(
1605 "Resolved Zig dependency: {} -> {} (file_id={})",
1606 import_info.imported_path,
1607 resolved_path,
1608 id
1609 );
1610 Some(id)
1611 }
1612 Ok(None) => {
1613 log::trace!(
1614 "Zig dependency resolved to path but file not in index: {} -> {}",
1615 import_info.imported_path,
1616 resolved_path
1617 );
1618 None
1619 }
1620 Err(e) => {
1621 log::debug!(
1622 "Skipping Zig dependency resolution for '{}': {}",
1623 resolved_path,
1624 e
1625 );
1626 None
1627 }
1628 }
1629 } else {
1630 log::trace!(
1631 "Could not resolve Zig import (external or stdlib): {}",
1632 import_info.imported_path
1633 );
1634 None
1635 }
1636 } else if file_path.ends_with(".vue") || file_path.ends_with(".svelte") {
1637 let alias_map = find_nearest_tsconfig(&file_path, root, &tsconfigs);
1639 if let Some(candidates_str) =
1640 crate::parsers::typescript::resolve_ts_import_to_path(
1641 &import_info.imported_path,
1642 Some(&file_path),
1643 alias_map,
1644 )
1645 {
1646 let candidates: Vec<&str> = candidates_str.split('|').collect();
1648
1649 let mut resolved_id = None;
1651 for candidate_path in candidates {
1652 let normalized_candidate = if let Ok(rel_path) =
1655 std::path::Path::new(candidate_path).strip_prefix(root)
1656 {
1657 rel_path.to_string_lossy().replace('\\', "/")
1658 } else {
1659 candidate_path.replace('\\', "/")
1662 };
1663
1664 match dep_index.get_file_id_by_path(&normalized_candidate) {
1665 Ok(Some(id)) => {
1666 log::trace!(
1667 "Resolved Vue/Svelte dependency: {} -> {} (file_id={})",
1668 import_info.imported_path,
1669 candidate_path,
1670 id
1671 );
1672 resolved_id = Some(id);
1673 break; }
1675 Ok(None) => {
1676 log::trace!(
1677 "Vue/Svelte candidate not in index: {}",
1678 candidate_path
1679 );
1680 }
1681 Err(e) => {
1682 log::debug!(
1683 "Skipping Vue/Svelte dependency resolution for '{}': {}",
1684 normalized_candidate,
1685 e
1686 );
1687 }
1688 }
1689 }
1690
1691 if resolved_id.is_none() {
1692 log::trace!(
1693 "Vue/Svelte dependency: no matching file found in database for any candidate: {}",
1694 candidates_str
1695 );
1696 }
1697
1698 resolved_id
1699 } else {
1700 log::trace!(
1701 "Could not resolve Vue/Svelte import (non-relative or external): {}",
1702 import_info.imported_path
1703 );
1704 None
1705 }
1706 } else {
1707 None
1708 };
1709
1710 resolved_deps.push(Dependency {
1713 file_id,
1714 imported_path: import_info.imported_path.clone(),
1715 resolved_file_id,
1716 import_type: import_info.import_type,
1717 line_number: import_info.line_number,
1718 imported_symbols: import_info.imported_symbols.clone(),
1719 });
1720 }
1721
1722 dep_index.clear_dependencies(file_id)?;
1724
1725 if !resolved_deps.is_empty() {
1727 dep_index.batch_insert_dependencies(&resolved_deps)?;
1728 total_deps_inserted += resolved_deps.len();
1729 }
1730 }
1731
1732 log::info!("Extracted {} dependencies", total_deps_inserted);
1733 }
1734
1735 if !all_exports.is_empty() {
1737 *progress_status.lock().unwrap() = "Extracting exports...".to_string();
1738 if show_progress {
1739 pb.set_message("Extracting exports...".to_string());
1740 }
1741
1742 let tsconfigs =
1744 crate::parsers::tsconfig::parse_all_tsconfigs(root).unwrap_or_else(|e| {
1745 log::warn!("Failed to parse tsconfig.json files: {}", e);
1746 HashMap::new()
1747 });
1748
1749 let cache_for_exports = CacheManager::new(root);
1751 let dep_index = DependencyIndex::new(cache_for_exports);
1752
1753 let mut total_exports_inserted = 0;
1754
1755 for (file_path, export_infos) in all_exports {
1757 let file_id = match dep_index.get_file_id_by_path(&file_path)? {
1759 Some(id) => id,
1760 None => {
1761 log::warn!(
1762 "File not found in database (skipping exports): {}",
1763 file_path
1764 );
1765 continue;
1766 }
1767 };
1768
1769 for export_info in export_infos {
1771 let resolved_source_id = if file_path.ends_with(".ts")
1773 || file_path.ends_with(".tsx")
1774 || file_path.ends_with(".js")
1775 || file_path.ends_with(".jsx")
1776 || file_path.ends_with(".mts")
1777 || file_path.ends_with(".cts")
1778 || file_path.ends_with(".mjs")
1779 || file_path.ends_with(".cjs")
1780 || file_path.ends_with(".vue")
1781 {
1782 let alias_map = find_nearest_tsconfig(&file_path, root, &tsconfigs);
1784 if let Some(candidates_str) =
1785 crate::parsers::typescript::resolve_ts_import_to_path(
1786 &export_info.source_path,
1787 Some(&file_path),
1788 alias_map,
1789 )
1790 {
1791 let candidates: Vec<&str> = candidates_str.split('|').collect();
1793
1794 let mut resolved_id = None;
1796 for candidate_path in candidates {
1797 let normalized_candidate = if let Ok(rel_path) =
1799 std::path::Path::new(candidate_path).strip_prefix(root)
1800 {
1801 rel_path.to_string_lossy().to_string()
1802 } else {
1803 candidate_path.to_string()
1804 };
1805
1806 match dep_index.get_file_id_by_path(&normalized_candidate) {
1807 Ok(Some(id)) => {
1808 log::trace!(
1809 "Resolved export source: {} -> {} (file_id={})",
1810 export_info.source_path,
1811 normalized_candidate,
1812 id
1813 );
1814 resolved_id = Some(id);
1815 break; }
1817 Ok(None) => {
1818 log::trace!(
1819 "Export source candidate not in index: {}",
1820 candidate_path
1821 );
1822 }
1823 Err(e) => {
1824 log::debug!(
1825 "Skipping export source resolution for '{}': {}",
1826 normalized_candidate,
1827 e
1828 );
1829 }
1830 }
1831 }
1832
1833 if resolved_id.is_none() {
1834 log::trace!(
1835 "Export source: no matching file found in database for any candidate: {}",
1836 candidates_str
1837 );
1838 }
1839
1840 resolved_id
1841 } else {
1842 log::trace!(
1843 "Could not resolve export source (non-relative or external): {}",
1844 export_info.source_path
1845 );
1846 None
1847 }
1848 } else {
1849 None
1850 };
1851
1852 dep_index.insert_export(
1854 file_id,
1855 export_info.exported_symbol,
1856 export_info.source_path,
1857 resolved_source_id,
1858 export_info.line_number,
1859 )?;
1860
1861 total_exports_inserted += 1;
1862 }
1863 }
1864
1865 log::info!("Extracted {} exports", total_exports_inserted);
1866 }
1867
1868 log::info!("Indexed {} files", files_indexed);
1869
1870 *progress_status.lock().unwrap() = "Writing trigram index...".to_string();
1876 if show_progress {
1877 pb.set_message("Writing trigram index...".to_string());
1878 }
1879 let trigrams_path = self.cache.path().join("trigrams.bin");
1880 log::info!(
1881 "Writing trigram index with {} trigrams to trigrams.bin",
1882 trigram_index.trigram_count()
1883 );
1884
1885 trigram_index
1886 .write(&trigrams_path)
1887 .context("Failed to write trigram index")?;
1888 log::info!("Wrote {} files to trigrams.bin", trigram_index.file_count());
1889
1890 *progress_status.lock().unwrap() = "Finalizing content store...".to_string();
1892 if show_progress {
1893 pb.set_message("Finalizing content store...".to_string());
1894 }
1895 content_writer
1896 .finalize_if_needed()
1897 .context("Failed to finalize content store")?;
1898 log::info!(
1899 "Wrote {} files ({} bytes) to content.bin",
1900 content_writer.file_count(),
1901 content_writer.content_size()
1902 );
1903
1904 *progress_status.lock().unwrap() = "Updating statistics...".to_string();
1906 if show_progress {
1907 pb.set_message("Updating statistics...".to_string());
1908 }
1909 self.cache.update_stats(&branch)?;
1911
1912 self.cache.update_schema_hash()?;
1914
1915 pb.finish_with_message("Indexing complete");
1916
1917 let mut stats = self.cache.stats()?;
1919 stats.new_files = new_file_count;
1920 stats.modified_files = modified_file_count;
1921 stats.unchanged_files = unchanged_file_count;
1922 stats.skipped_too_large = skipped_too_large;
1923 stats.skipped_bytes_too_large = skipped_bytes_too_large;
1924 log::info!(
1925 "Indexing complete: {} files (new={}, modified={}, unchanged={})",
1926 stats.total_files,
1927 new_file_count,
1928 modified_file_count,
1929 unchanged_file_count
1930 );
1931
1932 Ok(stats)
1933 }
1934
1935 fn discover_files(&self, root: &Path) -> Result<(Vec<PathBuf>, usize, u64)> {
1939 let mut files = Vec::new();
1940 let mut skipped_count = 0usize;
1941 let mut skipped_bytes = 0u64;
1942
1943 let walker = WalkBuilder::new(root)
1948 .follow_links(self.config.follow_symlinks)
1949 .git_ignore(true) .git_global(false) .git_exclude(false) .build();
1953
1954 for entry in walker {
1955 let entry = entry?;
1956 let path = entry.path();
1957
1958 if !entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
1960 continue;
1961 }
1962
1963 if !self.should_index_lang(path) {
1965 continue;
1966 }
1967
1968 if let Ok(metadata) = std::fs::metadata(path) {
1970 let size = metadata.len();
1971 if size > self.config.max_file_size as u64 {
1972 log::debug!("Skipping {} (too large: {} bytes)", path.display(), size);
1973 skipped_count += 1;
1974 skipped_bytes += size;
1975 continue;
1976 }
1977 }
1978
1979 files.push(path.to_path_buf());
1980 }
1981
1982 Ok((files, skipped_count, skipped_bytes))
1983 }
1984
1985 fn should_index_lang(&self, path: &Path) -> bool {
1987 let ext = match path.extension() {
1988 Some(ext) => ext.to_string_lossy(),
1989 None => return false,
1990 };
1991
1992 let lang = Language::from_extension(&ext);
1993
1994 if !lang.is_supported() {
1995 if !matches!(lang, Language::Unknown) {
1996 log::debug!(
1997 "Skipping {} ({:?} parser not yet implemented)",
1998 path.display(),
1999 lang
2000 );
2001 }
2002 return false;
2003 }
2004
2005 if !self.config.languages.is_empty() && !self.config.languages.contains(&lang) {
2006 log::debug!(
2007 "Skipping {} ({:?} not in configured languages)",
2008 path.display(),
2009 lang
2010 );
2011 return false;
2012 }
2013
2014 true
2015 }
2016
2017 #[allow(dead_code)]
2019 fn should_index(&self, path: &Path) -> bool {
2020 if !self.should_index_lang(path) {
2021 return false;
2022 }
2023
2024 if let Ok(metadata) = std::fs::metadata(path)
2026 && metadata.len() > self.config.max_file_size as u64
2027 {
2028 log::debug!(
2029 "Skipping {} (too large: {} bytes)",
2030 path.display(),
2031 metadata.len()
2032 );
2033 return false;
2034 }
2035
2036 true
2040 }
2041
2042 fn hash_content(&self, content: &[u8]) -> String {
2044 let hash = blake3::hash(content);
2045 hash.to_hex().to_string()
2046 }
2047
2048 #[cfg_attr(not(unix), allow(unused_variables))]
2053 fn check_disk_space(&self, root: &Path) -> Result<()> {
2054 let cache_path = self.cache.path();
2056
2057 #[cfg(unix)]
2059 {
2060 let test_file = cache_path.join(".space_check");
2063 match std::fs::write(&test_file, b"test") {
2064 Ok(_) => {
2065 let _ = std::fs::remove_file(&test_file);
2066
2067 if let Ok(output) = std::process::Command::new("df")
2069 .arg("-k")
2070 .arg(cache_path.parent().unwrap_or(root))
2071 .output()
2072 && let Ok(df_output) = String::from_utf8(output.stdout)
2073 {
2074 if let Some(line) = df_output.lines().nth(1) {
2076 let parts: Vec<&str> = line.split_whitespace().collect();
2077 if parts.len() >= 4
2078 && let Ok(available_kb) = parts[3].parse::<u64>()
2079 {
2080 let available_mb = available_kb / 1024;
2081
2082 if available_mb < 100 {
2084 log::warn!(
2085 "Low disk space: only {}MB available. Indexing may fail.",
2086 available_mb
2087 );
2088 output::warn(&format!(
2089 "Low disk space ({}MB available). Consider freeing up space.",
2090 available_mb
2091 ));
2092 } else {
2093 log::debug!("Available disk space: {}MB", available_mb);
2094 }
2095 }
2096 }
2097 }
2098
2099 Ok(())
2100 }
2101 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
2102 anyhow::bail!(
2103 "Permission denied writing to cache directory: {}. Check file permissions.",
2104 cache_path.display()
2105 )
2106 }
2107 Err(e) => {
2108 log::warn!(
2110 "Failed to write test file (possible disk space issue): {}",
2111 e
2112 );
2113 Err(e).context(
2114 "Failed to verify disk space - indexing may fail due to insufficient space",
2115 )
2116 }
2117 }
2118 }
2119
2120 #[cfg(not(unix))]
2121 {
2122 let test_file = cache_path.join(".space_check");
2124 match std::fs::write(&test_file, b"test") {
2125 Ok(_) => {
2126 let _ = std::fs::remove_file(&test_file);
2127 Ok(())
2128 }
2129 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
2130 anyhow::bail!(
2131 "Permission denied writing to cache directory: {}. Check file permissions.",
2132 cache_path.display()
2133 )
2134 }
2135 Err(e) => {
2136 log::warn!(
2137 "Failed to write test file (possible disk space issue): {}",
2138 e
2139 );
2140 Err(e).context(
2141 "Failed to verify disk space - indexing may fail due to insufficient space",
2142 )
2143 }
2144 }
2145 }
2146 }
2147}
2148
2149#[cfg(test)]
2150mod tests {
2151 use super::*;
2152 use std::fs;
2153 use tempfile::TempDir;
2154
2155 #[test]
2156 fn test_indexer_creation() {
2157 let temp = TempDir::new().unwrap();
2158 let cache = CacheManager::new(temp.path());
2159 let config = IndexConfig::default();
2160 let indexer = Indexer::new(cache, config);
2161
2162 assert!(indexer.cache.path().ends_with(".reflex"));
2163 }
2164
2165 #[test]
2166 fn test_hash_content() {
2167 let temp = TempDir::new().unwrap();
2168 let cache = CacheManager::new(temp.path());
2169 let config = IndexConfig::default();
2170 let indexer = Indexer::new(cache, config);
2171
2172 let content1 = b"hello world";
2173 let content2 = b"hello world";
2174 let content3 = b"different content";
2175
2176 let hash1 = indexer.hash_content(content1);
2177 let hash2 = indexer.hash_content(content2);
2178 let hash3 = indexer.hash_content(content3);
2179
2180 assert_eq!(hash1, hash2);
2182
2183 assert_ne!(hash1, hash3);
2185
2186 assert_eq!(hash1.len(), 64); }
2189
2190 #[test]
2191 fn test_should_index_rust_file() {
2192 let temp = TempDir::new().unwrap();
2193 let cache = CacheManager::new(temp.path());
2194 let config = IndexConfig::default();
2195 let indexer = Indexer::new(cache, config);
2196
2197 let rust_file = temp.path().join("test.rs");
2199 fs::write(&rust_file, "fn main() {}").unwrap();
2200
2201 assert!(indexer.should_index(&rust_file));
2202 }
2203
2204 #[test]
2205 fn test_should_index_unsupported_extension() {
2206 let temp = TempDir::new().unwrap();
2207 let cache = CacheManager::new(temp.path());
2208 let config = IndexConfig::default();
2209 let indexer = Indexer::new(cache, config);
2210
2211 let unsupported_file = temp.path().join("test.txt");
2212 fs::write(&unsupported_file, "plain text").unwrap();
2213
2214 assert!(!indexer.should_index(&unsupported_file));
2215 }
2216
2217 #[test]
2218 fn test_should_index_no_extension() {
2219 let temp = TempDir::new().unwrap();
2220 let cache = CacheManager::new(temp.path());
2221 let config = IndexConfig::default();
2222 let indexer = Indexer::new(cache, config);
2223
2224 let no_ext_file = temp.path().join("Makefile");
2225 fs::write(&no_ext_file, "all:\n\techo hello").unwrap();
2226
2227 assert!(!indexer.should_index(&no_ext_file));
2228 }
2229
2230 #[test]
2231 fn test_should_index_size_limit() {
2232 let temp = TempDir::new().unwrap();
2233 let cache = CacheManager::new(temp.path());
2234
2235 let config = IndexConfig {
2237 max_file_size: 100,
2238 ..Default::default()
2239 };
2240
2241 let indexer = Indexer::new(cache, config);
2242
2243 let small_file = temp.path().join("small.rs");
2245 fs::write(&small_file, "fn main() {}").unwrap();
2246 assert!(indexer.should_index(&small_file));
2247
2248 let large_file = temp.path().join("large.rs");
2250 let large_content = "a".repeat(150);
2251 fs::write(&large_file, large_content).unwrap();
2252 assert!(!indexer.should_index(&large_file));
2253 }
2254
2255 #[test]
2256 fn test_discover_files_empty_dir() {
2257 let temp = TempDir::new().unwrap();
2258 let cache = CacheManager::new(temp.path());
2259 let config = IndexConfig::default();
2260 let indexer = Indexer::new(cache, config);
2261
2262 let (files, _, _) = indexer.discover_files(temp.path()).unwrap();
2263 assert_eq!(files.len(), 0);
2264 }
2265
2266 #[test]
2267 fn test_discover_files_single_file() {
2268 let temp = TempDir::new().unwrap();
2269 let cache = CacheManager::new(temp.path());
2270 let config = IndexConfig::default();
2271 let indexer = Indexer::new(cache, config);
2272
2273 let rust_file = temp.path().join("main.rs");
2275 fs::write(&rust_file, "fn main() {}").unwrap();
2276
2277 let (files, _, _) = indexer.discover_files(temp.path()).unwrap();
2278 assert_eq!(files.len(), 1);
2279 assert!(files[0].ends_with("main.rs"));
2280 }
2281
2282 #[test]
2283 fn test_discover_files_multiple_languages() {
2284 let temp = TempDir::new().unwrap();
2285 let cache = CacheManager::new(temp.path());
2286 let config = IndexConfig::default();
2287 let indexer = Indexer::new(cache, config);
2288
2289 fs::write(temp.path().join("main.rs"), "fn main() {}").unwrap();
2291 fs::write(temp.path().join("script.py"), "print('hello')").unwrap();
2292 fs::write(temp.path().join("app.js"), "console.log('hi')").unwrap();
2293 fs::write(temp.path().join("README.md"), "# Project").unwrap(); let (files, _, _) = indexer.discover_files(temp.path()).unwrap();
2296 assert_eq!(files.len(), 3); }
2298
2299 #[test]
2300 fn test_discover_files_subdirectories() {
2301 let temp = TempDir::new().unwrap();
2302 let cache = CacheManager::new(temp.path());
2303 let config = IndexConfig::default();
2304 let indexer = Indexer::new(cache, config);
2305
2306 let src_dir = temp.path().join("src");
2308 fs::create_dir(&src_dir).unwrap();
2309 fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
2310 fs::write(src_dir.join("lib.rs"), "pub mod test {}").unwrap();
2311
2312 let tests_dir = temp.path().join("tests");
2313 fs::create_dir(&tests_dir).unwrap();
2314 fs::write(tests_dir.join("test.rs"), "#[test] fn test() {}").unwrap();
2315
2316 let (files, _, _) = indexer.discover_files(temp.path()).unwrap();
2317 assert_eq!(files.len(), 3);
2318 }
2319
2320 #[test]
2321 fn test_discover_files_respects_gitignore() {
2322 let temp = TempDir::new().unwrap();
2323
2324 std::process::Command::new("git")
2326 .arg("init")
2327 .current_dir(temp.path())
2328 .output()
2329 .expect("Failed to initialize git repo");
2330
2331 let cache = CacheManager::new(temp.path());
2332 let config = IndexConfig::default();
2333 let indexer = Indexer::new(cache, config);
2334
2335 fs::write(temp.path().join(".gitignore"), "ignored/\n").unwrap();
2338
2339 fs::write(temp.path().join("included.rs"), "fn main() {}").unwrap();
2341 fs::write(temp.path().join("also_included.py"), "print('hi')").unwrap();
2342
2343 let ignored_dir = temp.path().join("ignored");
2344 fs::create_dir(&ignored_dir).unwrap();
2345 fs::write(ignored_dir.join("excluded.rs"), "fn test() {}").unwrap();
2346
2347 let (files, _, _) = indexer.discover_files(temp.path()).unwrap();
2348
2349 assert!(
2351 files.iter().any(|f| f.ends_with("included.rs")),
2352 "Should find included.rs"
2353 );
2354 assert!(
2355 files.iter().any(|f| f.ends_with("also_included.py")),
2356 "Should find also_included.py"
2357 );
2358
2359 assert!(
2362 !files.iter().any(|f| {
2363 let path_str = f.to_string_lossy();
2364 path_str.contains("ignored") && f.ends_with("excluded.rs")
2365 }),
2366 "Should NOT find excluded.rs in ignored/ directory (gitignore pattern)"
2367 );
2368
2369 assert_eq!(
2372 files.len(),
2373 2,
2374 "Should find exactly 2 files (not including .gitignore or ignored/excluded.rs)"
2375 );
2376 }
2377
2378 #[test]
2379 fn test_index_empty_directory() {
2380 let temp = TempDir::new().unwrap();
2381 let cache = CacheManager::new(temp.path());
2382 let config = IndexConfig::default();
2383 let indexer = Indexer::new(cache, config);
2384
2385 let stats = indexer.index(temp.path(), false).unwrap();
2386
2387 assert_eq!(stats.total_files, 0);
2388 }
2389
2390 #[test]
2391 fn test_index_single_rust_file() {
2392 let temp = TempDir::new().unwrap();
2393 let project_root = temp.path().join("project");
2394 fs::create_dir(&project_root).unwrap();
2395
2396 let cache = CacheManager::new(&project_root);
2397 let config = IndexConfig::default();
2398 let indexer = Indexer::new(cache, config);
2399
2400 fs::write(
2402 project_root.join("main.rs"),
2403 "fn main() { println!(\"Hello\"); }",
2404 )
2405 .unwrap();
2406
2407 let stats = indexer.index(&project_root, false).unwrap();
2408
2409 assert_eq!(stats.total_files, 1);
2410 assert!(stats.files_by_language.contains_key("Rust"));
2411 }
2412
2413 #[test]
2414 fn test_index_multiple_files() {
2415 let temp = TempDir::new().unwrap();
2416 let project_root = temp.path().join("project");
2417 fs::create_dir(&project_root).unwrap();
2418
2419 let cache = CacheManager::new(&project_root);
2420 let config = IndexConfig::default();
2421 let indexer = Indexer::new(cache, config);
2422
2423 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2425 fs::write(project_root.join("lib.rs"), "pub fn test() {}").unwrap();
2426 fs::write(project_root.join("script.py"), "def main(): pass").unwrap();
2427
2428 let stats = indexer.index(&project_root, false).unwrap();
2429
2430 assert_eq!(stats.total_files, 3);
2431 assert_eq!(stats.files_by_language.get("Rust"), Some(&2));
2432 assert_eq!(stats.files_by_language.get("Python"), Some(&1));
2433 }
2434
2435 #[test]
2436 fn test_index_creates_trigram_index() {
2437 let temp = TempDir::new().unwrap();
2438 let project_root = temp.path().join("project");
2439 fs::create_dir(&project_root).unwrap();
2440
2441 let cache = CacheManager::new(&project_root);
2442 let config = IndexConfig::default();
2443 let indexer = Indexer::new(cache, config);
2444
2445 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2446
2447 indexer.index(&project_root, false).unwrap();
2448
2449 let trigrams_path = project_root.join(".reflex/trigrams.bin");
2451 assert!(trigrams_path.exists());
2452 }
2453
2454 #[test]
2455 fn test_index_creates_content_store() {
2456 let temp = TempDir::new().unwrap();
2457 let project_root = temp.path().join("project");
2458 fs::create_dir(&project_root).unwrap();
2459
2460 let cache = CacheManager::new(&project_root);
2461 let config = IndexConfig::default();
2462 let indexer = Indexer::new(cache, config);
2463
2464 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2465
2466 indexer.index(&project_root, false).unwrap();
2467
2468 let content_path = project_root.join(".reflex/content.bin");
2470 assert!(content_path.exists());
2471 }
2472
2473 #[test]
2474 fn test_index_incremental_no_changes() {
2475 let temp = TempDir::new().unwrap();
2476 let project_root = temp.path().join("project");
2477 fs::create_dir(&project_root).unwrap();
2478
2479 let cache = CacheManager::new(&project_root);
2480 let config = IndexConfig::default();
2481 let indexer = Indexer::new(cache, config);
2482
2483 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2484
2485 let stats1 = indexer.index(&project_root, false).unwrap();
2487 assert_eq!(stats1.total_files, 1);
2488
2489 let stats2 = indexer.index(&project_root, false).unwrap();
2491 assert_eq!(stats2.total_files, 1);
2492 }
2493
2494 #[test]
2495 fn test_index_incremental_with_changes() {
2496 let temp = TempDir::new().unwrap();
2497 let project_root = temp.path().join("project");
2498 fs::create_dir(&project_root).unwrap();
2499
2500 let cache = CacheManager::new(&project_root);
2501 let config = IndexConfig::default();
2502 let indexer = Indexer::new(cache, config);
2503
2504 let main_path = project_root.join("main.rs");
2505 fs::write(&main_path, "fn main() {}").unwrap();
2506
2507 indexer.index(&project_root, false).unwrap();
2509
2510 fs::write(&main_path, "fn main() { println!(\"changed\"); }").unwrap();
2512
2513 let stats = indexer.index(&project_root, false).unwrap();
2515 assert_eq!(stats.total_files, 1);
2516 }
2517
2518 #[test]
2519 fn test_index_incremental_new_file() {
2520 let temp = TempDir::new().unwrap();
2521 let project_root = temp.path().join("project");
2522 fs::create_dir(&project_root).unwrap();
2523
2524 let cache = CacheManager::new(&project_root);
2525 let config = IndexConfig::default();
2526 let indexer = Indexer::new(cache, config);
2527
2528 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2529
2530 let stats1 = indexer.index(&project_root, false).unwrap();
2532 assert_eq!(stats1.total_files, 1);
2533
2534 fs::write(project_root.join("lib.rs"), "pub fn test() {}").unwrap();
2536
2537 let stats2 = indexer.index(&project_root, false).unwrap();
2539 assert_eq!(stats2.total_files, 2);
2540 }
2541
2542 #[test]
2543 fn test_index_parallel_threads_config() {
2544 let temp = TempDir::new().unwrap();
2545 let project_root = temp.path().join("project");
2546 fs::create_dir(&project_root).unwrap();
2547
2548 let cache = CacheManager::new(&project_root);
2549
2550 let config = IndexConfig {
2552 parallel_threads: 2,
2553 ..Default::default()
2554 };
2555
2556 let indexer = Indexer::new(cache, config);
2557
2558 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2559
2560 let stats = indexer.index(&project_root, false).unwrap();
2561 assert_eq!(stats.total_files, 1);
2562 }
2563
2564 #[test]
2565 fn test_index_parallel_threads_auto() {
2566 let temp = TempDir::new().unwrap();
2567 let project_root = temp.path().join("project");
2568 fs::create_dir(&project_root).unwrap();
2569
2570 let cache = CacheManager::new(&project_root);
2571
2572 let config = IndexConfig {
2574 parallel_threads: 0,
2575 ..Default::default()
2576 };
2577
2578 let indexer = Indexer::new(cache, config);
2579
2580 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2581
2582 let stats = indexer.index(&project_root, false).unwrap();
2583 assert_eq!(stats.total_files, 1);
2584 }
2585
2586 #[test]
2587 fn test_index_respects_size_limit() {
2588 let temp = TempDir::new().unwrap();
2589 let project_root = temp.path().join("project");
2590 fs::create_dir(&project_root).unwrap();
2591
2592 let cache = CacheManager::new(&project_root);
2593
2594 let config = IndexConfig {
2596 max_file_size: 50,
2597 ..Default::default()
2598 };
2599
2600 let indexer = Indexer::new(cache, config);
2601
2602 fs::write(project_root.join("small.rs"), "fn a() {}").unwrap();
2604
2605 let large_content = "fn main() {}\n".repeat(10);
2607 fs::write(project_root.join("large.rs"), large_content).unwrap();
2608
2609 let stats = indexer.index(&project_root, false).unwrap();
2610
2611 assert_eq!(stats.total_files, 1);
2613 }
2614
2615 #[test]
2616 fn test_index_mixed_languages() {
2617 let temp = TempDir::new().unwrap();
2618 let project_root = temp.path().join("project");
2619 fs::create_dir(&project_root).unwrap();
2620
2621 let cache = CacheManager::new(&project_root);
2622 let config = IndexConfig::default();
2623 let indexer = Indexer::new(cache, config);
2624
2625 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2627 fs::write(project_root.join("test.py"), "def test(): pass").unwrap();
2628 fs::write(project_root.join("app.js"), "function main() {}").unwrap();
2629 fs::write(project_root.join("lib.go"), "func main() {}").unwrap();
2630
2631 let stats = indexer.index(&project_root, false).unwrap();
2632
2633 assert_eq!(stats.total_files, 4);
2634 assert!(stats.files_by_language.contains_key("Rust"));
2635 assert!(stats.files_by_language.contains_key("Python"));
2636 assert!(stats.files_by_language.contains_key("JavaScript"));
2637 assert!(stats.files_by_language.contains_key("Go"));
2638 }
2639
2640 #[test]
2641 fn test_index_updates_cache_stats() {
2642 let temp = TempDir::new().unwrap();
2643 let project_root = temp.path().join("project");
2644 fs::create_dir(&project_root).unwrap();
2645
2646 let cache = CacheManager::new(&project_root);
2647 let config = IndexConfig::default();
2648 let indexer = Indexer::new(cache, config);
2649
2650 fs::write(project_root.join("main.rs"), "fn main() {}").unwrap();
2651
2652 indexer.index(&project_root, false).unwrap();
2653
2654 let cache = CacheManager::new(&project_root);
2656 let stats = cache.stats().unwrap();
2657
2658 assert_eq!(stats.total_files, 1);
2659 assert!(stats.index_size_bytes > 0);
2660 }
2661}