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