backhopper_core/compat/
local_call_resolve.rs1use std::collections::{BTreeMap, BTreeSet};
28use std::str::FromStr;
29
30use crate::compat::added_lines::{AddedLinesSubject, file_line};
31use crate::compat::qualified_call_resolve::{
32 ReferenceCaches, ReferenceContext, ShapeComparison, TreeReader, compare_return_shapes,
33 resolve_qualified_reference,
34};
35use crate::compat::source_attributes::{
36 FunctionSignature, ImportedFunction, SpecTable, declares_parse_transform,
37 extract_function_signatures, extract_imports, extract_specs,
38};
39use crate::model::names::{Arity, FunctionName};
40use crate::model::verdict::{Reason, ShapeCheckTally};
41
42#[derive(Debug, Default)]
45pub struct LocalCallAnalysis {
46 pub reasons: Vec<Reason>,
47 pub shape_checks: ShapeCheckTally,
48}
49
50pub fn analyse_local_calls(
56 subjects: &[AddedLinesSubject<'_>],
57 ctx: &ReferenceContext<'_>,
58) -> LocalCallAnalysis {
59 let mut analysis = LocalCallAnalysis::default();
60 let mut caches = ReferenceCaches::default();
61 for subject in subjects {
62 let sigs = extract_function_signatures(subject.added_text);
63 if sigs.iter().all(|s| s.is_definition) {
64 continue;
65 }
66 let Some(target) = (ctx.read_target)(subject.source_path) else {
67 continue;
68 };
69 if declares_parse_transform(&target) {
71 continue;
72 }
73 let patch_defs = defined_set(&sigs);
74 let patch_specs = extract_specs(subject.added_text);
77 let target_defined = defined_set(&extract_function_signatures(&target));
78 let target_imported = imported_keys(&target);
79 let patch_imports = patch_import_map(subject.added_text);
80 let mut spec_tables: Option<(SpecTable, Option<SpecTable>)> = None;
83 let mut flagged = BTreeSet::new();
84 let mut shape_seen = BTreeSet::new();
85 caches.begin_subject();
86 for call in sigs.iter().filter(|s| !s.is_definition) {
87 if is_auto_imported_bif(&call.name) {
88 continue;
89 }
90 let Some(key) = signature_key(call) else {
91 continue;
92 };
93 if patch_defs.contains(&key) {
94 continue;
95 }
96 if target_defined.contains(&key) {
97 if !shape_seen.insert(key.clone()) {
98 continue;
99 }
100 if !patch_specs.contains_key(&key) {
103 check_local_return_shape(
104 subject,
105 &target,
106 call,
107 &key,
108 ctx.read_source,
109 &mut spec_tables,
110 &mut analysis,
111 );
112 }
113 continue;
114 }
115 if target_imported.contains(&key) {
116 if shape_seen.insert(key.clone()) {
117 analysis.shape_checks.withheld_imported += 1;
118 }
119 continue;
120 }
121 if let Some(import) = patch_imports.get(&key) {
125 let qkey = (import.function.clone(), import.arity);
126 resolve_qualified_reference(
127 subject,
128 &import.module,
129 &qkey,
130 call.line,
131 ctx,
132 &mut caches,
133 &mut analysis.reasons,
134 &mut analysis.shape_checks,
135 );
136 continue;
137 }
138 if !flagged.insert(key.clone()) {
139 continue;
140 }
141 analysis.reasons.push(Reason::LocalCallUndefinedOnTarget {
142 source_path: subject.source_path.clone(),
143 function: key.0,
144 arity: key.1,
145 line: file_line(subject.line_map, call.line),
146 });
147 }
148 }
149 analysis
150}
151
152fn imported_keys(src: &str) -> BTreeSet<(FunctionName, Arity)> {
155 extract_imports(src)
156 .into_iter()
157 .map(|i| (i.function, i.arity))
158 .collect()
159}
160
161fn patch_import_map(src: &str) -> BTreeMap<(FunctionName, Arity), ImportedFunction> {
164 extract_imports(src)
165 .into_iter()
166 .map(|i| ((i.function.clone(), i.arity), i))
167 .collect()
168}
169
170fn check_local_return_shape(
171 subject: &AddedLinesSubject<'_>,
172 target_text: &str,
173 call: &FunctionSignature,
174 key: &(FunctionName, Arity),
175 read_source: Option<TreeReader<'_>>,
176 spec_tables: &mut Option<(SpecTable, Option<SpecTable>)>,
177 analysis: &mut LocalCallAnalysis,
178) {
179 let tally = &mut analysis.shape_checks;
180 let Some(read_source) = read_source else {
181 tally.withheld_no_source += 1;
182 return;
183 };
184 let (target_specs, source_specs) = spec_tables.get_or_insert_with(|| {
185 (
186 extract_specs(target_text),
187 read_source(subject.source_path).map(|t| extract_specs(&t)),
188 )
189 });
190 let Some(source_specs) = source_specs.as_ref() else {
191 tally.withheld_no_source += 1;
192 return;
193 };
194 match compare_return_shapes(target_specs, source_specs, key) {
195 ShapeComparison::Same => tally.compared += 1,
196 ShapeComparison::NoSpec => tally.withheld_no_spec += 1,
197 ShapeComparison::UnknownType => tally.withheld_unknown_type += 1,
198 ShapeComparison::Drift {
199 source_signature,
200 target_signature,
201 } => {
202 tally.compared += 1;
203 analysis.reasons.push(Reason::LocalCallReturnShapeDrift {
204 source_path: subject.source_path.clone(),
205 function: key.0.clone(),
206 arity: key.1,
207 source_signature,
208 target_signature,
209 line: file_line(subject.line_map, call.line),
210 });
211 }
212 }
213}
214
215fn defined_set(sigs: &[FunctionSignature]) -> BTreeSet<(FunctionName, Arity)> {
216 sigs.iter()
217 .filter(|s| s.is_definition)
218 .filter_map(signature_key)
219 .collect()
220}
221
222fn signature_key(sig: &FunctionSignature) -> Option<(FunctionName, Arity)> {
226 Some((
227 FunctionName::from_str(&sig.name).ok()?,
228 Arity::try_from(sig.arity).ok()?,
229 ))
230}
231
232fn is_auto_imported_bif(name: &str) -> bool {
236 matches!(
237 name,
238 "abs"
239 | "apply"
240 | "atom_to_binary"
241 | "atom_to_list"
242 | "binary_part"
243 | "binary_to_atom"
244 | "binary_to_existing_atom"
245 | "binary_to_float"
246 | "binary_to_integer"
247 | "binary_to_list"
248 | "binary_to_term"
249 | "bit_size"
250 | "bitstring_to_list"
251 | "byte_size"
252 | "ceil"
253 | "date"
254 | "element"
255 | "erase"
256 | "error"
257 | "exit"
258 | "float"
259 | "float_to_binary"
260 | "float_to_list"
261 | "floor"
262 | "garbage_collect"
263 | "get"
264 | "get_keys"
265 | "group_leader"
266 | "halt"
267 | "hd"
268 | "integer_to_binary"
269 | "integer_to_list"
270 | "iolist_size"
271 | "iolist_to_binary"
272 | "is_alive"
273 | "is_atom"
274 | "is_binary"
275 | "is_bitstring"
276 | "is_boolean"
277 | "is_float"
278 | "is_function"
279 | "is_integer"
280 | "is_list"
281 | "is_map"
282 | "is_map_key"
283 | "is_number"
284 | "is_pid"
285 | "is_port"
286 | "is_process_alive"
287 | "is_record"
288 | "is_reference"
289 | "is_tuple"
290 | "length"
291 | "link"
292 | "list_to_atom"
293 | "list_to_binary"
294 | "list_to_bitstring"
295 | "list_to_existing_atom"
296 | "list_to_float"
297 | "list_to_integer"
298 | "list_to_pid"
299 | "list_to_tuple"
300 | "make_ref"
301 | "map_get"
302 | "map_size"
303 | "max"
304 | "min"
305 | "monitor"
306 | "node"
307 | "nodes"
308 | "now"
309 | "open_port"
310 | "pid_to_list"
311 | "process_flag"
312 | "process_info"
313 | "processes"
314 | "put"
315 | "register"
316 | "registered"
317 | "round"
318 | "self"
319 | "setelement"
320 | "size"
321 | "spawn"
322 | "spawn_link"
323 | "spawn_monitor"
324 | "spawn_opt"
325 | "split_binary"
326 | "statistics"
327 | "term_to_binary"
328 | "throw"
329 | "time"
330 | "tl"
331 | "trunc"
332 | "tuple_size"
333 | "tuple_to_list"
334 | "unlink"
335 | "unregister"
336 | "whereis"
337 )
338}