Skip to main content

backhopper_core/compat/
behaviour_callback_resolve.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Compares a declared behaviour's `-callback` surface between the two
6//! trees: the case where a pick adds an implementation written against
7//! the source's callback set and lands on a target whose set is wider
8//! or shape-different. First-party behaviours only, the ones no pin
9//! snapshot covers. A legacy behaviour that declares its surface
10//! through `behaviour_info/1` has no `-callback` forms and is
11//! invisible here, which withholds.
12
13use std::collections::BTreeSet;
14
15use crate::compat::added_lines::{AddedLinesSubject, file_line};
16use crate::compat::qualified_call_resolve::{ModuleProvenance, TreeReader, classify_module};
17use crate::compat::source_attributes::{
18    extract_behaviours, extract_callbacks, extract_optional_callbacks,
19};
20use crate::model::names::{ModuleName, RelativePath};
21use crate::model::verdict::Reason;
22
23/// Flag callbacks the target's version of a declared behaviour
24/// requires that the source's does not declare (an implementation
25/// written against the source is missing them), and shared callbacks
26/// whose normalized signatures disagree. Runs only for behaviours the
27/// added lines declare; an unreadable side withholds silently, and a
28/// callback the source requires but the target lacks is dead code, not
29/// a backport risk. One pass per `(file, behaviour)`.
30pub fn analyse_behaviour_callbacks(
31    subjects: &[AddedLinesSubject<'_>],
32    covered_modules: &BTreeSet<ModuleName>,
33    resolve_module_path: &dyn Fn(&ModuleName) -> Option<RelativePath>,
34    read_target: TreeReader<'_>,
35    read_source: Option<TreeReader<'_>>,
36) -> Vec<Reason> {
37    let Some(read_source) = read_source else {
38        return Vec::new();
39    };
40    let mut reasons = Vec::new();
41    for subject in subjects {
42        let mut seen = BTreeSet::new();
43        for declared in extract_behaviours(subject.added_text) {
44            if !seen.insert(declared.behaviour.clone()) {
45                continue;
46            }
47            let ModuleProvenance::FirstParty { path } =
48                classify_module(&declared.behaviour, covered_modules, resolve_module_path)
49            else {
50                continue;
51            };
52            let (Some(target_text), Some(source_text)) = (read_target(&path), read_source(&path))
53            else {
54                continue;
55            };
56            let target_callbacks = extract_callbacks(&target_text);
57            if target_callbacks.is_empty() {
58                continue;
59            }
60            let source_callbacks = extract_callbacks(&source_text);
61            let optional = extract_optional_callbacks(&target_text);
62            let line = file_line(subject.line_map, declared.line);
63            for (key, target_signature) in &target_callbacks {
64                match source_callbacks.get(key) {
65                    None if !optional.contains(key) => {
66                        reasons.push(Reason::BehaviourCallbackAddedOnTarget {
67                            source_path: subject.source_path.clone(),
68                            behaviour: declared.behaviour.clone(),
69                            callback: key.0.clone(),
70                            arity: key.1,
71                            line,
72                        });
73                    }
74                    Some(source_signature) if source_signature != target_signature => {
75                        reasons.push(Reason::BehaviourCallbackDriftOnTarget {
76                            source_path: subject.source_path.clone(),
77                            behaviour: declared.behaviour.clone(),
78                            callback: key.0.clone(),
79                            arity: key.1,
80                            source_signature: source_signature.clone(),
81                            target_signature: target_signature.clone(),
82                            line,
83                        });
84                    }
85                    _ => {}
86                }
87            }
88        }
89    }
90    reasons
91}