pub mod barrel;
pub mod cargo_workspace;
pub mod file_resolver;
pub mod go_resolver;
pub mod python_resolver;
pub mod rust_mod_tree;
pub mod rust_resolver;
pub mod workspace;
pub use file_resolver::{
ResolutionOutcome, build_resolver, resolve_import, workspace_map_to_aliases,
};
pub use workspace::discover_workspace_packages;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use petgraph::visit::EdgeRef;
use crate::graph::CodeGraph;
use crate::parser::ParseResult;
use crate::parser::relationships::RelationshipKind;
#[derive(Debug, Default)]
pub struct ResolveStats {
pub resolved: usize,
pub unresolved: usize,
pub external: usize,
pub builtin: usize,
pub relationships_added: usize,
pub named_reexport_edges: usize,
pub rust_resolved: usize,
pub rust_external: usize,
pub rust_builtin: usize,
pub rust_unresolved: usize,
pub go_resolved: usize,
pub go_stdlib: usize,
pub go_external: usize,
pub go_unresolved: usize,
}
pub fn resolve_all(
graph: &mut CodeGraph,
project_root: &Path,
parse_results: &HashMap<PathBuf, ParseResult>,
verbose: bool,
) -> ResolveStats {
let mut stats = ResolveStats::default();
let workspace_map = discover_workspace_packages(project_root);
if verbose && !workspace_map.is_empty() {
eprintln!(" Workspace packages found: {}", workspace_map.len());
for (name, path) in &workspace_map {
eprintln!(" {} -> {}", name, path.display());
}
}
let aliases = workspace_map_to_aliases(&workspace_map);
let resolver = build_resolver(project_root, aliases);
let file_imports: Vec<(PathBuf, Vec<crate::parser::imports::ImportInfo>)> = parse_results
.iter()
.map(|(path, result)| (path.clone(), result.imports.clone()))
.collect();
for (file_path, imports) in &file_imports {
let from_idx = match graph.file_index.get(file_path).copied() {
Some(idx) => idx,
None => {
continue;
}
};
for import in imports {
let specifier = &import.module_path;
let outcome = resolve_import(&resolver, file_path, specifier);
match outcome {
ResolutionOutcome::Resolved(target_path) => {
if let Some(&target_idx) = graph.file_index.get(&target_path) {
graph.add_resolved_import(from_idx, target_idx, specifier);
stats.resolved += 1;
} else {
if verbose {
eprintln!(
" resolve: {} imports '{}' -> {} (not indexed, skipping edge)",
file_path.display(),
specifier,
target_path.display()
);
}
stats.resolved += 1; }
}
ResolutionOutcome::BuiltinModule(name) => {
graph.add_unresolved_import(from_idx, specifier, "builtin");
stats.builtin += 1;
if verbose {
eprintln!(
" resolve: {} imports '{}' -> builtin:{}",
file_path.display(),
specifier,
name
);
}
}
ResolutionOutcome::Unresolved(_reason) => {
if is_external_package(specifier) {
let pkg_name = extract_package_name(specifier);
graph.add_external_package(from_idx, pkg_name, specifier);
stats.external += 1;
if verbose {
eprintln!(
" resolve: {} imports '{}' -> external:{}",
file_path.display(),
specifier,
pkg_name
);
}
} else {
graph.add_unresolved_import(from_idx, specifier, &_reason);
stats.unresolved += 1;
if verbose {
eprintln!(
" resolve: {} imports '{}' -> unresolved: {}",
file_path.display(),
specifier,
_reason
);
}
}
}
}
}
}
barrel::resolve_barrel_chains(graph, parse_results, verbose);
let named_reexport_edges = barrel::resolve_named_reexport_chains(graph, parse_results, verbose);
stats.named_reexport_edges = named_reexport_edges;
if verbose {
eprintln!(" Named re-export edges added: {}", named_reexport_edges);
}
let file_relationships: Vec<(PathBuf, Vec<crate::parser::relationships::RelationshipInfo>)> =
parse_results
.iter()
.map(|(path, result)| (path.clone(), result.relationships.clone()))
.collect();
for (_file_path, relationships) in &file_relationships {
let from_file_idx = match graph.file_index.get(_file_path).copied() {
Some(idx) => idx,
None => continue,
};
for rel in relationships {
match rel.kind {
RelationshipKind::Extends
| RelationshipKind::Implements
| RelationshipKind::InterfaceExtends => {
let from_name = match &rel.from_name {
Some(n) => n,
None => continue,
};
let from_candidates = graph
.symbol_index
.get(from_name)
.cloned()
.unwrap_or_default();
let to_candidates = graph
.symbol_index
.get(&rel.to_name)
.cloned()
.unwrap_or_default();
if from_candidates.is_empty() || to_candidates.is_empty() {
continue;
}
let from_sym_idx = from_candidates
.iter()
.copied()
.find(|&idx| {
graph.graph.edges(from_file_idx).any(|e| e.target() == idx)
})
.unwrap_or(from_candidates[0]);
let same_file_to: Vec<_> = to_candidates
.iter()
.copied()
.filter(|&idx| graph.graph.edges(from_file_idx).any(|e| e.target() == idx))
.collect();
let to_indices = if same_file_to.is_empty() {
to_candidates.clone()
} else {
same_file_to
};
for to_sym_idx in to_indices {
match rel.kind {
RelationshipKind::Extends => {
graph.add_extends_edge(from_sym_idx, to_sym_idx);
stats.relationships_added += 1;
}
RelationshipKind::Implements => {
graph.add_implements_edge(from_sym_idx, to_sym_idx);
stats.relationships_added += 1;
}
RelationshipKind::InterfaceExtends => {
graph.add_extends_edge(from_sym_idx, to_sym_idx);
stats.relationships_added += 1;
}
_ => unreachable!(),
}
}
}
RelationshipKind::Calls
| RelationshipKind::MethodCall
| RelationshipKind::TypeReference => {
let to_candidates = match graph.symbol_index.get(&rel.to_name) {
Some(c) if !c.is_empty() => c.clone(),
_ => continue,
};
if to_candidates.len() == 1 {
let callee_idx = to_candidates[0];
graph.add_calls_edge(from_file_idx, callee_idx);
stats.relationships_added += 1;
}
}
}
}
}
let has_rust_files = graph.graph.node_indices().any(|idx| {
if let crate::graph::node::GraphNode::File(ref f) = graph.graph[idx] {
f.language == "rust"
} else {
false
}
});
if has_rust_files {
let rust_stats =
rust_resolver::resolve_rust_uses(graph, project_root, parse_results, verbose);
stats.rust_resolved = rust_stats.resolved;
stats.rust_external = rust_stats.external;
stats.rust_builtin = rust_stats.builtin;
stats.rust_unresolved = rust_stats.unresolved;
if verbose {
eprintln!(
" Rust resolution: {} resolved, {} external, {} builtin, {} unresolved",
rust_stats.resolved, rust_stats.external, rust_stats.builtin, rust_stats.unresolved
);
}
}
let has_python_files = graph.graph.node_indices().any(|idx| {
if let crate::graph::node::GraphNode::File(ref f) = graph.graph[idx] {
f.language == "python"
} else {
false
}
});
if has_python_files {
let py_stats = python_resolver::resolve_python_imports(graph, parse_results, project_root);
stats.resolved += py_stats.resolved;
stats.unresolved += py_stats.unresolved;
if verbose {
eprintln!(
" Python resolution: {} resolved, {} unresolved, {} conditional",
py_stats.resolved, py_stats.unresolved, py_stats.conditional,
);
}
}
let has_go_files = graph.graph.node_indices().any(|idx| {
if let crate::graph::node::GraphNode::File(ref f) = graph.graph[idx] {
f.language == "go"
} else {
false
}
});
if has_go_files {
let go_stats = go_resolver::resolve_go_imports(graph, parse_results, project_root, verbose);
stats.go_resolved = go_stats.resolved;
stats.go_stdlib = go_stats.stdlib;
stats.go_external = go_stats.external;
stats.go_unresolved = go_stats.unresolved;
if verbose {
eprintln!(
" Go resolution: {} resolved, {} stdlib, {} external, {} unresolved",
go_stats.resolved, go_stats.stdlib, go_stats.external, go_stats.unresolved
);
}
}
stats
}
fn is_external_package(specifier: &str) -> bool {
!specifier.starts_with('.') && !specifier.starts_with('/')
}
fn extract_package_name(specifier: &str) -> &str {
if specifier.starts_with('@') {
let parts: Vec<&str> = specifier.splitn(3, '/').collect();
if parts.len() >= 2 {
let scope_end = parts[0].len() + 1 + parts[1].len();
&specifier[..scope_end]
} else {
specifier
}
} else {
match specifier.find('/') {
Some(idx) => &specifier[..idx],
None => specifier,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_external_package() {
assert!(is_external_package("react"));
assert!(is_external_package("@org/utils"));
assert!(is_external_package("lodash/merge"));
assert!(!is_external_package("./local"));
assert!(!is_external_package("../parent"));
assert!(!is_external_package("/absolute"));
}
#[test]
fn test_extract_package_name() {
assert_eq!(extract_package_name("react"), "react");
assert_eq!(extract_package_name("@org/utils"), "@org/utils");
assert_eq!(extract_package_name("@org/utils/helpers"), "@org/utils");
assert_eq!(extract_package_name("lodash/merge"), "lodash");
assert_eq!(extract_package_name("lodash"), "lodash");
}
}