use crate::ingest::imports::{ImportFact, ImportKind};
use crate::resolve::module_resolver::{resolve_module_path, ModulePathIndex};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedSymbol {
pub name: String,
pub file_path: String,
pub kind: String,
}
type SymbolRegistry = HashMap<String, HashMap<String, (String, String)>>;
pub struct CrossFileResolver<'a> {
index: &'a ModulePathIndex,
symbols: SymbolRegistry,
imports: HashMap<String, Vec<ImportFact>>,
}
impl<'a> CrossFileResolver<'a> {
pub fn new(index: &'a ModulePathIndex) -> Self {
Self {
index,
symbols: HashMap::new(),
imports: HashMap::new(),
}
}
pub fn add_local_symbol(&mut self, file_path: &str, name: &str, kind: &str) {
let entry = self.symbols.entry(file_path.to_string()).or_default();
entry.insert(name.to_string(), (name.to_string(), kind.to_string()));
}
pub fn add_import(&mut self, import: ImportFact) {
let file_path = import.file_path.to_str().unwrap_or("").to_string();
self.imports.entry(file_path).or_default().push(import);
}
pub fn resolve_symbol(&self, current_file: &str, identifier: &str) -> Option<ResolvedSymbol> {
if let Some(symbol) = self.find_local_symbol(current_file, identifier) {
return Some(symbol);
}
if let Some(symbol) = self.find_in_explicit_imports(current_file, identifier) {
return Some(symbol);
}
if let Some(symbol) = self.find_in_glob_imports(current_file, identifier) {
return Some(symbol);
}
None
}
fn find_local_symbol(&self, file_path: &str, identifier: &str) -> Option<ResolvedSymbol> {
self.symbols
.get(file_path)
.and_then(|symbols| symbols.get(identifier))
.map(|(name, kind)| ResolvedSymbol {
name: name.clone(),
file_path: file_path.to_string(),
kind: kind.clone(),
})
}
fn find_in_explicit_imports(
&self,
current_file: &str,
identifier: &str,
) -> Option<ResolvedSymbol> {
let imports = self.imports.get(current_file)?;
for import in imports {
if import.is_glob {
continue;
}
let import_index = import.imported_names.iter().position(|n| n == identifier);
if let Some(idx) = import_index {
let target_file = self.resolve_import_path(current_file, import)?;
let local_name = &import.imported_names[idx];
return self
.find_symbol_in_file(&target_file, local_name)
.or_else(|| self.find_first_symbol_in_file(&target_file));
}
}
None
}
fn find_in_glob_imports(&self, current_file: &str, identifier: &str) -> Option<ResolvedSymbol> {
let imports = self.imports.get(current_file)?;
for import in imports {
if !import.is_glob {
continue;
}
let target_file = self.resolve_import_path(current_file, import)?;
if let Some(symbol) = self.find_symbol_in_file(&target_file, identifier) {
return Some(symbol);
}
}
None
}
fn resolve_import_path(&self, current_file: &str, import: &ImportFact) -> Option<String> {
let module_path_str = import.path.join("::");
match import.import_kind {
ImportKind::UseCrate | ImportKind::PlainUse => {
self.index.resolve(&module_path_str)
}
ImportKind::UseSuper => {
resolve_module_path(self.index, current_file, &module_path_str)
}
ImportKind::UseSelf => {
Some(current_file.to_string())
}
ImportKind::ExternCrate => {
None
}
ImportKind::PythonImport => {
self.resolve_python_module(&import.path.join("."))
}
ImportKind::PythonFrom => {
self.resolve_python_module(&import.path.join("."))
}
ImportKind::PythonFromRelative => {
self.resolve_python_relative_import(current_file, 1, &import.path.join("."))
}
ImportKind::PythonFromParent => {
self.resolve_python_relative_import(current_file, 2, &import.path.join("."))
}
ImportKind::PythonFromAncestor => {
let levels = import.path.len();
self.resolve_python_relative_import(current_file, levels, &import.path.join("."))
}
ImportKind::CppLocalInclude => {
self.resolve_cpp_local_include(current_file, &module_path_str)
}
ImportKind::CppSystemInclude => None,
ImportKind::JsImport
| ImportKind::JsDefaultImport
| ImportKind::JsNamespaceImport
| ImportKind::JsSideEffectImport
| ImportKind::JsRequire => None,
ImportKind::JavaImport | ImportKind::JavaStaticImport => None,
ImportKind::TsTypeImport | ImportKind::TsTypeDefaultImport => None,
}
}
fn resolve_python_module(&self, module_path: &str) -> Option<String> {
let py_file = format!("{}.py", module_path);
if let Some(file) = self.index.resolve(&py_file) {
return Some(file);
}
let init_file = format!("{}/__init__.py", module_path);
if let Some(file) = self.index.resolve(&init_file) {
return Some(file);
}
let module_with_slashes = module_path.replace('.', "/");
let py_file_slash = format!("{}.py", module_with_slashes);
if let Some(file) = self.index.resolve(&py_file_slash) {
return Some(file);
}
let init_file_slash = format!("{}/__init__.py", module_with_slashes);
if let Some(file) = self.index.resolve(&init_file_slash) {
return Some(file);
}
None
}
fn resolve_python_relative_import(
&self,
current_file: &str,
levels: usize,
target: &str,
) -> Option<String> {
use std::path::Path;
let current_path = Path::new(current_file);
let mut dir = current_path.parent()?;
for _ in 0..levels {
dir = dir.parent()?;
}
let target_path = if target.is_empty() {
dir.join("__init__.py")
} else {
let as_py = dir.join(format!("{}.py", target));
if as_py.exists() {
as_py
} else {
dir.join(target).join("__init__.py")
}
};
let path_str = target_path.to_str()?;
self.index.resolve(path_str).or_else(|| {
Some(path_str.to_string())
})
}
fn resolve_cpp_local_include(&self, current_file: &str, header_name: &str) -> Option<String> {
use std::path::Path;
let current_path = Path::new(current_file);
let dir = current_path.parent()?;
let same_dir = dir.join(header_name);
let path_str = same_dir.to_str()?;
if let Some(file) = self.index.resolve(path_str) {
return Some(file);
}
if same_dir.exists() {
return Some(path_str.to_string());
}
None
}
fn find_symbol_in_file(&self, file_path: &str, name: &str) -> Option<ResolvedSymbol> {
self.symbols
.get(file_path)
.and_then(|symbols| symbols.get(name))
.map(|(name, kind)| ResolvedSymbol {
name: name.clone(),
file_path: file_path.to_string(),
kind: kind.clone(),
})
}
fn find_first_symbol_in_file(&self, file_path: &str) -> Option<ResolvedSymbol> {
self.symbols
.get(file_path)
.and_then(|symbols| symbols.values().next())
.map(|(name, kind)| ResolvedSymbol {
name: name.clone(),
file_path: file_path.to_string(),
kind: kind.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolved_symbol_equality() {
let s1 = ResolvedSymbol {
name: "foo".to_string(),
file_path: "/src/a.rs".to_string(),
kind: "function".to_string(),
};
let s2 = ResolvedSymbol {
name: "foo".to_string(),
file_path: "/src/a.rs".to_string(),
kind: "function".to_string(),
};
assert_eq!(s1, s2);
}
}