use super::graph::{CallEdge, CallGraph, FunctionId};
use super::strategies::{Candidate, ResolutionContext, ResolutionStrategy};
use crate::types::{Tag, TagKind};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ResolverConfig {
pub min_confidence: f64,
pub include_unresolved: bool,
pub language: Option<String>,
}
impl Default for ResolverConfig {
fn default() -> Self {
Self {
min_confidence: 0.3,
include_unresolved: false,
language: None,
}
}
}
pub struct CallResolver {
strategies: Vec<Box<dyn ResolutionStrategy>>,
config: ResolverConfig,
}
impl CallResolver {
pub fn new() -> Self {
Self {
strategies: vec![],
config: ResolverConfig::default(),
}
}
pub fn with_config(mut self, config: ResolverConfig) -> Self {
self.config = config;
self
}
pub fn add_strategy(&mut self, strategy: Box<dyn ResolutionStrategy>) {
self.strategies.push(strategy);
}
pub fn with_strategy(mut self, strategy: Box<dyn ResolutionStrategy>) -> Self {
self.add_strategy(strategy);
self
}
pub fn build_graph(&self, tags: &[Tag]) -> CallGraph {
let context = ResolutionContext::new(tags);
let mut graph = CallGraph::new();
for tag in tags {
if tag.kind.is_definition() {
let node_type = tag.node_type.as_ref();
if node_type.contains("function") || node_type.contains("method") {
let id = FunctionId::new(tag.rel_fname.clone(), tag.name.clone(), tag.line)
.with_parent_opt(tag.parent_name.clone());
graph.add_function(id);
}
}
}
for tag in tags {
if !tag.kind.is_reference() {
continue;
}
let caller = self.find_enclosing_function(tag, tags);
let Some(caller) = caller else {
continue; };
let mut all_candidates: Vec<(Candidate, &str)> = vec![];
for strategy in &self.strategies {
if let Some(ref lang) = self.config.language {
if !strategy.supports_language(lang) {
continue;
}
}
let candidates = strategy.resolve(tag, &context);
for c in candidates {
all_candidates.push((c, strategy.name()));
}
}
all_candidates.sort_by(|a, b| b.0.confidence.partial_cmp(&a.0.confidence).unwrap());
if let Some((best, strategy_name)) = all_candidates.first() {
if best.confidence >= self.config.min_confidence {
let edge = CallEdge::new(best.confidence, *strategy_name, tag.line);
let edge = if let Some(ref hint) = best.type_hint {
edge.with_type_hint(hint.clone())
} else {
edge
};
graph.add_call(caller, best.target.clone(), edge);
}
} else if self.config.include_unresolved {
let unresolved = FunctionId::new(
Arc::<str>::from("?"), tag.name.clone(),
0,
);
let edge = CallEdge::new(0.0, "unresolved", tag.line);
graph.add_call(caller, unresolved, edge);
}
}
graph
}
fn find_enclosing_function(&self, tag: &Tag, all_tags: &[Tag]) -> Option<FunctionId> {
let mut functions: Vec<&Tag> = all_tags
.iter()
.filter(|t| {
t.rel_fname == tag.rel_fname
&& t.kind.is_definition()
&& (t.node_type.contains("function") || t.node_type.contains("method"))
})
.collect();
functions.sort_by_key(|t| std::cmp::Reverse(t.line));
for func in functions {
if func.line <= tag.line {
return Some(
FunctionId::new(func.rel_fname.clone(), func.name.clone(), func.line)
.with_parent_opt(func.parent_name.clone()),
);
}
}
None
}
pub fn stats(&self, tags: &[Tag]) -> ResolutionStats {
let context = ResolutionContext::new(tags);
let mut stats = ResolutionStats::default();
for tag in tags {
if !tag.kind.is_reference() {
continue;
}
stats.total_calls += 1;
let mut resolved = false;
for strategy in &self.strategies {
let candidates = strategy.resolve(tag, &context);
if !candidates.is_empty() {
*stats
.by_strategy
.entry(strategy.name().to_string())
.or_insert(0) += 1;
resolved = true;
break;
}
}
if !resolved {
stats.unresolved += 1;
}
}
stats
}
}
impl Default for CallResolver {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct ResolutionStats {
pub total_calls: usize,
pub unresolved: usize,
pub by_strategy: HashMap<String, usize>,
}
impl ResolutionStats {
pub fn resolution_rate(&self) -> f64 {
if self.total_calls == 0 {
return 1.0;
}
(self.total_calls - self.unresolved) as f64 / self.total_calls as f64
}
}
pub struct ResolverBuilder {
config: ResolverConfig,
same_file: bool,
type_hints: bool,
imports: bool,
name_match: bool,
}
impl ResolverBuilder {
pub fn new() -> Self {
Self {
config: ResolverConfig::default(),
same_file: true,
type_hints: true,
imports: true,
name_match: true,
}
}
pub fn config(mut self, config: ResolverConfig) -> Self {
self.config = config;
self
}
pub fn same_file(mut self, enabled: bool) -> Self {
self.same_file = enabled;
self
}
pub fn type_hints(mut self, enabled: bool) -> Self {
self.type_hints = enabled;
self
}
pub fn imports(mut self, enabled: bool) -> Self {
self.imports = enabled;
self
}
pub fn name_match(mut self, enabled: bool) -> Self {
self.name_match = enabled;
self
}
pub fn build(self) -> CallResolver {
use super::strategies::*;
let mut resolver = CallResolver::new().with_config(self.config);
if self.same_file {
resolver.add_strategy(Box::new(SameFileStrategy::new()));
}
if self.type_hints {
resolver.add_strategy(Box::new(TypeHintStrategy::new()));
}
if self.imports {
resolver.add_strategy(Box::new(ImportStrategy::new()));
}
if self.name_match {
resolver.add_strategy(Box::new(NameMatchStrategy::new()));
}
resolver
}
}
impl Default for ResolverBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_def(file: &str, name: &str, line: u32) -> 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: None,
parent_line: None,
signature: None,
fields: None,
metadata: None,
}
}
fn make_call(file: &str, name: &str, line: u32) -> Tag {
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: None,
}
}
#[test]
fn test_build_graph() {
let tags = vec![
make_def("test.py", "main", 1),
make_def("test.py", "helper", 10),
make_call("test.py", "helper", 5), ];
let resolver = ResolverBuilder::new().build();
let graph = resolver.build_graph(&tags);
assert_eq!(graph.function_count(), 2);
assert_eq!(graph.call_count(), 1);
}
#[test]
fn test_resolution_stats() {
let tags = vec![
make_def("test.py", "main", 1),
make_def("test.py", "helper", 10),
make_call("test.py", "helper", 5),
make_call("test.py", "unknown", 7), ];
let resolver = ResolverBuilder::new()
.name_match(false) .build();
let stats = resolver.stats(&tags);
assert_eq!(stats.total_calls, 2);
assert!(stats.resolution_rate() > 0.0);
}
}