use crate::models::{FileInfo, FunctionInfo, TypeRelationship};
use crate::parsers::registry;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
pub struct CallEdge {
pub caller: String,
pub callee: String,
pub call_lines: String,
pub call_count: i64,
pub raw_targets: Option<String>,
pub offsets: Option<String>,
pub via: Option<String>,
pub address_lines: Option<String>,
pub resolution: Option<String>,
pub candidates: Option<i64>,
pub import_backed: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Resolution {
ExactQualified,
Receiver,
Inherited,
SameOwner,
NamespaceImport,
SameFile,
UniqueName,
LangGroup,
GlobalFallback,
}
impl Resolution {
pub fn as_str(self) -> &'static str {
match self {
Resolution::ExactQualified => "exact_qualified",
Resolution::Receiver => "receiver",
Resolution::Inherited => "inherited",
Resolution::SameOwner => "same_owner",
Resolution::NamespaceImport => "namespace_import",
Resolution::SameFile => "same_file",
Resolution::UniqueName => "unique_name",
Resolution::LangGroup => "lang_group",
Resolution::GlobalFallback => "global_fallback",
}
}
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
pub struct CallResolutionStats {
pub total_calls: u64,
pub excluded_noise: u64,
pub no_candidate: u64,
pub ambiguous_dropped: u64,
pub resolved_call_sites: u64,
pub resolved_edges: u64,
pub resolved_via_inheritance: u64,
}
impl CallResolutionStats {
pub(crate) fn merge(&mut self, other: Self) {
self.total_calls += other.total_calls;
self.excluded_noise += other.excluded_noise;
self.no_candidate += other.no_candidate;
self.ambiguous_dropped += other.ambiguous_dropped;
self.resolved_call_sites += other.resolved_call_sites;
self.resolved_edges += other.resolved_edges;
self.resolved_via_inheritance += other.resolved_via_inheritance;
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Counts {
total: u64,
excluded: u64,
no_candidate: u64,
ambiguous: u64,
resolved: u64,
inherited: u64,
}
pub(crate) fn noise_names_for_files(files: &[FileInfo]) -> HashSet<&'static str> {
let present_languages: HashSet<&str> =
files.iter().map(|file| file.language.as_str()).collect();
registry::LANGUAGES
.iter()
.filter(|language| present_languages.contains(language.id))
.flat_map(|language| language.noise_names.iter().copied())
.collect()
}
fn short_type_name(name: &str) -> &str {
let mut cut = 0usize;
for sep in ["::", ".", "/"] {
if let Some(i) = name.rfind(sep) {
let after = i + sep.len();
if after > cut {
cut = after;
}
}
}
&name[cut..]
}
fn build_ancestor_map(rels: &[TypeRelationship]) -> HashMap<&str, HashSet<&str>> {
let mut parents: HashMap<&str, Vec<&str>> = HashMap::new();
for tr in rels {
if tr.relationship == "extends" || tr.relationship == "implements" {
if let Some(tgt) = tr.target_type.as_deref() {
parents
.entry(short_type_name(&tr.source_type))
.or_default()
.push(short_type_name(tgt));
}
}
}
let mut out: HashMap<&str, HashSet<&str>> = HashMap::with_capacity(parents.len());
for &child in parents.keys() {
let mut seen: HashSet<&str> = HashSet::new();
let mut stack: Vec<&str> = parents.get(child).cloned().unwrap_or_default();
while let Some(p) = stack.pop() {
if p != child && seen.insert(p) {
if let Some(gp) = parents.get(p) {
stack.extend(gp.iter().copied());
}
}
}
out.insert(child, seen);
}
out
}
type FnMatchResult<'a> = (Vec<(&'a str, &'a str, u32, Resolution, usize)>, Counts);
fn qname_starts_with_any(qname: &str, scopes: &[String]) -> bool {
for scope in scopes {
if scope.is_empty() {
continue;
}
if qname.len() > scope.len()
&& qname.starts_with(scope.as_str())
&& (qname.as_bytes()[scope.len()] == b'.'
|| (qname.len() > scope.len() + 1 && &qname[scope.len()..scope.len() + 2] == "::"))
{
return true;
}
}
false
}
fn infer_lang_group(qname: &str) -> &'static str {
if qname.contains("::") {
"rust_cpp"
} else if qname.contains('/') {
"go_ts_js"
} else {
"python_java"
}
}
pub fn build_call_edges(
functions: &[FunctionInfo],
files: &[FileInfo],
excluded_names: &std::collections::HashSet<&str>,
max_targets: usize,
type_relationships: &[TypeRelationship],
file_import_pairs: &HashSet<(&str, &str)>,
) -> (Vec<CallEdge>, CallResolutionStats) {
let verbose = std::env::var_os("KGLITE_CODE_TREE_VERBOSE").is_some();
let t0 = std::time::Instant::now();
let mut name_lookup: HashMap<&str, Vec<&str>> = HashMap::new();
for fn_info in functions {
name_lookup
.entry(fn_info.name.as_str())
.or_default()
.push(fn_info.qualified_name.as_str());
}
let mut qualified_lookup: HashMap<&str, Vec<&str>> = HashMap::new();
for function in functions {
let qualified_name = function.qualified_name.as_str();
qualified_lookup
.entry(qualified_name)
.or_default()
.push(qualified_name);
if let Some(base) = super::overload_base_qualified_name(qualified_name) {
qualified_lookup
.entry(base)
.or_default()
.push(qualified_name);
}
}
let mut qname_to_owner: HashMap<&str, &str> = HashMap::new();
let mut qname_to_prefix: HashMap<&str, &str> = HashMap::new();
for fn_info in functions {
let qn = fn_info.qualified_name.as_str();
for sep in ["::", ".", "/"] {
if let Some(idx) = qn.rfind(sep) {
let owner_path = &qn[..idx];
qname_to_prefix.insert(qn, owner_path);
let mut short = owner_path;
for sep2 in ["::", ".", "/"] {
if let Some(i2) = owner_path.rfind(sep2) {
short = &owner_path[i2 + sep2.len()..];
break;
}
}
qname_to_owner.insert(qn, short);
break;
}
}
}
let qname_to_file: HashMap<&str, &str> = functions
.iter()
.map(|f| (f.qualified_name.as_str(), f.file_path.as_str()))
.collect();
let file_imports: HashMap<&str, &Vec<String>> = files
.iter()
.filter(|f| !f.imports.is_empty())
.map(|f| (f.path.as_str(), &f.imports))
.collect();
let ancestors = build_ancestor_map(type_relationships);
if verbose {
eprintln!(
"[calls] lookup build: {:.3}s",
t0.elapsed().as_secs_f64()
);
}
let t_match = std::time::Instant::now();
let per_fn: Vec<FnMatchResult> = functions
.par_iter()
.map(|fn_info| {
let caller_qn = fn_info.qualified_name.as_str();
let caller_lang = infer_lang_group(caller_qn);
let caller_prefix = qname_to_prefix.get(caller_qn).copied();
let caller_owner = qname_to_owner.get(caller_qn).copied();
let caller_file = fn_info.file_path.as_str();
let mut out: Vec<(&str, &str, u32, Resolution, usize)> = Vec::new();
let mut counts = Counts::default();
for (called_name, line) in &fn_info.calls {
counts.total += 1;
let noise_name = called_name.rsplit('.').next().unwrap_or(called_name);
if excluded_names.contains(noise_name) {
counts.excluded += 1;
continue;
}
if let Some(targets) = qualified_lookup.get(called_name.as_str()) {
counts.resolved += 1;
for &target in targets {
if target != caller_qn {
out.push((
caller_qn,
target,
*line,
Resolution::ExactQualified,
targets.len(),
));
}
}
continue;
}
let (explicit_hint, method_name) = match called_name.rfind('.') {
Some(idx) => (Some(&called_name[..idx]), &called_name[idx + 1..]),
None => (None, called_name.as_str()),
};
let Some(candidates) = name_lookup.get(method_name) else {
counts.no_candidate += 1;
continue;
};
if candidates.len() == 1 {
counts.resolved += 1;
let target = candidates[0];
if target != caller_qn {
out.push((caller_qn, target, *line, Resolution::UniqueName, 1));
}
continue;
}
let mut targets: &[&str] = candidates.as_slice();
let mut filtered: Vec<&str>;
let implicit_hint = if explicit_hint.is_none() {
caller_owner
} else {
None
};
let mut owner_hint_hit = false;
let mut tier = Resolution::GlobalFallback;
if let Some(hint) = explicit_hint.or(implicit_hint) {
filtered = targets
.iter()
.copied()
.filter(|t| qname_to_owner.get(t).copied() == Some(hint))
.collect();
if !filtered.is_empty() {
targets = &filtered[..];
owner_hint_hit = true;
tier = Resolution::Receiver;
}
}
if !owner_hint_hit && targets.len() > 1 {
if let Some(owner) = implicit_hint {
if let Some(anc) = ancestors.get(owner) {
let inh: Vec<&str> = candidates
.iter()
.copied()
.filter(|t| {
qname_to_owner
.get(t)
.copied()
.is_some_and(|o| anc.contains(o))
})
.collect();
if inh.len() == 1 {
counts.resolved += 1;
counts.inherited += 1;
if inh[0] != caller_qn {
out.push((caller_qn, inh[0], *line, Resolution::Inherited, 1));
}
continue;
} else if !inh.is_empty() {
filtered = inh;
targets = &filtered[..];
tier = Resolution::Inherited;
}
}
}
}
if targets.len() > 1 {
if let Some(prefix) = caller_prefix {
let narrowed: Vec<&str> = targets
.iter()
.copied()
.filter(|t| qname_to_prefix.get(t).copied() == Some(prefix))
.collect();
if !narrowed.is_empty() {
filtered = narrowed;
targets = &filtered[..];
tier = Resolution::SameOwner;
}
}
}
if targets.len() > 1 {
if let Some(imports) = file_imports.get(caller_file) {
let narrowed: Vec<&str> = targets
.iter()
.copied()
.filter(|t| qname_starts_with_any(t, imports))
.collect();
if !narrowed.is_empty() {
filtered = narrowed;
targets = &filtered[..];
tier = Resolution::NamespaceImport;
}
}
}
if targets.len() > 1 {
let narrowed: Vec<&str> = targets
.iter()
.copied()
.filter(|t| qname_to_file.get(t).copied() == Some(caller_file))
.collect();
if !narrowed.is_empty() {
filtered = narrowed;
targets = &filtered[..];
tier = Resolution::SameFile;
}
}
if targets.len() > 1 {
let narrowed: Vec<&str> = targets
.iter()
.copied()
.filter(|t| infer_lang_group(t) == caller_lang)
.collect();
if !narrowed.is_empty() {
filtered = narrowed;
targets = &filtered[..];
tier = Resolution::LangGroup;
}
}
if targets.len() > max_targets {
counts.ambiguous += 1;
continue;
}
counts.resolved += 1;
for &target in targets {
if target != caller_qn {
out.push((caller_qn, target, *line, tier, targets.len()));
}
}
}
(out, counts)
})
.collect();
let mut stats = CallResolutionStats::default();
for (_, c) in &per_fn {
stats.total_calls += c.total;
stats.excluded_noise += c.excluded;
stats.no_candidate += c.no_candidate;
stats.ambiguous_dropped += c.ambiguous;
stats.resolved_call_sites += c.resolved;
stats.resolved_via_inheritance += c.inherited;
}
let total: usize = per_fn.iter().map(|(v, _)| v.len()).sum();
let mut seen: HashMap<(&str, &str), (Vec<u32>, Resolution, usize)> =
HashMap::with_capacity(total);
for (edges, _) in per_fn {
for (caller, callee, line, tier, candidates) in edges {
let entry = seen
.entry((caller, callee))
.or_insert_with(|| (Vec::new(), tier, candidates));
entry.0.push(line);
entry.1 = entry.1.min(tier);
entry.2 = entry.2.min(candidates);
}
}
if verbose {
eprintln!(
"[calls] match loop: {:.3}s ({} entries)",
t_match.elapsed().as_secs_f64(),
seen.len()
);
}
let t_out = std::time::Instant::now();
let mut keys: Vec<(&str, &str)> = seen.keys().copied().collect();
keys.sort_unstable();
let result: Vec<CallEdge> = keys
.into_iter()
.map(|(caller, callee)| {
let (mut lines, tier, candidates) = seen.remove(&(caller, callee)).unwrap_or((
Vec::new(),
Resolution::GlobalFallback,
0,
));
lines.sort_unstable();
lines.dedup();
let count = lines.len() as i64;
let mut call_lines = String::with_capacity(lines.len() * 4);
for (i, l) in lines.iter().enumerate() {
if i > 0 {
call_lines.push(',');
}
use std::fmt::Write;
let _ = write!(call_lines, "{}", l);
}
let caller_file = qname_to_file.get(caller).copied();
let callee_file = qname_to_file.get(callee).copied();
let import_backed = match (caller_file, callee_file) {
(Some(from), Some(to)) => from == to || file_import_pairs.contains(&(from, to)),
_ => false,
};
CallEdge {
caller: caller.to_string(),
callee: callee.to_string(),
call_lines,
call_count: count,
raw_targets: None,
offsets: None,
via: None,
address_lines: None,
resolution: Some(tier.as_str().to_string()),
candidates: Some(candidates as i64),
import_backed: Some(import_backed),
}
})
.collect();
stats.resolved_edges = result.len() as u64;
if verbose {
eprintln!(
"[calls] output build: {:.3}s ({} edges, {}/{} call sites resolved)",
t_out.elapsed().as_secs_f64(),
stats.resolved_edges,
stats.resolved_call_sites,
stats.total_calls.saturating_sub(stats.excluded_noise),
);
}
(result, stats)
}
#[cfg(test)]
mod stats_tests {
use super::*;
use crate::models::{FileInfo, FunctionInfo, TypeRelationship};
fn no_imports() -> HashSet<(&'static str, &'static str)> {
HashSet::new()
}
fn func(qn: &str, file: &str, calls: &[(&str, u32)]) -> FunctionInfo {
FunctionInfo {
name: qn.rsplit(['.', ':']).next().unwrap_or(qn).to_string(),
qualified_name: qn.to_string(),
file_path: file.to_string(),
calls: calls.iter().map(|(n, l)| (n.to_string(), *l)).collect(),
..Default::default()
}
}
fn meta(edges: &[CallEdge]) -> Vec<(&str, &str, i64, bool)> {
let mut rows: Vec<_> = edges
.iter()
.map(|e| {
(
e.callee.as_str(),
e.resolution.as_deref().expect("resolution set"),
e.candidates.expect("candidates set"),
e.import_backed.expect("import_backed set"),
)
})
.collect();
rows.sort();
rows
}
#[test]
fn unique_name_tier_is_labelled_and_counted() {
let functions = vec![
func("a.foo", "a.py", &[("bar", 1)]),
func("a.bar", "a.py", &[]),
];
let files = vec![FileInfo {
path: "a.py".into(),
..Default::default()
}];
let (edges, _) = build_call_edges(
&functions,
&files,
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
assert_eq!(meta(&edges), vec![("a.bar", "unique_name", 1, true)]);
}
#[test]
fn exact_qualified_tier_is_labelled() {
let caller = func("m.START", "m.agc", &[("m.P61", 7)]);
let mut target = func("m.P61", "other.agc", &[]);
target.name = "P61".into();
let (edges, _) = build_call_edges(
&[caller, target],
&[],
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
assert_eq!(meta(&edges), vec![("m.P61", "exact_qualified", 1, false)]);
}
#[test]
fn receiver_tier_is_labelled() {
let functions = vec![
func("m.Cfg.read", "a.py", &[]),
func("m.Db.read", "b.py", &[]),
func("m.caller", "c.py", &[("Cfg.read", 3)]),
];
let (edges, _) = build_call_edges(
&functions,
&[],
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
assert_eq!(meta(&edges), vec![("m.Cfg.read", "receiver", 1, false)]);
}
#[test]
fn inherited_tier_is_labelled() {
let files = vec![FileInfo {
path: "a.py".into(),
..Default::default()
}];
let functions = vec![
func("mod.Base.m", "a.py", &[]),
func("mod.Other.m", "a.py", &[]),
func("mod.Derived.caller", "a.py", &[("m", 1)]),
];
let rels = vec![TypeRelationship {
source_type: "mod.Derived".into(),
target_type: Some("mod.Base".into()),
relationship: "extends".into(),
methods: vec![],
}];
let (edges, _) = build_call_edges(
&functions,
&files,
&std::collections::HashSet::new(),
5,
&rels,
&no_imports(),
);
assert_eq!(meta(&edges), vec![("mod.Base.m", "inherited", 1, true)]);
}
#[test]
fn namespace_import_tier_is_labelled() {
let functions = vec![
func("xunit.Assert.True", "x.cs", &[]),
func("fluent.Assert.True", "f.cs", &[]),
func("app.caller", "app.cs", &[("True", 4)]),
];
let files = vec![FileInfo {
path: "app.cs".into(),
imports: vec!["xunit".into()],
..Default::default()
}];
let (edges, _) = build_call_edges(
&functions,
&files,
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
assert_eq!(
meta(&edges),
vec![("xunit.Assert.True", "namespace_import", 1, false)]
);
}
#[test]
fn fan_out_records_the_surviving_candidate_count_on_every_edge() {
let functions = vec![
func("one/a.fetch", "one/a.ts", &[]),
func("two/b.fetch", "two/b.ts", &[]),
func("three/c.caller", "three/c.ts", &[("fetch", 9)]),
];
let (edges, _) = build_call_edges(
&functions,
&[],
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
assert_eq!(
meta(&edges),
vec![
("one/a.fetch", "lang_group", 2, false),
("two/b.fetch", "lang_group", 2, false),
]
);
}
#[test]
fn import_backed_follows_the_resolved_file_import_set() {
let functions = vec![
func("a.helper", "a.ts", &[]),
func("b.caller", "b.ts", &[("helper", 1)]),
func("c.caller", "c.ts", &[("helper", 1)]),
];
let imports: HashSet<(&str, &str)> = HashSet::from([("b.ts", "a.ts")]);
let (edges, _) = build_call_edges(
&functions,
&[],
&std::collections::HashSet::new(),
5,
&[],
&imports,
);
let backed: Vec<(&str, bool)> = {
let mut v: Vec<_> = edges
.iter()
.map(|e| (e.caller.as_str(), e.import_backed.expect("set")))
.collect();
v.sort();
v
};
assert_eq!(backed, vec![("b.caller", true), ("c.caller", false)]);
}
#[test]
fn import_backed_is_one_hop_and_does_not_follow_re_exports() {
let functions = vec![
func("a.helper", "a.ts", &[]),
func("c.caller", "c.ts", &[("helper", 1)]),
];
let imports: HashSet<(&str, &str)> = HashSet::from([("c.ts", "b.ts"), ("b.ts", "a.ts")]);
let (edges, _) = build_call_edges(
&functions,
&[],
&std::collections::HashSet::new(),
5,
&[],
&imports,
);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].import_backed, Some(false));
}
#[test]
fn aggregation_keeps_the_best_tier_and_the_smallest_candidate_count() {
let functions = vec![
func("m.Cfg.read", "a.py", &[]),
func("m.Db.read", "b.py", &[]),
func("m.caller", "c.py", &[("m.Cfg.read", 1), ("read", 2)]),
];
let (edges, _) = build_call_edges(
&functions,
&[],
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
let cfg = edges
.iter()
.find(|e| e.callee == "m.Cfg.read")
.expect("edge to m.Cfg.read");
assert_eq!(cfg.resolution.as_deref(), Some("exact_qualified"));
assert_eq!(cfg.candidates, Some(1));
assert_eq!(cfg.call_lines, "1,2");
}
#[test]
fn stats_classify_every_call_site() {
let functions = vec![
func(
"a.foo",
"a.py",
&[("bar", 1), ("bar", 2), ("external_thing", 3), ("noisy", 4)],
),
func("a.bar", "a.py", &[]),
];
let files = vec![FileInfo {
path: "a.py".into(),
..Default::default()
}];
let mut noise = std::collections::HashSet::new();
noise.insert("noisy");
let (edges, stats) = build_call_edges(&functions, &files, &noise, 5, &[], &no_imports());
assert_eq!(stats.total_calls, 4);
assert_eq!(stats.excluded_noise, 1);
assert_eq!(stats.no_candidate, 1);
assert_eq!(stats.ambiguous_dropped, 0);
assert_eq!(stats.resolved_call_sites, 2); assert_eq!(stats.resolved_edges, 1); assert_eq!(edges.len(), 1);
}
#[test]
fn exact_qualified_name_precedes_receiver_dot_parsing() {
let caller = func(
"Comanche055.START",
"Comanche055/MAIN.agc",
&[("Comanche055.P61.1", 7)],
);
let mut target = func("Comanche055.P61.1", "Comanche055/P61-P67.agc", &[]);
target.name = "P61.1".into();
let functions = vec![caller, target];
let files = vec![FileInfo {
path: "Comanche055/MAIN.agc".into(),
language: "agc".into(),
..Default::default()
}];
let (edges, stats) = build_call_edges(
&functions,
&files,
&std::collections::HashSet::new(),
5,
&[],
&no_imports(),
);
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.resolved_call_sites, 1);
assert_eq!(stats.no_candidate, 0);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].caller, "Comanche055.START");
assert_eq!(edges[0].callee, "Comanche055.P61.1");
assert_eq!(edges[0].call_lines, "7");
}
#[test]
fn exact_qualified_name_does_not_bypass_noise_filtering() {
let caller = func(
"Comanche055.START",
"Comanche055/MAIN.agc",
&[("Comanche055.WAITLIST", 8)],
);
let target = func("Comanche055.WAITLIST", "Comanche055/WAITLIST.agc", &[]);
let functions = vec![caller, target];
let mut noise = std::collections::HashSet::new();
noise.insert("WAITLIST");
let (edges, stats) = build_call_edges(&functions, &[], &noise, 5, &[], &no_imports());
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.excluded_noise, 1);
assert_eq!(stats.resolved_call_sites, 0);
assert!(edges.is_empty());
}
#[test]
fn inheritance_tier_resolves_self_call_to_ancestor_method() {
let files = vec![FileInfo {
path: "a.py".into(),
..Default::default()
}];
let functions = vec![
func("mod.Base.m", "a.py", &[]),
func("mod.Other.m", "a.py", &[]),
func("mod.Derived.caller", "a.py", &[("m", 1)]),
];
let rels = vec![TypeRelationship {
source_type: "mod.Derived".into(),
target_type: Some("mod.Base".into()),
relationship: "extends".into(),
methods: vec![],
}];
let noise = std::collections::HashSet::new();
let (edges, stats) = build_call_edges(&functions, &files, &noise, 5, &rels, &no_imports());
assert_eq!(stats.resolved_via_inheritance, 1);
let pairs: Vec<(&str, &str)> = edges
.iter()
.map(|e| (e.caller.as_str(), e.callee.as_str()))
.collect();
assert!(pairs.contains(&("mod.Derived.caller", "mod.Base.m")));
assert!(!pairs.iter().any(|(_, callee)| *callee == "mod.Other.m"));
}
#[test]
fn no_type_relationships_means_no_inheritance_resolution() {
let files = vec![FileInfo {
path: "a.py".into(),
..Default::default()
}];
let functions = vec![
func("mod.Base.m", "a.py", &[]),
func("mod.Other.m", "a.py", &[]),
func("mod.Derived.caller", "a.py", &[("m", 1)]),
];
let noise = std::collections::HashSet::new();
let (_edges, stats) = build_call_edges(&functions, &files, &noise, 5, &[], &no_imports());
assert_eq!(stats.resolved_via_inheritance, 0);
}
#[test]
fn foreign_noise_does_not_hide_python_calls() {
let functions = vec![
func("mod.caller", "main.py", &[("find", 3)]),
func("mod.find", "main.py", &[]),
];
let python_files = vec![FileInfo {
path: "main.py".into(),
language: "python".into(),
..Default::default()
}];
let python_noise = noise_names_for_files(&python_files);
let (edges, stats) = build_call_edges(
&functions,
&python_files,
&python_noise,
5,
&[],
&no_imports(),
);
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.excluded_noise, 0);
assert_eq!(stats.resolved_call_sites, 1);
assert_eq!(stats.resolved_edges, 1);
assert_eq!(edges.len(), 1);
let mut polyglot_files = python_files;
polyglot_files.push(FileInfo {
path: "native.cpp".into(),
language: "cpp".into(),
..Default::default()
});
let polyglot_noise = noise_names_for_files(&polyglot_files);
let (edges, stats) = build_call_edges(
&functions,
&polyglot_files,
&polyglot_noise,
5,
&[],
&no_imports(),
);
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.excluded_noise, 1);
assert_eq!(stats.resolved_call_sites, 0);
assert_eq!(stats.resolved_edges, 0);
assert!(edges.is_empty());
}
}