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