use std::collections::{HashSet, VecDeque};
use std::path::{Path, PathBuf};
use super::cross_file_types::{
CallSite, CallType, ClassDef, FileIR, FuncDef, VarType,
};
use super::import_resolver::{ReExportTracer, DEFAULT_MAX_DEPTH};
use super::type_resolver::{resolve_receiver_type};
use crate::types::Language;
use super::types::{FuncIndex, ClassIndex, ClassEntry, capitalize_first};
use super::module_path::path_to_module;
use super::imports::{ImportMap, ModuleImports};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedTarget {
pub file: PathBuf,
pub name: String,
pub line: Option<u32>,
pub is_method: bool,
pub class_name: Option<String>,
}
impl ResolvedTarget {
pub fn function(file: PathBuf, name: impl Into<String>, line: Option<u32>) -> Self {
Self {
file,
name: name.into(),
line,
is_method: false,
class_name: None,
}
}
pub fn method(
file: PathBuf,
name: impl Into<String>,
class_name: impl Into<String>,
line: Option<u32>,
) -> Self {
Self {
file,
name: name.into(),
line,
is_method: true,
class_name: Some(class_name.into()),
}
}
pub fn qualified_name(&self) -> String {
if let Some(ref class) = self.class_name {
format!("{}.{}", class, self.name)
} else {
self.name.clone()
}
}
}
pub struct ResolutionContext<'a, 'b> {
pub import_map: &'a ImportMap,
pub module_imports: &'a ModuleImports,
pub func_index: &'a FuncIndex,
pub class_index: &'a ClassIndex,
pub reexport_tracer: &'a mut ReExportTracer<'b>,
pub current_file: &'a Path,
pub root: &'a Path,
pub language: &'a str,
}
fn constructor_method_candidates(language: &str, class_name: &str) -> Vec<String> {
match language.to_lowercase().as_str() {
"python" => vec!["__init__".to_string()],
"ruby" => vec!["initialize".to_string()],
"php" => vec!["__construct".to_string()],
"typescript" | "javascript" => vec!["constructor".to_string()],
"swift" => vec!["init".to_string()],
"kotlin" => vec!["init".to_string(), "constructor".to_string()],
"java" | "csharp" | "cpp" => vec![class_name.to_string()],
"scala" => vec![class_name.to_string()],
_ => Vec::new(),
}
}
pub(crate) fn resolve_constructor_target(
class_name: &str,
class_entry: &ClassEntry,
func_index: &FuncIndex,
language: &str,
) -> Option<ResolvedTarget> {
for ctor_name in constructor_method_candidates(language, class_name) {
if class_entry.methods.contains(&ctor_name) {
let qualified = format!("{}.{}", class_name, ctor_name);
let module = path_to_module(&class_entry.file_path, language);
if let Some(entry) = func_index.get(&module, &qualified) {
return Some(ResolvedTarget::method(
entry.file_path.clone(),
ctor_name,
class_name.to_string(),
Some(entry.line),
));
}
return Some(ResolvedTarget::method(
class_entry.file_path.clone(),
ctor_name,
class_name.to_string(),
Some(class_entry.line),
));
}
}
None
}
pub(crate) fn compute_via_import(
call_site: &CallSite,
import_map: &ImportMap,
module_imports: &ModuleImports,
) -> Option<String> {
match call_site.call_type {
CallType::Method | CallType::Attr => {
if let Some(ref receiver) = call_site.receiver {
if let Some(module_path) = module_imports.get(receiver) {
return Some(module_path.clone());
}
if let Some((module_path, original_name)) = import_map.get(receiver) {
return Some(format!("{}.{}", module_path, original_name));
}
}
None
}
CallType::Direct | CallType::Ref | CallType::Static => {
if let Some((module_path, _)) = import_map.get(&call_site.target) {
return Some(module_path.clone());
}
None
}
CallType::Intra => None,
}
}
pub(crate) fn enclosing_class_for_call(funcs: &[FuncDef], call_site: &CallSite) -> Option<String> {
let line = call_site.line?;
let mut best: Option<&FuncDef> = None;
let mut best_span: u32 = u32::MAX;
for func in funcs {
if line < func.line || line > func.end_line {
continue;
}
let span = func.end_line.saturating_sub(func.line);
if span < best_span {
best_span = span;
best = Some(func);
}
}
if let Some(func) = best {
if let Some(class_name) = &func.class_name {
return Some(class_name.clone());
}
}
if let Some((class_name, _)) = call_site.caller.split_once('.') {
return Some(class_name.to_string());
}
let mut unique: Option<String> = None;
for func in funcs {
if func.name == call_site.caller {
if let Some(class_name) = &func.class_name {
if let Some(ref existing) = unique {
if existing != class_name {
return None;
}
} else {
unique = Some(class_name.clone());
}
}
}
}
unique
}
pub(crate) fn first_base_for_class(classes: &[ClassDef], class_name: &str) -> Option<String> {
classes
.iter()
.find(|class_def| class_def.name == class_name)
.and_then(|class_def| class_def.bases.first())
.cloned()
}
pub fn apply_type_resolution(file_ir: &mut FileIR, source: &str, language: Language) {
let supports_type_resolution = matches!(
language,
Language::Python
| Language::TypeScript
| Language::JavaScript
| Language::Go
| Language::Rust
| Language::Java
| Language::C
| Language::Cpp
| Language::Ruby
| Language::Kotlin
| Language::Swift
| Language::CSharp
| Language::Scala
| Language::Php
| Language::Lua
| Language::Luau
| Language::Elixir
| Language::Ocaml
);
let (funcs, classes, var_types, calls) = (
&file_ir.funcs,
&file_ir.classes,
&file_ir.var_types,
&mut file_ir.calls,
);
for (caller_name, call_sites) in calls.iter_mut() {
for call_site in call_sites.iter_mut() {
if !matches!(call_site.call_type, CallType::Method | CallType::Attr) {
continue;
}
if call_site.receiver_type.is_some() {
continue;
}
let receiver = match call_site.receiver.as_deref() {
Some(r) => r,
None => continue,
};
let line = match call_site.line {
Some(l) => l,
None => continue,
};
let receiver_key = receiver.trim();
let receiver_simple = if receiver_key == "super"
|| receiver_key.starts_with("super(")
|| receiver_key.starts_with("super<")
{
"super"
} else {
receiver_key
};
let enclosing_class = enclosing_class_for_call(funcs, call_site);
let base_class = enclosing_class
.as_deref()
.and_then(|class_name| first_base_for_class(classes, class_name));
if supports_type_resolution {
let (resolved, confidence) = resolve_receiver_type(
language,
source,
line,
receiver_key,
enclosing_class.as_deref(),
);
if resolved.is_some() && confidence != crate::types::Confidence::Low {
call_site.receiver_type = resolved;
continue;
}
}
if call_site.receiver_type.is_none() && !var_types.is_empty() {
let vartype_key = receiver_key.strip_prefix('$').unwrap_or(receiver_key);
if let Some(type_name) =
find_best_vartype(var_types, vartype_key, caller_name, line)
{
call_site.receiver_type = Some(type_name);
continue;
}
}
if call_site.receiver_type.is_some() {
continue;
}
match receiver_simple {
"self" | "cls" | "this" | "Self" => {
if let Some(class_name) = enclosing_class {
call_site.receiver_type = Some(class_name);
}
}
"super" | "base" => {
if let Some(base_name) = base_class {
call_site.receiver_type = Some(base_name);
}
}
_ => {}
}
}
}
}
fn find_best_vartype(
var_types: &[VarType],
receiver_name: &str,
caller_name: &str,
call_line: u32,
) -> Option<String> {
let mut best_scoped: Option<&VarType> = None;
let mut best_module: Option<&VarType> = None;
for vt in var_types {
if vt.var_name != receiver_name {
continue;
}
if vt.line > call_line {
continue;
}
match &vt.scope {
Some(scope) if scope == caller_name => {
if best_scoped.is_none_or(|prev| vt.line > prev.line) {
best_scoped = Some(vt);
}
}
None => {
if best_module.is_none_or(|prev| vt.line > prev.line) {
best_module = Some(vt);
}
}
_ => {
}
}
}
best_scoped
.or(best_module)
.map(|vt| vt.type_name.clone())
}
pub(crate) fn resolve_caller_name(file_ir: &FileIR, call_site: &CallSite) -> String {
let line = match call_site.line {
Some(l) => l,
None => return call_site.caller.clone(),
};
let mut best: Option<&FuncDef> = None;
let mut best_span: u32 = u32::MAX;
for func in &file_ir.funcs {
if line < func.line || line > func.end_line {
continue;
}
let span = func.end_line.saturating_sub(func.line);
if span <= best_span {
best_span = span;
best = Some(func);
}
}
if let Some(func) = best {
if func.is_method {
if let Some(ref class_name) = func.class_name {
return format!("{}.{}", class_name, func.name);
}
}
return func.name.clone();
}
call_site.caller.clone()
}
fn resolve_reexported_name(
module_path: &str,
name: &str,
tracer: &mut ReExportTracer<'_>,
func_index: &FuncIndex,
class_index: &ClassIndex,
language: &str,
) -> Option<ResolvedTarget> {
if language != "python" {
return None;
}
let traced = tracer.trace(module_path, name, DEFAULT_MAX_DEPTH)?;
let traced_module = path_to_module(&traced.definition_file, language);
if let Some(entry) = func_index.get(&traced_module, &traced.qualified_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: traced.qualified_name.clone(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(class_entry) = class_index.get(&traced.qualified_name) {
if let Some(ctor_target) = resolve_constructor_target(
&traced.qualified_name,
class_entry,
func_index,
language,
) {
return Some(ctor_target);
}
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: traced.qualified_name.clone(),
line: Some(class_entry.line),
is_method: false,
class_name: None,
});
}
None
}
fn resolve_reexported_receiver_target(
module_path: &str,
receiver_name: &str,
method_name: &str,
tracer: &mut ReExportTracer<'_>,
func_index: &FuncIndex,
class_index: &ClassIndex,
language: &str,
) -> Option<ResolvedTarget> {
if language != "python" {
return None;
}
let traced = tracer.trace(module_path, receiver_name, DEFAULT_MAX_DEPTH)?;
let traced_module = path_to_module(&traced.definition_file, language);
if let Some(entry) = func_index.get(&traced_module, method_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: method_name.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(class_entry) = class_index.get(method_name) {
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: method_name.to_string(),
line: Some(class_entry.line),
is_method: false,
class_name: None,
});
}
None
}
pub fn resolve_call(
target: &str,
call_type: &CallType,
context: &mut ResolutionContext<'_, '_>,
) -> Option<ResolvedTarget> {
let import_map = context.import_map;
let func_index = context.func_index;
let class_index = context.class_index;
let current_file = context.current_file;
let language = context.language;
if target.contains("__import__") || target.contains("importlib") {
return None;
}
if matches!(language, "javascript" | "js" | "typescript" | "tsx") && target == "import" {
return None;
}
let current_module = path_to_module(current_file, language);
match call_type {
CallType::Intra => {
if let Some(entry) = func_index.get(¤t_module, target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(class_entry) = class_index.get(target) {
if let Some(ctor) = resolve_constructor_target(target, class_entry, func_index, language) {
return Some(ctor);
}
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: target.to_string(),
line: Some(class_entry.line),
is_method: false,
class_name: None,
});
}
None
}
CallType::Direct => {
if let Some(entry) = func_index.get(¤t_module, target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some((module_path, original_name)) = import_map.get(target) {
let simple_module = module_path.split('.').next_back().unwrap_or(module_path);
let stripped_ext = module_path
.strip_suffix(".js")
.or_else(|| module_path.strip_suffix(".jsx"))
.or_else(|| module_path.strip_suffix(".ts"))
.or_else(|| module_path.strip_suffix(".tsx"))
.or_else(|| module_path.strip_suffix(".mjs"))
.unwrap_or(module_path);
let mut bare = stripped_ext;
while let Some(rest) = bare.strip_prefix("../") {
bare = rest;
}
let bare_module = bare.strip_prefix("./").unwrap_or(bare);
if stripped_ext != bare_module {
if let Some(entry) = func_index.get(stripped_ext, original_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: original_name.clone(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
}
if let Some(entry) = func_index.get(bare_module, original_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: original_name.clone(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(entry) = func_index.get(simple_module, original_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: original_name.clone(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(entry) = func_index.get(module_path, original_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: original_name.clone(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(class_entry) = class_index.get(original_name) {
if let Some(ctor) = resolve_constructor_target(original_name, class_entry, func_index, language) {
return Some(ctor);
}
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: original_name.clone(),
line: Some(class_entry.line),
is_method: false,
class_name: None,
});
}
if let Some(resolved) = resolve_reexported_name(
module_path,
original_name,
context.reexport_tracer,
func_index,
class_index,
language,
) {
return Some(resolved);
}
}
if let Some(class_entry) = class_index.get(target) {
if let Some(ctor_target) =
resolve_constructor_target(target, class_entry, func_index, language)
{
return Some(ctor_target);
}
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: target.to_string(),
line: Some(class_entry.line),
is_method: false,
class_name: None,
});
}
None
}
CallType::Attr => {
None
}
CallType::Method => {
None
}
CallType::Ref => {
if let Some((module_path, original_name)) = import_map.get(target) {
if let Some(entry) = func_index.get(module_path, original_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: original_name.clone(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if let Some(resolved) = resolve_reexported_name(
module_path,
original_name,
context.reexport_tracer,
func_index,
class_index,
language,
) {
return Some(resolved);
}
}
if let Some(entry) = func_index.get(¤t_module, target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
None
}
CallType::Static => {
if let Some(sep_pos) = target.find("::") {
let class_name = &target[..sep_pos];
let method_name = &target[sep_pos + 2..];
if let Some(resolved) = resolve_call_with_receiver(
target,
class_name,
None,
call_type,
context,
) {
return Some(resolved);
}
if let Some(entry) = func_index.get(¤t_module, target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
let qualified_dot = format!("{}.{}", class_name, method_name);
if let Some(entry) = func_index.get(¤t_module, &qualified_dot) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: method_name.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: Some(class_name.to_string()),
});
}
if let Some(resolved) = resolve_method_in_class(
class_name,
method_name,
class_index,
func_index,
language,
)
.or_else(|| {
resolve_method_in_bases(class_name, method_name, class_index, func_index, language)
}) {
return Some(resolved);
}
}
None
}
}
}
pub(crate) fn resolve_method_in_class(
class_name: &str,
method_name: &str,
class_index: &ClassIndex,
func_index: &FuncIndex,
language: &str,
) -> Option<ResolvedTarget> {
let class_entry = class_index.get(class_name)?;
let module = path_to_module(&class_entry.file_path, language);
let qualified = format!("{}.{}", class_name, method_name);
if let Some(entry) = func_index.get(&module, &qualified) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: method_name.to_string(),
line: Some(entry.line),
is_method: true,
class_name: Some(class_name.to_string()),
});
}
if class_entry.methods.contains(&method_name.to_string()) {
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: method_name.to_string(),
line: Some(class_entry.line),
is_method: true,
class_name: Some(class_name.to_string()),
});
}
None
}
pub(crate) fn resolve_method_in_bases(
class_name: &str,
method_name: &str,
class_index: &ClassIndex,
func_index: &FuncIndex,
language: &str,
) -> Option<ResolvedTarget> {
let mut queue: VecDeque<String> = VecDeque::new();
let mut seen: HashSet<String> = HashSet::new();
if let Some(entry) = class_index.get(class_name) {
for base in &entry.bases {
queue.push_back(base.clone());
}
}
while let Some(base) = queue.pop_front() {
if !seen.insert(base.clone()) {
continue;
}
if let Some(resolved) =
resolve_method_in_class(&base, method_name, class_index, func_index, language)
{
return Some(resolved);
}
if let Some(entry) = class_index.get(&base) {
for parent in &entry.bases {
if !seen.contains(parent) {
queue.push_back(parent.clone());
}
}
}
}
None
}
fn is_stdlib_type(name: &str) -> bool {
matches!(
name,
"dict" | "list" | "set" | "tuple" | "frozenset" | "str" | "bytes"
| "bytearray" | "int" | "float" | "bool" | "complex" | "object"
| "type" | "range" | "memoryview" | "slice" | "None" | "NoneType"
| "OrderedDict" | "defaultdict" | "deque" | "Counter" | "ChainMap"
| "namedtuple" | "UserDict" | "UserList" | "UserString"
| "StringIO" | "BytesIO" | "TextIOWrapper" | "BufferedReader"
| "Path" | "PurePath" | "PosixPath" | "WindowsPath"
| "Dict" | "List" | "Set" | "Tuple" | "FrozenSet" | "Optional"
| "Union" | "Any" | "Callable" | "Type" | "Sequence" | "Mapping"
| "MutableMapping" | "MutableSequence" | "MutableSet" | "Iterator"
| "Iterable" | "Generator" | "Coroutine" | "AsyncGenerator"
| "Array" | "Hash" | "String" | "Integer" | "Float" | "Symbol"
| "Regexp" | "Proc" | "Lambda" | "IO" | "File" | "Dir"
)
}
fn is_builtin_method_name(name: &str) -> bool {
matches!(
name,
"items" | "values" | "keys" | "get" | "pop" | "update" | "setdefault"
| "clear" | "copy" | "popitem"
| "append" | "extend" | "insert" | "remove" | "sort" | "reverse" | "count" | "index"
| "add" | "discard" | "union" | "intersection" | "difference"
| "strip" | "split" | "join" | "replace" | "format" | "encode" | "decode"
| "startswith" | "endswith" | "lower" | "upper" | "find"
| "close" | "read" | "write" | "flush" | "seek" | "tell" | "readline"
| "MarshalJSON" | "UnmarshalJSON" | "MarshalText" | "UnmarshalText"
| "invoke" | "call" | "run" | "execute" | "send" | "receive"
| "start" | "stop" | "reset" | "setup" | "teardown"
)
}
fn is_in_inheritance_chain(
receiver_class: &str,
candidate_class: &str,
class_index: &ClassIndex,
) -> bool {
if receiver_class == candidate_class {
return true;
}
{
let mut queue: VecDeque<String> = VecDeque::new();
let mut seen: HashSet<String> = HashSet::new();
if let Some(entry) = class_index.get(receiver_class) {
for base in &entry.bases {
queue.push_back(base.clone());
}
}
while let Some(base) = queue.pop_front() {
if !seen.insert(base.clone()) {
continue;
}
if base == candidate_class {
return true;
}
if let Some(entry) = class_index.get(&base) {
for parent in &entry.bases {
if !seen.contains(parent) {
queue.push_back(parent.clone());
}
}
}
}
}
{
let mut queue: VecDeque<String> = VecDeque::new();
let mut seen: HashSet<String> = HashSet::new();
if let Some(entry) = class_index.get(candidate_class) {
for base in &entry.bases {
queue.push_back(base.clone());
}
}
while let Some(base) = queue.pop_front() {
if !seen.insert(base.clone()) {
continue;
}
if base == receiver_class {
return true;
}
if let Some(entry) = class_index.get(&base) {
for parent in &entry.bases {
if !seen.contains(parent) {
queue.push_back(parent.clone());
}
}
}
}
}
false
}
pub fn resolve_call_with_receiver(
target: &str,
receiver: &str,
receiver_type: Option<&str>,
_call_type: &CallType,
context: &mut ResolutionContext<'_, '_>,
) -> Option<ResolvedTarget> {
let import_map = context.import_map;
let module_imports = context.module_imports;
let func_index = context.func_index;
let class_index = context.class_index;
let current_file = context.current_file;
let language = context.language;
let current_module = path_to_module(current_file, language);
let bare_target = normalize_receiver_target(target, receiver);
if let Some(resolved) = resolve_with_receiver_type(
receiver_type,
bare_target,
class_index,
func_index,
language,
) {
return Some(resolved);
}
if let Some(resolved) = resolve_self_receiver_in_current_file(
receiver,
bare_target,
¤t_module,
func_index,
class_index,
) {
return Some(resolved);
}
let mut receiver_context = ReceiverLookupContext {
func_index,
class_index,
reexport_tracer: context.reexport_tracer,
language,
};
if let Some(resolved) = resolve_module_import_receiver(
target,
receiver,
bare_target,
module_imports,
&mut receiver_context,
) {
return Some(resolved);
}
if let Some(resolved) = resolve_import_map_receiver(
target,
receiver,
bare_target,
import_map,
&mut receiver_context,
) {
return Some(resolved);
}
if let Some(resolved) =
resolve_method_in_class_or_bases(receiver, bare_target, class_index, func_index, language)
{
return Some(resolved);
}
if let Some(resolved) =
resolve_local_qualified_receiver(receiver, bare_target, ¤t_module, func_index)
{
return Some(resolved);
}
if let Some(resolved) =
resolve_capitalized_receiver(receiver, bare_target, class_index, func_index, language)
{
return Some(resolved);
}
let type_filter = receiver_type_filter(receiver_type, receiver, class_index);
if let Some(resolved) = resolve_local_fuzzy_match(
bare_target,
type_filter,
func_index,
class_index,
current_file,
) {
return Some(resolved);
}
if let Some(resolved) = resolve_global_fuzzy_match(bare_target, type_filter, func_index, class_index) {
return Some(resolved);
}
resolve_type_aware_fallback(receiver_type, bare_target, func_index, class_index)
}
fn normalize_receiver_target<'a>(target: &'a str, receiver: &str) -> &'a str {
target
.strip_prefix(&format!("{}.", receiver))
.or_else(|| target.strip_prefix(&format!("{}::", receiver)))
.or_else(|| target.strip_prefix(&format!("{}->", receiver)))
.unwrap_or(target)
}
fn resolve_with_receiver_type(
receiver_type: Option<&str>,
bare_target: &str,
class_index: &ClassIndex,
func_index: &FuncIndex,
language: &str,
) -> Option<ResolvedTarget> {
let type_name = receiver_type?;
resolve_method_in_class_or_bases(type_name, bare_target, class_index, func_index, language)
}
fn resolve_self_receiver_in_current_file(
receiver: &str,
bare_target: &str,
current_module: &str,
func_index: &FuncIndex,
class_index: &ClassIndex,
) -> Option<ResolvedTarget> {
if !matches!(receiver, "self" | "cls" | "this" | "Self") {
return None;
}
if let Some(entry) = func_index.get(current_module, bare_target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: true,
class_name: entry.class_name.clone(),
});
}
let class_entry = class_index.get(bare_target)?;
Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(class_entry.line),
is_method: false,
class_name: Some(bare_target.to_string()),
})
}
struct ReceiverLookupContext<'a, 'b> {
func_index: &'a FuncIndex,
class_index: &'a ClassIndex,
reexport_tracer: &'a mut ReExportTracer<'b>,
language: &'a str,
}
fn resolve_module_import_receiver(
target: &str,
receiver: &str,
bare_target: &str,
module_imports: &ModuleImports,
context: &mut ReceiverLookupContext<'_, '_>,
) -> Option<ResolvedTarget> {
let module_path = module_imports.get(receiver)?;
let simple_module = module_path.split('.').next_back().unwrap_or(module_path);
if let Some(entry) = context.func_index.get(module_path, bare_target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if simple_module != module_path.as_str() {
if let Some(entry) = context.func_index.get(simple_module, bare_target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
}
if bare_target != target {
if let Some(entry) = context.func_index.get(module_path, target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
}
resolve_reexported_name(
module_path,
bare_target,
context.reexport_tracer,
context.func_index,
context.class_index,
context.language,
)
}
fn resolve_import_map_receiver(
target: &str,
receiver: &str,
bare_target: &str,
import_map: &ImportMap,
context: &mut ReceiverLookupContext<'_, '_>,
) -> Option<ResolvedTarget> {
let (module_path, original_name) = import_map.get(receiver)?;
if let Some(resolved) = resolve_method_in_class_or_bases(
original_name,
bare_target,
context.class_index,
context.func_index,
context.language,
) {
return Some(resolved);
}
if let Some(entry) = context.func_index.get(module_path, bare_target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
if bare_target != target {
if let Some(entry) = context.func_index.get(module_path, target) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
}
resolve_reexported_receiver_target(
module_path,
original_name,
bare_target,
context.reexport_tracer,
context.func_index,
context.class_index,
context.language,
)
}
fn resolve_method_in_class_or_bases(
class_name: &str,
method_name: &str,
class_index: &ClassIndex,
func_index: &FuncIndex,
language: &str,
) -> Option<ResolvedTarget> {
resolve_method_in_class(class_name, method_name, class_index, func_index, language).or_else(|| {
resolve_method_in_bases(class_name, method_name, class_index, func_index, language)
})
}
fn resolve_local_qualified_receiver(
receiver: &str,
bare_target: &str,
current_module: &str,
func_index: &FuncIndex,
) -> Option<ResolvedTarget> {
let qualified = format!("{}.{}", receiver, bare_target);
let entry = func_index.get(current_module, &qualified)?;
Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
})
}
fn resolve_capitalized_receiver(
receiver: &str,
bare_target: &str,
class_index: &ClassIndex,
func_index: &FuncIndex,
language: &str,
) -> Option<ResolvedTarget> {
let capitalized = capitalize_first(receiver);
if capitalized == receiver {
return None;
}
resolve_method_in_class_or_bases(
&capitalized,
bare_target,
class_index,
func_index,
language,
)
}
fn receiver_type_filter<'a>(
receiver_type: Option<&'a str>,
receiver: &str,
class_index: &ClassIndex,
) -> Option<&'a str> {
receiver_type.filter(|type_name| {
if class_index.get(type_name).is_some() {
return true;
}
if matches!(receiver, "self" | "cls" | "this" | "Self") {
return true;
}
is_stdlib_type(type_name)
})
}
fn resolve_local_fuzzy_match(
bare_target: &str,
type_filter: Option<&str>,
func_index: &FuncIndex,
class_index: &ClassIndex,
current_file: &Path,
) -> Option<ResolvedTarget> {
if type_filter.is_none() && is_builtin_method_name(bare_target) {
return None;
}
let local_matches: Vec<_> = func_index
.iter()
.filter(|((_module, func_name), entry)| {
if *func_name != bare_target || entry.file_path != current_file {
return false;
}
if let Some(type_name) = type_filter {
if let Some(ref candidate_class) = entry.class_name {
return is_in_inheritance_chain(type_name, candidate_class, class_index);
}
}
true
})
.collect();
if local_matches.len() == 1 || (type_filter.is_some() && !local_matches.is_empty()) {
let (_, entry) = local_matches[0];
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: entry.is_method,
class_name: entry.class_name.clone(),
});
}
None
}
fn resolve_global_fuzzy_match(
bare_target: &str,
type_filter: Option<&str>,
func_index: &FuncIndex,
class_index: &ClassIndex,
) -> Option<ResolvedTarget> {
if type_filter.is_none() && is_builtin_method_name(bare_target) {
return None;
}
let mut candidates: Vec<_> = func_index
.find_by_name(bare_target)
.filter(|e| e.is_method)
.collect();
if let Some(type_name) = type_filter {
candidates.retain(|e| match &e.class_name {
Some(c) => is_in_inheritance_chain(type_name, c, class_index),
None => false,
});
}
if candidates.len() != 1 {
return None;
}
let entry = candidates[0];
Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: true,
class_name: entry.class_name.clone(),
})
}
fn resolve_type_aware_fallback(
receiver_type: Option<&str>,
bare_target: &str,
func_index: &FuncIndex,
class_index: &ClassIndex,
) -> Option<ResolvedTarget> {
let type_name = receiver_type?;
if let Some(class_entry) = class_index.get(type_name) {
if class_entry.methods.contains(&bare_target.to_string()) {
return Some(ResolvedTarget {
file: class_entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(class_entry.line),
is_method: true,
class_name: Some(type_name.to_string()),
});
}
for base in &class_entry.bases {
if let Some(base_entry) = class_index.get(base.as_str()) {
if base_entry.methods.contains(&bare_target.to_string()) {
return Some(ResolvedTarget {
file: base_entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(base_entry.line),
is_method: true,
class_name: Some(base.to_string()),
});
}
}
}
}
for ((_module, func_name), entry) in func_index.iter() {
if func_name == bare_target && entry.class_name.as_deref() == Some(type_name) {
return Some(ResolvedTarget {
file: entry.file_path.clone(),
name: bare_target.to_string(),
line: Some(entry.line),
is_method: true,
class_name: Some(type_name.to_string()),
});
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::types::{FuncIndex, ClassIndex, FuncEntry, ClassEntry};
use super::super::imports::{ImportMap, ModuleImports, augment_go_module_imports};
use super::super::module_path::path_to_module;
use crate::callgraph::cross_file_types::{
CallType, ImportDef,
};
use crate::callgraph::import_resolver::ReExportTracer;
use crate::callgraph::module_index::ModuleIndex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
macro_rules! resolve_call {
(
$target:expr,
$call_type:expr,
$import_map:expr,
$module_imports:expr,
$func_index:expr,
$class_index:expr,
$reexport_tracer:expr,
$current_file:expr,
$root:expr,
$language:expr $(,)?
) => {{
let mut context = ResolutionContext {
import_map: $import_map,
module_imports: $module_imports,
func_index: $func_index,
class_index: $class_index,
reexport_tracer: $reexport_tracer,
current_file: $current_file,
root: $root,
language: $language,
};
super::resolve_call($target, $call_type, &mut context)
}};
}
macro_rules! resolve_call_with_receiver {
(
$target:expr,
$receiver:expr,
$receiver_type:expr,
$call_type:expr,
$import_map:expr,
$module_imports:expr,
$func_index:expr,
$class_index:expr,
$reexport_tracer:expr,
$current_file:expr,
$root:expr,
$language:expr $(,)?
) => {{
let mut context = ResolutionContext {
import_map: $import_map,
module_imports: $module_imports,
func_index: $func_index,
class_index: $class_index,
reexport_tracer: $reexport_tracer,
current_file: $current_file,
root: $root,
language: $language,
};
super::resolve_call_with_receiver(
$target,
$receiver,
$receiver_type,
$call_type,
&mut context,
)
}};
}
#[test]
fn test_resolved_target_function() {
let target = ResolvedTarget::function(
PathBuf::from("helper.py"),
"process",
Some(10),
);
assert_eq!(target.file, PathBuf::from("helper.py"));
assert_eq!(target.name, "process");
assert_eq!(target.line, Some(10));
assert!(!target.is_method);
assert!(target.class_name.is_none());
assert_eq!(target.qualified_name(), "process");
}
#[test]
fn test_resolved_target_method() {
let target = ResolvedTarget::method(
PathBuf::from("models.py"),
"save",
"User",
Some(42),
);
assert_eq!(target.file, PathBuf::from("models.py"));
assert_eq!(target.name, "save");
assert_eq!(target.line, Some(42));
assert!(target.is_method);
assert_eq!(target.class_name, Some("User".to_string()));
assert_eq!(target.qualified_name(), "User.save");
}
#[test]
fn test_resolve_call_intra() {
let mut func_index = FuncIndex::new();
func_index.insert(
"main",
"helper",
FuncEntry::function(PathBuf::from("main.py"), 10, 15),
);
let import_map = ImportMap::new();
let module_imports = ModuleImports::new();
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"helper",
&CallType::Intra,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve intra-file call");
let target = resolved.unwrap();
assert_eq!(target.file, PathBuf::from("main.py"));
assert_eq!(target.name, "helper");
assert!(!target.is_method);
}
#[test]
fn test_resolve_call_direct_import() {
let mut func_index = FuncIndex::new();
func_index.insert(
"helper",
"process",
FuncEntry::function(PathBuf::from("helper.py"), 5, 10),
);
let mut import_map = ImportMap::new();
import_map.insert("process".to_string(), ("helper".to_string(), "process".to_string()));
let module_imports = ModuleImports::new();
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"process",
&CallType::Direct,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve direct call via import map");
let target = resolved.unwrap();
assert_eq!(target.file, PathBuf::from("helper.py"));
assert_eq!(target.name, "process");
}
#[test]
fn test_resolve_call_external() {
let func_index = FuncIndex::new();
let import_map = ImportMap::new();
let module_imports = ModuleImports::new();
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"json_loads",
&CallType::Direct,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_none(), "External/stdlib calls should return None");
}
#[test]
fn test_resolve_call_dynamic_import() {
let func_index = FuncIndex::new();
let import_map = ImportMap::new();
let module_imports = ModuleImports::new();
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"__import__",
&CallType::Direct,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_none(), "Dynamic imports should return None");
}
#[test]
fn test_resolve_call_module_func() {
let mut func_index = FuncIndex::new();
func_index.insert(
"json",
"loads",
FuncEntry::function(PathBuf::from("json.py"), 100, 120),
);
let import_map = ImportMap::new();
let mut module_imports = ModuleImports::new();
module_imports.insert("json".to_string(), "json".to_string());
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call_with_receiver!(
"loads",
"json",
None,
&CallType::Attr,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve module.func pattern");
let target = resolved.unwrap();
assert_eq!(target.name, "loads");
}
#[test]
fn test_resolve_call_method_with_type() {
let mut func_index = FuncIndex::new();
func_index.insert(
"models",
"User.save",
FuncEntry::method(PathBuf::from("models.py"), 50, 60, "User".to_string()),
);
let mut class_index = ClassIndex::new();
class_index.insert(
"User",
ClassEntry::new(
PathBuf::from("models.py"),
10,
100,
vec!["save".to_string(), "delete".to_string()],
vec![],
),
);
let import_map = ImportMap::new();
let module_imports = ModuleImports::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call_with_receiver!(
"save",
"user",
Some("User"),
&CallType::Method,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve method with known type");
let target = resolved.unwrap();
assert_eq!(target.name, "save");
assert!(target.is_method);
assert_eq!(target.class_name, Some("User".to_string()));
}
#[test]
fn test_resolve_call_ref() {
let mut func_index = FuncIndex::new();
func_index.insert(
"utils",
"transform",
FuncEntry::function(PathBuf::from("utils.py"), 5, 15),
);
let mut import_map = ImportMap::new();
import_map.insert("transform".to_string(), ("utils".to_string(), "transform".to_string()));
let module_imports = ModuleImports::new();
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"transform",
&CallType::Ref,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve Ref call type");
let target = resolved.unwrap();
assert_eq!(target.name, "transform");
}
#[test]
fn test_resolve_call_static() {
let mut func_index = FuncIndex::new();
func_index.insert(
"models",
"User.create",
FuncEntry::method(PathBuf::from("models.py"), 25, 35, "User".to_string()),
);
let mut class_index = ClassIndex::new();
class_index.insert(
"User",
ClassEntry::new(
PathBuf::from("models.py"),
5,
50,
vec!["create".to_string()],
vec![],
),
);
let import_map = ImportMap::new();
let module_imports = ModuleImports::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"User::create",
&CallType::Static,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.php"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve static call");
let target = resolved.unwrap();
assert_eq!(target.name, "create");
assert!(target.is_method);
assert_eq!(target.class_name, Some("User".to_string()));
}
#[test]
fn test_resolve_call_constructor() {
let mut class_index = ClassIndex::new();
class_index.insert(
"MyClass",
ClassEntry::new(
PathBuf::from("classes.py"),
10,
50,
vec!["__init__".to_string()],
vec![],
),
);
let func_index = FuncIndex::new();
let import_map = ImportMap::new();
let module_imports = ModuleImports::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call!(
"MyClass",
&CallType::Direct,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve constructor call");
let target = resolved.unwrap();
assert_eq!(target.file, PathBuf::from("classes.py"));
assert_eq!(target.name, "__init__");
}
#[test]
fn test_resolve_imported_class_method() {
let mut func_index = FuncIndex::new();
func_index.insert(
"models",
"User.create",
FuncEntry::method(PathBuf::from("models.py"), 30, 40, "User".to_string()),
);
let mut class_index = ClassIndex::new();
class_index.insert(
"User",
ClassEntry::new(
PathBuf::from("models.py"),
10,
50,
vec!["create".to_string()],
vec![],
),
);
let mut import_map = ImportMap::new();
import_map.insert("User".to_string(), ("models".to_string(), "User".to_string()));
let module_imports = ModuleImports::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call_with_receiver!(
"create",
"User",
None,
&CallType::Attr,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("/project"),
"python",
);
assert!(resolved.is_some(), "Should resolve imported class method");
let target = resolved.unwrap();
assert_eq!(target.name, "create");
assert!(target.is_method);
}
#[test]
fn test_resolve_call_typescript_module_keys_match() {
let mut func_index = FuncIndex::new();
let class_index = ClassIndex::new();
let module = path_to_module(Path::new("errors.ts"), "typescript");
func_index.insert(&module, "ZodError", FuncEntry::function(
PathBuf::from("errors.ts"), 10, 20,
));
let mut import_map: ImportMap = HashMap::new();
import_map.insert("ZodError".to_string(), ("./errors".to_string(), "ZodError".to_string()));
let module_imports: ModuleImports = HashMap::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "typescript");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call!(
"ZodError",
&CallType::Direct,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("core.ts"),
Path::new("."),
"typescript",
);
assert!(
result.is_some(),
"resolve_call should find ZodError when func_index key './errors' matches import_map key './errors'"
);
let resolved = result.unwrap();
assert_eq!(resolved.name, "ZodError");
assert_eq!(resolved.file, PathBuf::from("errors.ts"));
}
#[test]
fn test_resolve_call_with_receiver_typescript_module_import() {
let mut func_index = FuncIndex::new();
let class_index = ClassIndex::new();
let module = path_to_module(Path::new("errors.ts"), "typescript");
func_index.insert(&module, "createZodError", FuncEntry::function(
PathBuf::from("errors.ts"), 5, 15,
));
let import_map: ImportMap = HashMap::new();
let mut module_imports: ModuleImports = HashMap::new();
module_imports.insert("errors".to_string(), "./errors".to_string());
let module_index = ModuleIndex::new(PathBuf::from("."), "typescript");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call_with_receiver!(
"createZodError",
"errors",
None,
&CallType::Attr,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("core.ts"),
Path::new("."),
"typescript",
);
assert!(
result.is_some(),
"resolve_call_with_receiver should find createZodError via module import './errors'"
);
let resolved = result.unwrap();
assert_eq!(resolved.name, "createZodError");
}
#[test]
fn test_strategy8_self_receiver_filters_unrelated_class() {
let mut func_index = FuncIndex::new();
let mut class_index = ClassIndex::new();
func_index.insert(
"cookies",
"items",
FuncEntry::method(PathBuf::from("cookies.py"), 80, 90, "RequestsCookieJar".to_string()),
);
class_index.insert(
"RequestsCookieJar",
ClassEntry::new(
PathBuf::from("cookies.py"), 5, 200,
vec!["items".to_string(), "values".to_string()],
vec!["cookielib.CookieJar".to_string()],
),
);
let import_map: ImportMap = HashMap::new();
let module_imports: ModuleImports = HashMap::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call_with_receiver!(
"items",
"self",
Some("CaseInsensitiveDict"),
&CallType::Method,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("structures.py"),
Path::new("."),
"python",
);
if let Some(ref resolved) = result {
assert_ne!(
resolved.class_name.as_deref(),
Some("RequestsCookieJar"),
"self.items() in CaseInsensitiveDict must NOT resolve to RequestsCookieJar.items (false positive)"
);
}
}
#[test]
fn test_strategy7_self_receiver_filters_unrelated_class_same_file() {
let mut func_index = FuncIndex::new();
let mut class_index = ClassIndex::new();
func_index.insert(
"module_b",
"process",
FuncEntry::method(PathBuf::from("module.py"), 30, 40, "ClassB".to_string()),
);
func_index.insert(
"module_a",
"process",
FuncEntry::method(PathBuf::from("module.py"), 10, 20, "ClassA".to_string()),
);
class_index.insert(
"ClassA",
ClassEntry::new(
PathBuf::from("module.py"), 5, 25,
vec!["process".to_string()],
vec![],
),
);
class_index.insert(
"ClassB",
ClassEntry::new(
PathBuf::from("module.py"), 26, 45,
vec!["process".to_string()],
vec![],
),
);
let import_map: ImportMap = HashMap::new();
let module_imports: ModuleImports = HashMap::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call_with_receiver!(
"process",
"self",
Some("ClassA"),
&CallType::Method,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("module.py"),
Path::new("."),
"python",
);
if let Some(ref resolved) = result {
assert_ne!(
resolved.class_name.as_deref(),
Some("ClassB"),
"self.process() in ClassA must NOT resolve to ClassB.process (false positive)"
);
}
}
#[test]
fn test_strategy8_non_self_receiver_still_resolves_unique() {
let mut func_index = FuncIndex::new();
let class_index = ClassIndex::new();
func_index.insert(
"helpers",
"unique_method",
FuncEntry::method(PathBuf::from("helpers.py"), 10, 20, "Helper".to_string()),
);
let import_map: ImportMap = HashMap::new();
let module_imports: ModuleImports = HashMap::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call_with_receiver!(
"unique_method",
"obj",
None,
&CallType::Method,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.py"),
Path::new("."),
"python",
);
assert!(
result.is_some(),
"obj.unique_method() should still resolve via Strategy 8 when unique"
);
let resolved = result.unwrap();
assert_eq!(resolved.name, "unique_method");
}
#[test]
fn test_strategy8_self_receiver_allows_base_class_method() {
let mut func_index = FuncIndex::new();
let mut class_index = ClassIndex::new();
func_index.insert(
"base",
"save",
FuncEntry::method(PathBuf::from("base.py"), 10, 20, "BaseClass".to_string()),
);
class_index.insert(
"ChildClass",
ClassEntry::new(
PathBuf::from("child.py"), 5, 50,
vec!["run".to_string()],
vec!["BaseClass".to_string()],
),
);
class_index.insert(
"BaseClass",
ClassEntry::new(
PathBuf::from("base.py"), 1, 30,
vec!["save".to_string()],
vec![],
),
);
func_index.insert(
"other",
"save",
FuncEntry::method(PathBuf::from("other.py"), 10, 20, "UnrelatedClass".to_string()),
);
class_index.insert(
"UnrelatedClass",
ClassEntry::new(
PathBuf::from("other.py"), 1, 30,
vec!["save".to_string()],
vec![],
),
);
let import_map: ImportMap = HashMap::new();
let module_imports: ModuleImports = HashMap::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call_with_receiver!(
"save",
"self",
Some("ChildClass"),
&CallType::Method,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("child.py"),
Path::new("."),
"python",
);
assert!(
result.is_some(),
"self.save() in ChildClass should resolve to base class BaseClass.save"
);
let resolved = result.unwrap();
assert_eq!(
resolved.class_name.as_deref(),
Some("BaseClass"),
"self.save() should resolve to BaseClass.save (inherited), not {:?}",
resolved.class_name
);
assert_eq!(resolved.file, PathBuf::from("base.py"));
}
#[test]
fn test_strategy8_stdlib_receiver_type_filters_project_methods() {
let mut func_index = FuncIndex::new();
let mut class_index = ClassIndex::new();
func_index.insert(
"cookies",
"items",
FuncEntry::method(PathBuf::from("cookies.py"), 80, 90, "RequestsCookieJar".to_string()),
);
class_index.insert(
"RequestsCookieJar",
ClassEntry::new(
PathBuf::from("cookies.py"), 5, 200,
vec!["items".to_string()],
vec![],
),
);
let import_map: ImportMap = HashMap::new();
let module_imports: ModuleImports = HashMap::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "python");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let result = resolve_call_with_receiver!(
"items",
"_store",
Some("OrderedDict"),
&CallType::Method,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("structures.py"),
Path::new("."),
"python",
);
if let Some(ref resolved) = result {
assert_ne!(
resolved.class_name.as_deref(),
Some("RequestsCookieJar"),
"OrderedDict.items() must NOT resolve to RequestsCookieJar.items (false positive)"
);
}
}
#[test]
fn test_go_cross_package_resolve_end_to_end() {
let mut func_index = FuncIndex::new();
func_index.insert(
"pkg/models",
"NewUser",
FuncEntry::function(PathBuf::from("pkg/models/user.go"), 12, 14),
);
func_index.insert(
"pkg/models",
"NewAdmin",
FuncEntry::function(PathBuf::from("pkg/models/user.go"), 33, 38),
);
func_index.insert(
"pkg/service",
"NewUserService",
FuncEntry::function(PathBuf::from("pkg/service/service.go"), 10, 13),
);
let imports = vec![
ImportDef::simple_import("go-callgraph-test/pkg/models"),
ImportDef::simple_import("go-callgraph-test/pkg/service"),
];
let mut module_imports = ModuleImports::new();
augment_go_module_imports(&imports, &mut module_imports, &func_index);
let import_map = ImportMap::new();
let class_index = ClassIndex::new();
let module_index = ModuleIndex::new(PathBuf::from("."), "go");
let mut reexport_tracer = ReExportTracer::new(&module_index);
let resolved = resolve_call_with_receiver!(
"models.NewUser",
"models",
None,
&CallType::Attr,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.go"),
Path::new("/project"),
"go",
);
assert!(
resolved.is_some(),
"models.NewUser() should resolve via Strategy 2"
);
let target = resolved.unwrap();
assert_eq!(target.name, "NewUser");
assert_eq!(target.file, PathBuf::from("pkg/models/user.go"));
let resolved2 = resolve_call_with_receiver!(
"service.NewUserService",
"service",
None,
&CallType::Attr,
&import_map,
&module_imports,
&func_index,
&class_index,
&mut reexport_tracer,
Path::new("main.go"),
Path::new("/project"),
"go",
);
assert!(
resolved2.is_some(),
"service.NewUserService() should resolve via Strategy 2"
);
let target2 = resolved2.unwrap();
assert_eq!(target2.name, "NewUserService");
assert_eq!(target2.file, PathBuf::from("pkg/service/service.go"));
}
}