diff --git a/src/indexer.rs b/src/indexer.rs index 30c5d0b..708cd67 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -182,6 +182,24 @@ impl Indexer { } } + // ============================================================================ + // WORKSPACE SUPPORT - 20260112 + // ============================================================================ + // NEW: Find and parse all Cargo.toml files for Rust workspaces + let rust_crates = crate::parsers::rust::parse_all_rust_crates(root) + .unwrap_or_else(|e| { + log::warn!("Failed to parse Cargo.toml files: {}", e); + Vec::new() + }); + if !rust_crates.is_empty() { + log::info!("Found {} Rust workspace crates", rust_crates.len()); + } + + // ============================================================================ + // WORKSPACE SUPPORT - 20260112 + // ============================================================================ + + // Step 1.5: Quick incremental check - are all files unchanged? // If yes, skip expensive rebuild entirely and return cached stats if !existing_hashes.is_empty() && total_files == existing_hashes.len() { @@ -894,6 +912,25 @@ impl Indexer { } } + // ============================================================================ + // WORKSPACE SUPPORT - 20260112 + // ============================================================================ + // NEW: Reclassify Rust imports using workspace crates + if file_path.ends_with(".rs") && !rust_crates.is_empty() { + let new_type = crate::parsers::rust::reclassify_rust_import( + &import_info.imported_path, + &rust_crates + ); + + // ============================================================================ + // WORKSPACE SUPPORT - 20260112 + // ============================================================================ + + + if matches!(new_type, ImportType::Internal) { + import_info.import_type = new_type; + } + } // ONLY insert Internal dependencies - skip External and Stdlib if !matches!(import_info.import_type, ImportType::Internal) { continue; @@ -1022,14 +1059,42 @@ impl Indexer { log::trace!("Could not resolve TS/JS import (non-relative or external): {}", import_info.imported_path); None } + // } else if file_path.ends_with(".rs") { + // // Resolve Rust dependencies (crate::, super::, self::, mod declarations) + // if let Some(resolved_path) = crate::parsers::rust::resolve_rust_use_to_path( + // &import_info.imported_path, + // Some(&file_path), + // Some(root.to_str().unwrap_or("")), + // ) { + // // Look up file ID in database using exact match + // match dep_index.get_file_id_by_path(&resolved_path)? { + // Some(id) => { + + + // ============================================================================ + // WORKSPACE SUPPORT (END) - 20260112 + // ============================================================================ } else if file_path.ends_with(".rs") { // Resolve Rust dependencies (crate::, super::, self::, mod declarations) - if let Some(resolved_path) = crate::parsers::rust::resolve_rust_use_to_path( + let resolved = crate::parsers::rust::resolve_rust_use_to_path( &import_info.imported_path, - Some(&file_path), + Some(&file_path), Some(root.to_str().unwrap_or("")), - ) { - // Look up file ID in database using exact match + ); + + // NEW: If standard resolution failed, try Workspace Resolution (ts_shared::...) + let resolved_path_opt = resolved.or_else(|| { + crate::parsers::rust::resolve_rust_workspace_path( + &import_info.imported_path, + &rust_crates + ) + }); + + if let Some(resolved_path) = resolved_path_opt { + // Look up file ID in database using exact match + // ============================================================================ + // WORKSPACE SUPPORT - 20260112 + // ============================================================================ match dep_index.get_file_id_by_path(&resolved_path)? { Some(id) => { log::trace!("Resolved Rust dependency: {} -> {} (file_id={})", diff --git a/src/parsers/rust.rs b/src/parsers/rust.rs index 8e6b5b9..1e950fa 100644 --- a/src/parsers/rust.rs +++ b/src/parsers/rust.rs @@ -1422,3 +1422,131 @@ mod path_resolution_tests { assert!(result.is_none()); } } + +// ============================================================================ +// WORKSPACE SUPPORT (NEW) - 20260112 +// ============================================================================ + +#[derive(Debug, Clone)] +pub struct RustCrate { + pub name: String, + pub root_path: std::path::PathBuf, +} + +/// Find all Rust crates in the workspace by looking for Cargo.toml files +pub fn parse_all_rust_crates(root: &std::path::Path) -> anyhow::Result> { + let mut crates = Vec::new(); + // Scan the entire directory tree respecting gitignore + let walker = ignore::WalkBuilder::new(root) + .git_ignore(true) + .build(); + + for entry in walker { + let entry = entry?; + // Found a Cargo.toml + if entry.file_name() == "Cargo.toml" { + // Read it to extract the package name + // Simple string parsing to avoid adding new dependencies like 'toml' + let content = std::fs::read_to_string(entry.path())?; + if let Some(name) = extract_crate_name(&content) { + // The directory containing Cargo.toml is the crate root + if let Some(crate_root) = entry.path().parent() { + crates.push(RustCrate { + name, + root_path: crate_root.to_path_buf(), + }); + } + } + } + } + Ok(crates) +} + +/// Robust extraction of name="..." from [package] section +fn extract_crate_name(content: &str) -> Option { + let mut in_package_section = false; + for line in content.lines() { + let line = line.trim(); + if line == "[package]" { + in_package_section = true; + continue; + } + if in_package_section { + if line.starts_with('[') { + break; // End of package section + } + if line.starts_with("name") { + // Parse: name = "ts_shared" + if let Some(val) = line.split('=').nth(1) { + return Some(val.trim().trim_matches('"').trim_matches('\'').to_string()); + } + } + } + } + None +} + +/// Reclassify an import based on known workspace crates +pub fn reclassify_rust_import( + path: &str, + crates: &[RustCrate], +) -> crate::models::ImportType { + // If the import matches a known crate name, it is Internal + for krate in crates { + // Match "ts_shared" or "ts_shared::submodule" + if path == krate.name || path.starts_with(&format!("{}::", krate.name)) { + return crate::models::ImportType::Internal; + } + } + // Fallback to default classification (std, external, etc.) + classify_rust_import(path) +} + +/// Resolve a workspace import to a file path +/// e.g., "ts_shared::config" -> "/path/to/ts_shared/src/config.rs" +pub fn resolve_rust_workspace_path( + import_path: &str, + crates: &[RustCrate], +) -> Option { + // 1. Find which crate matches the start of the import + for krate in crates { + if import_path == krate.name || import_path.starts_with(&format!("{}::", krate.name)) { + // 2. Strip crate name to get relative module path + // "ts_shared::config" -> "config" + let relative_module = if import_path == krate.name { + "" + } else { + import_path.strip_prefix(&format!("{}::", krate.name)).unwrap_or("") + }; + + // 3. Assume standard "src/" layout + let src_root = krate.root_path.join("src"); + + // 4. Try to resolve the path + if relative_module.is_empty() { + // "use ts_shared;" -> likely lib.rs + let lib = src_root.join("lib.rs"); + if lib.exists() { return Some(lib.to_string_lossy().to_string()); } + let main = src_root.join("main.rs"); + if main.exists() { return Some(main.to_string_lossy().to_string()); } + } else { + // "use ts_shared::config;" + let parts: Vec<&str> = relative_module.split("::").collect(); + + // Attempt 1: The path points directly to a module/file + if let Some(path) = resolve_rust_module_path(&src_root, &parts) { + return Some(path); + } + + // Attempt 2: The path points to an ITEM (struct/func) inside a file + // Try popping the last component ("config::Struct") -> resolve "config" + if parts.len() > 1 { + if let Some(path) = resolve_rust_module_path(&src_root, &parts[..parts.len()-1]) { + return Some(path); + } + } + } + } + } + None +} \ No newline at end of file