use super::type_registry::{module_key_for_file, TypeRegistry};
use crate::config::typed_resolve_enabled;
use crate::db::models::Relationship;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
pub struct TypedHit {
pub qualified_name: String,
pub confidence: f64,
}
pub fn resolve_call(
registry: &TypeRegistry,
caller_file: &str,
callee_name: &str,
receiver: Option<&str>,
) -> Option<TypedHit> {
if callee_name.is_empty() || callee_name.starts_with("__unresolved__") {
let bare = callee_name.trim_start_matches("__unresolved__");
return resolve_call(registry, caller_file, bare, receiver);
}
if let Some(rec) = receiver {
if let Some(hit) = registry.lookup_type_method(rec, callee_name) {
return Some(TypedHit {
qualified_name: hit.qualified_name.clone(),
confidence: 0.97,
});
}
}
let module = module_key_for_file(caller_file);
if let Some(hit) = registry.lookup_in_module(&module, callee_name) {
return Some(TypedHit {
qualified_name: hit.qualified_name.clone(),
confidence: if hit.file_path == caller_file {
0.94
} else {
0.98
},
});
}
if let Some(hit) = registry.lookup_unique_name(callee_name) {
return Some(TypedHit {
qualified_name: hit.qualified_name.clone(),
confidence: 0.92,
});
}
None
}
pub fn caller_file_from_qn(source_qualified: &str) -> String {
let path = Path::new(source_qualified);
if let Some(idx) = source_qualified.find("::") {
let prefix = &source_qualified[..idx];
if prefix.contains('.') || prefix.contains('/') {
return prefix.to_string();
}
}
path.to_string_lossy().to_string()
}
fn language_from_file(file_path: &str) -> Option<&'static str> {
let ext = Path::new(file_path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"go" => Some("go"),
"ts" | "tsx" | "js" | "jsx" => Some("typescript"),
_ => None,
}
}
fn bare_callee(target: &str) -> &str {
if let Some(rest) = target.strip_prefix("__unresolved__") {
return rest;
}
target.rsplit("::").next().unwrap_or(target)
}
fn receiver_from_metadata(meta: &serde_json::Value) -> Option<&str> {
meta.get("receiver")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
}
pub fn apply_typed_resolve(
relationships: &mut [Relationship],
registry: &TypeRegistry,
typed_resolve: &str,
) -> usize {
if registry.is_empty() {
return 0;
}
let mut upgraded = 0;
for rel in relationships.iter_mut() {
if rel.rel_type != "calls" {
continue;
}
let caller_file = caller_file_from_qn(&rel.source_qualified);
let Some(lang) = language_from_file(&caller_file) else {
continue;
};
if !typed_resolve_enabled(typed_resolve, lang) {
continue;
}
if rel
.metadata
.get("resolution_method")
.and_then(|v| v.as_str())
== Some("typed")
{
continue;
}
let callee = bare_callee(&rel.target_qualified);
let receiver = receiver_from_metadata(&rel.metadata);
let Some(hit) = resolve_call(registry, &caller_file, callee, receiver) else {
continue;
};
rel.target_qualified = hit.qualified_name;
rel.confidence = hit.confidence.max(rel.confidence);
let mut meta = rel.metadata.clone();
if !meta.is_object() {
meta = serde_json::json!({});
}
if let Some(obj) = meta.as_object_mut() {
obj.insert("resolution_method".to_string(), serde_json::json!("typed"));
obj.insert("is_resolved".to_string(), serde_json::json!(true));
obj.insert("hybrid_tier".to_string(), serde_json::json!("in_process"));
}
rel.metadata = meta;
upgraded += 1;
}
upgraded
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::models::CodeElement;
use crate::lsp::type_registry::TypeRegistry;
fn elem(qn: &str, etype: &str, name: &str, file: &str, lang: &str) -> CodeElement {
CodeElement {
qualified_name: qn.to_string(),
element_type: etype.to_string(),
name: name.to_string(),
file_path: file.to_string(),
line_start: 1,
line_end: 2,
language: lang.to_string(),
parent_qualified: None,
cluster_id: None,
cluster_label: None,
metadata: serde_json::json!({}),
env: "local".to_string(),
}
}
#[test]
fn go_cross_file_same_package_is_typed() {
let elements = vec![
elem("pkg/a.go::Helper", "function", "Helper", "pkg/a.go", "go"),
elem("pkg/b.go::Main", "function", "Main", "pkg/b.go", "go"),
];
let reg = TypeRegistry::from_elements(&elements);
let hit = resolve_call(®, "pkg/b.go", "Helper", None).unwrap();
assert_eq!(hit.qualified_name, "pkg/a.go::Helper");
assert!(hit.confidence >= 0.95);
}
#[test]
fn typescript_unique_export_is_typed() {
let elements = vec![elem(
"src/util.ts::format",
"function",
"format",
"src/util.ts",
"typescript",
)];
let reg = TypeRegistry::from_elements(&elements);
let hit = resolve_call(®, "src/app.ts", "format", None).unwrap();
assert_eq!(hit.qualified_name, "src/util.ts::format");
}
#[test]
fn apply_typed_resolve_upgrades_unresolved_calls() {
let elements = vec![
elem("pkg/a.go::Helper", "function", "Helper", "pkg/a.go", "go"),
elem("pkg/b.go::Main", "function", "Main", "pkg/b.go", "go"),
];
let reg = TypeRegistry::from_elements(&elements);
let mut rels = vec![Relationship {
id: None,
source_qualified: "pkg/b.go::Main".to_string(),
target_qualified: "__unresolved__Helper".to_string(),
rel_type: "calls".to_string(),
confidence: 0.5,
metadata: serde_json::json!({"resolution_method": "unresolved"}),
env: "local".to_string(),
}];
let n = apply_typed_resolve(&mut rels, ®, "go,ts");
assert_eq!(n, 1);
assert_eq!(rels[0].target_qualified, "pkg/a.go::Helper");
assert_eq!(
rels[0].metadata["resolution_method"].as_str(),
Some("typed")
);
}
#[test]
fn typed_resolve_off_skips_upgrade() {
let elements = vec![elem(
"pkg/a.go::Helper",
"function",
"Helper",
"pkg/a.go",
"go",
)];
let reg = TypeRegistry::from_elements(&elements);
let mut rels = vec![Relationship {
id: None,
source_qualified: "pkg/b.go::Main".to_string(),
target_qualified: "__unresolved__Helper".to_string(),
rel_type: "calls".to_string(),
confidence: 0.5,
metadata: serde_json::json!({"resolution_method": "unresolved"}),
env: "local".to_string(),
}];
assert_eq!(apply_typed_resolve(&mut rels, ®, "off"), 0);
}
#[test]
fn never_spawns_process_on_resolve() {
let reg = TypeRegistry::default();
assert!(resolve_call(®, "x.go", "missing", None).is_none());
}
}