use std::collections::{BTreeMap, BTreeSet};
use crate::compat::added_lines::{AddedLinesSubject, file_line};
use crate::compat::qualified_call_resolve::{
ContextAwareSubject, ReferenceCaches, ReferenceContext, ShapeComparison, TreeReader,
compare_return_shapes, resolve_qualified_reference,
};
use crate::compat::source_attributes::{
FunctionSignature, ImportedFunction, SpecTable, declares_parse_transform,
extract_function_signatures, extract_function_signatures_with_context, extract_imports,
extract_specs,
};
use crate::model::names::{Arity, FunctionName};
use crate::model::verdict::{Reason, ShapeCheckTally};
#[derive(Debug, Default)]
pub struct LocalCallAnalysis {
pub reasons: Vec<Reason>,
pub shape_checks: ShapeCheckTally,
}
pub fn analyse_local_calls(
subjects: &[ContextAwareSubject<'_>],
ctx: &ReferenceContext<'_>,
) -> LocalCallAnalysis {
let mut analysis = LocalCallAnalysis::default();
let mut caches = ReferenceCaches::default();
for aware in subjects {
let subject = &aware.subject;
let sigs = extract_function_signatures_with_context(subject.added_text, aware.line_context);
if sigs.iter().all(|s| s.is_definition) {
continue;
}
let Some(target) = (ctx.read_target)(subject.source_path) else {
continue;
};
if declares_parse_transform(&target) {
continue;
}
let patch_defs = defined_set(&sigs);
let patch_specs = extract_specs(subject.added_text);
let target_defined = defined_set(&extract_function_signatures(&target));
let target_imported = imported_keys(&target);
let patch_imports = patch_import_map(subject.added_text);
let mut spec_tables: Option<(SpecTable, Option<SpecTable>)> = None;
let mut flagged = BTreeSet::new();
let mut shape_seen = BTreeSet::new();
caches.begin_subject();
for call in sigs.iter().filter(|s| !s.is_definition) {
if is_auto_imported_bif(call.name.as_str()) {
continue;
}
let Some(key) = signature_key(call) else {
continue;
};
if patch_defs.contains(&key) {
continue;
}
if target_defined.contains(&key) {
if !shape_seen.insert(key.clone()) {
continue;
}
if !patch_specs.contains_key(&key) {
check_local_return_shape(
subject,
&target,
call,
&key,
ctx.read_source,
&mut spec_tables,
&mut analysis,
);
}
continue;
}
if target_imported.contains(&key) {
if shape_seen.insert(key.clone()) {
analysis.shape_checks.withheld_imported += 1;
}
continue;
}
if let Some(import) = patch_imports.get(&key) {
let qkey = (import.function.clone(), import.arity);
resolve_qualified_reference(
subject,
&import.module,
&qkey,
call.line,
ctx,
&mut caches,
&mut analysis.reasons,
&mut analysis.shape_checks,
);
continue;
}
if !flagged.insert(key.clone()) {
continue;
}
analysis.reasons.push(Reason::LocalCallUndefinedOnTarget {
source_path: subject.source_path.clone(),
function: key.0,
arity: key.1,
line: file_line(subject.line_map, call.line),
});
}
}
analysis
}
fn imported_keys(src: &str) -> BTreeSet<(FunctionName, Arity)> {
extract_imports(src)
.into_iter()
.map(|i| (i.function, i.arity))
.collect()
}
fn patch_import_map(src: &str) -> BTreeMap<(FunctionName, Arity), ImportedFunction> {
extract_imports(src)
.into_iter()
.map(|i| ((i.function.clone(), i.arity), i))
.collect()
}
fn check_local_return_shape(
subject: &AddedLinesSubject<'_>,
target_text: &str,
call: &FunctionSignature,
key: &(FunctionName, Arity),
read_source: Option<TreeReader<'_>>,
spec_tables: &mut Option<(SpecTable, Option<SpecTable>)>,
analysis: &mut LocalCallAnalysis,
) {
let tally = &mut analysis.shape_checks;
let Some(read_source) = read_source else {
tally.withheld_no_source += 1;
return;
};
let (target_specs, source_specs) = spec_tables.get_or_insert_with(|| {
(
extract_specs(target_text),
read_source(subject.source_path).map(|t| extract_specs(&t)),
)
});
let Some(source_specs) = source_specs.as_ref() else {
tally.withheld_no_source += 1;
return;
};
match compare_return_shapes(target_specs, source_specs, key) {
ShapeComparison::Same => tally.compared += 1,
ShapeComparison::NoSpec => tally.withheld_no_spec += 1,
ShapeComparison::UnknownType => tally.withheld_unknown_type += 1,
ShapeComparison::Drift {
source_signature,
target_signature,
} => {
tally.compared += 1;
analysis.reasons.push(Reason::LocalCallReturnShapeDrift {
source_path: subject.source_path.clone(),
function: key.0.clone(),
arity: key.1,
source_signature,
target_signature,
line: file_line(subject.line_map, call.line),
});
}
}
}
fn defined_set(sigs: &[FunctionSignature]) -> BTreeSet<(FunctionName, Arity)> {
sigs.iter()
.filter(|s| s.is_definition)
.filter_map(signature_key)
.collect()
}
fn signature_key(sig: &FunctionSignature) -> Option<(FunctionName, Arity)> {
Some((sig.name.clone(), Arity::try_from(sig.arity).ok()?))
}
fn is_auto_imported_bif(name: &str) -> bool {
matches!(
name,
"abs"
| "apply"
| "atom_to_binary"
| "atom_to_list"
| "binary_part"
| "binary_to_atom"
| "binary_to_existing_atom"
| "binary_to_float"
| "binary_to_integer"
| "binary_to_list"
| "binary_to_term"
| "bit_size"
| "bitstring_to_list"
| "byte_size"
| "ceil"
| "date"
| "element"
| "erase"
| "error"
| "exit"
| "float"
| "float_to_binary"
| "float_to_list"
| "floor"
| "garbage_collect"
| "get"
| "get_keys"
| "group_leader"
| "halt"
| "hd"
| "integer_to_binary"
| "integer_to_list"
| "iolist_size"
| "iolist_to_binary"
| "is_alive"
| "is_atom"
| "is_binary"
| "is_bitstring"
| "is_boolean"
| "is_float"
| "is_function"
| "is_integer"
| "is_list"
| "is_map"
| "is_map_key"
| "is_number"
| "is_pid"
| "is_port"
| "is_process_alive"
| "is_record"
| "is_reference"
| "is_tuple"
| "length"
| "link"
| "list_to_atom"
| "list_to_binary"
| "list_to_bitstring"
| "list_to_existing_atom"
| "list_to_float"
| "list_to_integer"
| "list_to_pid"
| "list_to_tuple"
| "make_ref"
| "map_get"
| "map_size"
| "max"
| "min"
| "monitor"
| "node"
| "nodes"
| "now"
| "open_port"
| "pid_to_list"
| "process_flag"
| "process_info"
| "processes"
| "put"
| "register"
| "registered"
| "round"
| "self"
| "setelement"
| "size"
| "spawn"
| "spawn_link"
| "spawn_monitor"
| "spawn_opt"
| "split_binary"
| "statistics"
| "term_to_binary"
| "throw"
| "time"
| "tl"
| "trunc"
| "tuple_size"
| "tuple_to_list"
| "unlink"
| "unregister"
| "whereis"
)
}