use super::graph::FunctionId;
use crate::types::Tag;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Candidate {
pub target: FunctionId,
pub confidence: f64,
pub type_hint: Option<String>,
}
pub struct ResolutionContext<'a> {
pub tags: &'a [Tag],
pub definitions: HashMap<&'a str, Vec<&'a Tag>>,
pub type_map: HashMap<String, String>,
pub imports: HashMap<String, (String, String)>,
}
impl<'a> ResolutionContext<'a> {
pub fn new(tags: &'a [Tag]) -> Self {
let mut definitions: HashMap<&str, Vec<&Tag>> = HashMap::new();
let mut type_map = HashMap::new();
let mut imports = HashMap::new();
for tag in tags {
if tag.kind.is_definition() {
definitions.entry(tag.name.as_ref()).or_default().push(tag);
}
if let Some(ref meta) = tag.metadata {
if let Some(var_type) = meta.get("var_type") {
if let Some(var_name) = meta.get("var_name") {
type_map.insert(var_name.clone(), var_type.clone());
}
}
if let Some(receiver_type) = meta.get("receiver_type") {
if let Some(receiver) = meta.get("receiver") {
type_map.insert(receiver.clone(), receiver_type.clone());
}
}
if let Some(import_module) = meta.get("import_module") {
if let Some(import_name) = meta.get("import_name") {
imports.insert(
import_name.clone(),
(import_module.clone(), import_name.clone()),
);
}
}
}
}
Self {
tags,
definitions,
type_map,
imports,
}
}
pub fn find_definitions(&self, name: &str) -> Vec<&Tag> {
self.definitions.get(name).cloned().unwrap_or_default()
}
pub fn get_type(&self, name: &str) -> Option<&String> {
self.type_map.get(name)
}
pub fn get_import(&self, name: &str) -> Option<&(String, String)> {
self.imports.get(name)
}
}
pub trait ResolutionStrategy: Send + Sync {
fn name(&self) -> &'static str;
fn resolve(&self, call: &Tag, context: &ResolutionContext) -> Vec<Candidate>;
fn supports_language(&self, lang: &str) -> bool {
let _ = lang;
true
}
}
pub struct SameFileStrategy {
pub confidence: f64,
}
impl SameFileStrategy {
pub fn new() -> Self {
Self { confidence: 0.9 }
}
}
impl Default for SameFileStrategy {
fn default() -> Self {
Self::new()
}
}
impl ResolutionStrategy for SameFileStrategy {
fn name(&self) -> &'static str {
"same_file"
}
fn resolve(&self, call: &Tag, context: &ResolutionContext) -> Vec<Candidate> {
let defs = context.find_definitions(&call.name);
defs.into_iter()
.filter(|def| def.rel_fname == call.rel_fname)
.map(|def| Candidate {
target: FunctionId::new(def.rel_fname.clone(), def.name.clone(), def.line)
.with_parent_opt(def.parent_name.clone()),
confidence: self.confidence,
type_hint: None,
})
.collect()
}
}
pub struct TypeHintStrategy {
pub confidence: f64,
}
impl TypeHintStrategy {
pub fn new() -> Self {
Self { confidence: 0.85 }
}
}
impl Default for TypeHintStrategy {
fn default() -> Self {
Self::new()
}
}
impl ResolutionStrategy for TypeHintStrategy {
fn name(&self) -> &'static str {
"type_hint"
}
fn supports_language(&self, lang: &str) -> bool {
matches!(lang, "python" | "typescript" | "tsx")
}
fn resolve(&self, call: &Tag, context: &ResolutionContext) -> Vec<Candidate> {
let receiver = call
.metadata
.as_ref()
.and_then(|m| m.get("receiver"))
.map(|s| s.as_str());
let Some(receiver) = receiver else {
return vec![];
};
let Some(receiver_type) = context.get_type(receiver) else {
return vec![];
};
let defs = context.find_definitions(&call.name);
defs.into_iter()
.filter(|def| {
def.parent_name
.as_ref()
.map_or(false, |p| p.as_ref() == receiver_type)
})
.map(|def| Candidate {
target: FunctionId::new(def.rel_fname.clone(), def.name.clone(), def.line)
.with_parent(receiver_type.as_str()),
confidence: self.confidence,
type_hint: Some(format!("{}: {}", receiver, receiver_type)),
})
.collect()
}
}
pub struct ImportStrategy {
pub confidence: f64,
}
impl ImportStrategy {
pub fn new() -> Self {
Self { confidence: 0.8 }
}
}
impl Default for ImportStrategy {
fn default() -> Self {
Self::new()
}
}
impl ResolutionStrategy for ImportStrategy {
fn name(&self) -> &'static str {
"import"
}
fn resolve(&self, call: &Tag, context: &ResolutionContext) -> Vec<Candidate> {
let Some((module, original_name)) = context.get_import(&call.name) else {
return vec![];
};
let defs = context.find_definitions(original_name);
defs.into_iter()
.filter(|def| {
let module_path = module.replace('.', "/");
def.rel_fname.contains(&module_path)
})
.map(|def| Candidate {
target: FunctionId::new(def.rel_fname.clone(), def.name.clone(), def.line)
.with_parent_opt(def.parent_name.clone()),
confidence: self.confidence,
type_hint: Some(format!("from {} import {}", module, original_name)),
})
.collect()
}
}
pub struct NameMatchStrategy {
pub confidence: f64,
pub proximity_boost: f64,
}
impl NameMatchStrategy {
pub fn new() -> Self {
Self {
confidence: 0.5,
proximity_boost: 0.1,
}
}
}
impl Default for NameMatchStrategy {
fn default() -> Self {
Self::new()
}
}
impl ResolutionStrategy for NameMatchStrategy {
fn name(&self) -> &'static str {
"name_match"
}
fn resolve(&self, call: &Tag, context: &ResolutionContext) -> Vec<Candidate> {
let defs = context.find_definitions(&call.name);
let call_dir = std::path::Path::new(call.rel_fname.as_ref())
.parent()
.map(|p| p.to_string_lossy().to_string());
defs.into_iter()
.map(|def| {
let def_dir = std::path::Path::new(def.rel_fname.as_ref())
.parent()
.map(|p| p.to_string_lossy().to_string());
let confidence = if call_dir == def_dir {
self.confidence + self.proximity_boost
} else {
self.confidence
};
Candidate {
target: FunctionId::new(def.rel_fname.clone(), def.name.clone(), def.line)
.with_parent_opt(def.parent_name.clone()),
confidence,
type_hint: None,
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TagKind;
fn make_def(file: &str, name: &str, line: u32, parent: Option<&str>) -> Tag {
Tag {
rel_fname: Arc::from(file),
fname: Arc::from(file),
line,
name: Arc::from(name),
kind: TagKind::Def,
node_type: Arc::from("function"),
parent_name: parent.map(|s| Arc::from(s)),
parent_line: None,
signature: None,
fields: None,
metadata: None,
}
}
fn make_call(file: &str, name: &str, line: u32, receiver: Option<&str>) -> Tag {
let metadata = receiver.map(|r| {
let mut m = HashMap::new();
m.insert("receiver".to_string(), r.to_string());
m
});
Tag {
rel_fname: Arc::from(file),
fname: Arc::from(file),
line,
name: Arc::from(name),
kind: TagKind::Ref,
node_type: Arc::from("call"),
parent_name: None,
parent_line: None,
signature: None,
fields: None,
metadata,
}
}
#[test]
fn test_same_file_strategy() {
let tags = vec![
make_def("test.py", "helper", 5, None),
make_def("other.py", "helper", 10, None),
make_call("test.py", "helper", 20, None),
];
let ctx = ResolutionContext::new(&tags);
let strategy = SameFileStrategy::new();
let call = &tags[2];
let candidates = strategy.resolve(call, &ctx);
assert_eq!(candidates.len(), 1);
assert_eq!(candidates[0].target.file.as_ref(), "test.py");
assert_eq!(candidates[0].target.line, 5);
}
#[test]
fn test_name_match_strategy() {
let tags = vec![
make_def("test.py", "helper", 5, None),
make_def("other.py", "helper", 10, None),
make_call("another.py", "helper", 20, None),
];
let ctx = ResolutionContext::new(&tags);
let strategy = NameMatchStrategy::new();
let call = &tags[2];
let candidates = strategy.resolve(call, &ctx);
assert_eq!(candidates.len(), 2);
}
}