use crate::import_graph::GraphCache;
use crate::project::{ProjectRoot, collect_files};
use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::js_imports::{JSImportBindingIndex, is_import_sensitive_path};
use super::language::{best_path_proximity_candidate, call_language_for_path, same_call_language};
use super::types::CallEdge;
fn symbol_defined_in(
symbol_index: &HashMap<String, Vec<String>>,
symbol_name: &str,
file: &str,
) -> bool {
symbol_index
.get(symbol_name)
.map(|defs| defs.iter().any(|def| def == file))
.unwrap_or(false)
}
fn resolve_reexport_target(
import_bindings: Option<&JSImportBindingIndex>,
symbol_index: &HashMap<String, Vec<String>>,
resolved_file: &str,
canonical_name: &str,
) -> Option<(String, String)> {
let reexport_binding = import_bindings
.and_then(|index| index.get(resolved_file))
.and_then(|bindings| bindings.get(canonical_name).or_else(|| bindings.get("*")))?;
let reexport_file = reexport_binding.resolved_file.as_ref()?;
let reexport_name = match reexport_binding.imported_name.as_deref() {
Some("*") => canonical_name,
Some(name) => name,
None => canonical_name,
};
if symbol_defined_in(symbol_index, reexport_name, reexport_file) {
Some((reexport_file.clone(), reexport_name.to_owned()))
} else {
None
}
}
fn resolve_namespace_reexport_target(
import_bindings: Option<&JSImportBindingIndex>,
symbol_index: &HashMap<String, Vec<String>>,
resolved_file: &str,
namespace_name: &str,
callee_name: &str,
) -> Option<(String, String)> {
let namespace_binding = import_bindings
.and_then(|index| index.get(resolved_file))
.and_then(|bindings| bindings.get(namespace_name))?;
if namespace_binding.imported_name.as_deref() != Some("*") {
return None;
}
let namespace_file = namespace_binding.resolved_file.as_ref()?;
if symbol_defined_in(symbol_index, callee_name, namespace_file) {
return Some((namespace_file.clone(), callee_name.to_owned()));
}
resolve_reexport_target(import_bindings, symbol_index, namespace_file, callee_name)
}
pub(crate) fn collect_candidate_files(root: &Path) -> Result<Vec<PathBuf>> {
collect_files(root, |path| call_language_for_path(path).is_some())
}
pub(crate) fn maybe_import_graph(
project: &ProjectRoot,
files: &[PathBuf],
graph_cache: Option<&GraphCache>,
) -> Option<Arc<HashMap<String, crate::import_graph::FileNode>>> {
let cache = graph_cache?;
let needs_import_graph = files.iter().any(|file| {
let relative = project.to_relative(file);
crate::import_graph::supports_import_graph(&relative)
});
if !needs_import_graph {
return None;
}
let mut graph = crate::import_graph::build_graph_pub(project, cache)
.map(|graph| (*graph).clone())
.unwrap_or_default();
for file in files {
let relative = project.to_relative(file);
if !crate::import_graph::supports_import_graph(&relative) {
continue;
}
let needs_patch = graph
.get(&relative)
.map(|node| node.imports.is_empty())
.unwrap_or(true);
if !needs_patch {
continue;
}
let imports: HashSet<String> = crate::import_graph::extract_imports_for_file(file)
.into_iter()
.filter_map(|module| {
crate::import_graph::resolve_module_for_file(project, file, &module)
})
.collect();
let entry =
graph
.entry(relative.clone())
.or_insert_with(|| crate::import_graph::FileNode {
imports: HashSet::new(),
imported_by: HashSet::new(),
});
entry.imports = imports.clone();
for imported_file in imports {
graph
.entry(imported_file)
.or_insert_with(|| crate::import_graph::FileNode {
imports: HashSet::new(),
imported_by: HashSet::new(),
})
.imported_by
.insert(relative.clone());
}
}
if graph.is_empty() {
None
} else {
Some(Arc::new(graph))
}
}
pub(crate) fn resolve_call_edges(
edges: &mut [CallEdge],
project: &ProjectRoot,
import_graph: Option<&HashMap<String, crate::import_graph::FileNode>>,
import_bindings: Option<&JSImportBindingIndex>,
) {
let db_path = crate::db::index_db_path(project.as_path());
let symbol_index: HashMap<String, Vec<String>> = crate::db::IndexDb::open(&db_path)
.and_then(|db| {
let all = db.all_symbol_names()?;
let mut map: HashMap<String, Vec<String>> = HashMap::new();
for (name, _kind, file, _line, _signature, _name_path) in all {
map.entry(name).or_default().push(file);
}
Ok(map)
})
.unwrap_or_default();
for edge in edges.iter_mut() {
if edge.confidence > 0.0 {
continue; }
let callee = &edge.callee_name;
let caller_file = &edge.caller_file;
let has_imported_namespace_qualifier = edge
.callee_qualifier
.as_deref()
.and_then(|qualifier| {
import_bindings
.and_then(|index| index.get(caller_file))
.and_then(|bindings| bindings.get(qualifier))
})
.map(|binding| binding.imported_name.as_deref() == Some("*"))
.unwrap_or(false);
if !has_imported_namespace_qualifier
&& symbol_defined_in(&symbol_index, callee, caller_file)
{
edge.resolved_file = Some(caller_file.clone());
edge.confidence = 0.90;
edge.resolution_strategy = Some("same_file");
continue;
}
if let Some(namespace_binding) = edge.callee_qualifier.as_deref().and_then(|qualifier| {
import_bindings
.and_then(|index| index.get(caller_file))
.and_then(|bindings| bindings.get(qualifier))
}) && let Some(resolved_file) = namespace_binding.resolved_file.as_ref()
{
match namespace_binding.imported_name.as_deref() {
Some("*") => {
if symbol_defined_in(&symbol_index, callee, resolved_file) {
edge.resolved_file = Some(resolved_file.clone());
edge.confidence = 0.95;
edge.resolution_strategy = Some("import_map");
edge.canonical_callee_name = Some(callee.clone());
continue;
}
if let Some((reexport_file, reexport_name)) = resolve_reexport_target(
import_bindings,
&symbol_index,
resolved_file,
callee,
) {
edge.resolved_file = Some(reexport_file);
edge.confidence = 0.93;
edge.resolution_strategy = Some("import_reexport_map");
edge.canonical_callee_name = Some(reexport_name);
continue;
}
}
Some(namespace_name) => {
if let Some((reexport_file, reexport_name)) = resolve_namespace_reexport_target(
import_bindings,
&symbol_index,
resolved_file,
namespace_name,
callee,
) {
edge.resolved_file = Some(reexport_file);
edge.confidence = 0.93;
edge.resolution_strategy = Some("import_reexport_map");
edge.canonical_callee_name = Some(reexport_name);
continue;
}
}
None => {}
}
}
if let Some(binding) = import_bindings
.and_then(|index| index.get(caller_file))
.and_then(|bindings| bindings.get(callee))
&& let Some(resolved_file) = binding.resolved_file.as_ref()
{
let canonical_name = binding.imported_name.as_deref().unwrap_or(callee);
if symbol_defined_in(&symbol_index, canonical_name, resolved_file) {
edge.resolved_file = Some(resolved_file.clone());
edge.confidence = 0.95;
edge.resolution_strategy = Some("import_map");
edge.canonical_callee_name = Some(canonical_name.to_owned());
continue;
}
if let Some((reexport_file, reexport_name)) = resolve_reexport_target(
import_bindings,
&symbol_index,
resolved_file,
canonical_name,
) {
edge.resolved_file = Some(reexport_file);
edge.confidence = 0.93;
edge.resolution_strategy = Some("import_reexport_map");
edge.canonical_callee_name = Some(reexport_name);
continue;
}
}
if let Some(graph) = import_graph
&& let Some(node) = graph.get(caller_file)
{
for imported_file in &node.imports {
if let Some(defs) = symbol_index.get(callee)
&& defs.iter().any(|f| f == imported_file)
{
edge.resolved_file = Some(imported_file.clone());
edge.confidence = 0.95;
edge.resolution_strategy = Some("import_map");
edge.canonical_callee_name = Some(callee.clone());
break;
}
}
}
if edge.confidence > 0.0 {
continue;
}
if let Some(graph) = import_graph
&& let Some(node) = graph.get(caller_file)
&& let Some(defs) = symbol_index.get(callee)
{
for def_file in defs {
if node.imports.iter().any(|imp| {
def_file.ends_with(imp)
|| def_file.ends_with(&format!("/{imp}"))
|| imp.ends_with(def_file)
|| imp.ends_with(&format!("/{def_file}"))
}) {
edge.resolved_file = Some(def_file.clone());
edge.confidence = 0.70;
edge.resolution_strategy = Some("import_suffix");
edge.canonical_callee_name = Some(callee.clone());
break;
}
}
}
if edge.confidence > 0.0 {
continue;
}
if let Some(defs) = symbol_index.get(callee) {
let same_lang_defs: Vec<&String> = defs
.iter()
.filter(|def| same_call_language(caller_file, def))
.collect();
if same_lang_defs.len() == 1 {
let def = same_lang_defs[0];
edge.resolved_file = Some(def.clone());
if is_import_sensitive_path(caller_file) && def.as_str() != caller_file.as_str() {
edge.confidence = 0.50;
edge.resolution_strategy = Some("path_proximity");
} else {
edge.confidence = 0.65;
edge.resolution_strategy = Some("unique_name");
}
continue;
}
}
if let Some(defs) = symbol_index.get(callee)
&& !defs.is_empty()
&& let Some(best) = best_path_proximity_candidate(caller_file, defs)
{
edge.resolved_file = Some(best.clone());
edge.confidence = 0.50;
edge.resolution_strategy = Some("path_proximity");
continue;
}
edge.confidence = 0.25;
edge.resolution_strategy = Some("unresolved");
}
}