Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions src/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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={})",
Expand Down
128 changes: 128 additions & 0 deletions src/parsers/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<RustCrate>> {
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<String> {
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<String> {
// 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
}