Skip to main content

backhopper_xref/
analysis.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//! Predefined analyses.
6//!
7//! Each method on [`Xref`] returns a typed [`result`](crate::result) struct
8//! with location-tagged entries.
9
10use std::collections::{BTreeMap, BTreeSet};
11use std::iter;
12
13use backhopper_core::{BehaviourName, Mfa, ModuleName};
14use backhopper_xref_graph::{
15    CallKind, Deprecation, FunctionRef, FunctionSig, Functions, Loc, Mode, PathId, Position,
16    Relation, Vertex, VertexSet,
17};
18
19use crate::result::{
20    CalleeEntry, CalleesOf, CallerEntry, CallersOf, DeprecatedCall, DeprecatedFunctionCalls,
21    ExportsNotUsed, LocalsNotUsed, ModuleCallers, ModuleDependencies, UndefinedFunctionCall,
22    UndefinedFunctionCalls, UnresolvedCalls, UnusedExport, UnusedLocal,
23};
24use crate::xref::Xref;
25
26fn sig_to_mfa(module: &ModuleName, sig: &FunctionSig) -> Mfa {
27    Mfa::new(module.clone(), sig.name.clone(), sig.arity)
28}
29
30impl Xref<Functions> {
31    /// External calls whose target is not defined in any loaded module and
32    /// is not a builtin.
33    pub fn undefined_function_calls(&self) -> UndefinedFunctionCalls {
34        let defined = self.graph().defined_functions();
35        let builtins = self.graph().builtins();
36        let mut grouped: BTreeMap<(Mfa, Mfa), Vec<Loc>> = BTreeMap::new();
37        for (src, tgt) in self.graph().external_calls().iter() {
38            let (Vertex::Function(caller), Vertex::Function(callee)) = (src, tgt) else {
39                continue;
40            };
41            if defined.contains(tgt) || builtins.contains(tgt) {
42                continue;
43            }
44            // An undefined callee has no definition site: point at the caller's instead.
45            grouped
46                .entry((caller.clone(), callee.clone()))
47                .or_default()
48                .extend(self.graph().def_at(caller).cloned());
49        }
50        let entries = grouped
51            .into_iter()
52            .map(|((caller, callee), locations)| UndefinedFunctionCall {
53                caller,
54                callee: FunctionRef::Concrete(callee),
55                kind: CallKind::Direct,
56                locations,
57            })
58            .collect();
59        UndefinedFunctionCalls { entries }
60    }
61
62    /// Exports that no other module calls.
63    pub fn exports_not_used(&self) -> ExportsNotUsed {
64        let used_externally = self.graph().external_calls().targets();
65        let mut entries = Vec::new();
66        for (module, summary) in self.graph().modules() {
67            for sig in &summary.exports {
68                let mfa = sig_to_mfa(module, sig);
69                let v = Vertex::Function(mfa.clone());
70                if used_externally.contains(&v) {
71                    continue;
72                }
73                let is_callback = summary.callbacks_required.contains(sig)
74                    || summary.callbacks_optional.contains(sig)
75                    || self.satisfies_any_callback(module, sig);
76                let is_on_load = self.graph().on_load_functions().contains(&mfa);
77                let def_loc = self.graph().def_at(&mfa).cloned();
78                entries.push(UnusedExport {
79                    mfa,
80                    def_loc,
81                    satisfies_callback: is_callback,
82                    is_on_load,
83                });
84            }
85        }
86        ExportsNotUsed { entries }
87    }
88
89    fn satisfies_any_callback(&self, module: &ModuleName, sig: &FunctionSig) -> bool {
90        let Some(summary) = self.graph().module(module) else {
91            return false;
92        };
93        for b in &summary.behaviours {
94            let beh_module = ModuleName::new(b.as_str().to_owned()).ok();
95            if let Some(bm) = beh_module {
96                if let Some(beh_summary) = self.graph().module(&bm) {
97                    if beh_summary.callbacks_required.contains(sig)
98                        || beh_summary.callbacks_optional.contains(sig)
99                    {
100                        return true;
101                    }
102                }
103            }
104        }
105        false
106    }
107
108    /// Locals that are not called from any function in the same module.
109    pub fn locals_not_used(&self) -> LocalsNotUsed {
110        let used_locally = self.graph().local_calls().targets();
111        let mut entries = Vec::new();
112        for (module, summary) in self.graph().modules() {
113            for sig in &summary.locals {
114                let mfa = sig_to_mfa(module, sig);
115                let v = Vertex::Function(mfa.clone());
116                if used_locally.contains(&v) {
117                    continue;
118                }
119                if self.graph().on_load_functions().contains(&mfa) {
120                    continue;
121                }
122                entries.push(UnusedLocal {
123                    mfa: mfa.clone(),
124                    def_loc: self.graph().def_at(&mfa).cloned(),
125                });
126            }
127        }
128        LocalsNotUsed { entries }
129    }
130
131    /// External calls whose target is marked deprecated.
132    pub fn deprecated_function_calls(&self) -> DeprecatedFunctionCalls {
133        let mut grouped: BTreeMap<(Mfa, Mfa), Vec<Loc>> = BTreeMap::new();
134        let deprecated: BTreeMap<Mfa, Deprecation> = self
135            .graph()
136            .deprecated_mfas()
137            .map(|(m, d)| (m.clone(), d.clone()))
138            .collect();
139        for (src, tgt) in self.graph().external_calls().iter() {
140            let (Vertex::Function(caller), Vertex::Function(callee)) = (src, tgt) else {
141                continue;
142            };
143            if !deprecated.contains_key(callee) {
144                continue;
145            }
146            grouped
147                .entry((caller.clone(), callee.clone()))
148                .or_default()
149                .push(
150                    self.graph()
151                        .def_at(callee)
152                        .cloned()
153                        .unwrap_or_else(|| Loc::point(PathId::new(0), Position::zero())),
154                );
155        }
156        let entries = grouped
157            .into_iter()
158            .map(|((caller, callee), locations)| {
159                let d = &deprecated[&callee];
160                DeprecatedCall {
161                    caller,
162                    callee,
163                    tier: d.tier,
164                    message: d.message.clone(),
165                    locations,
166                }
167            })
168            .collect();
169        DeprecatedFunctionCalls { entries }
170    }
171
172    /// Functions that call into `target`. With `transitive`, the preimage
173    /// under the transitive closure of the call relation.
174    pub fn called_by(&self, target: &Mfa, transitive: bool) -> CallersOf {
175        let target_set: VertexSet = iter::once(Vertex::Function(target.clone())).collect();
176        let direct;
177        let relation: &Relation = if transitive {
178            self.all_calls_closure()
179        } else {
180            direct = self.graph().all_calls();
181            &direct
182        };
183        let sources = relation.preimage(&target_set);
184        let entries = sources
185            .iter()
186            .filter_map(|v| {
187                if let Vertex::Function(mfa) = v {
188                    Some(CallerEntry {
189                        caller: mfa.clone(),
190                        kind: CallKind::Direct,
191                        locations: vec![],
192                    })
193                } else {
194                    None
195                }
196            })
197            .collect();
198        CallersOf {
199            target: target.clone(),
200            entries,
201        }
202    }
203
204    /// Functions that `source` calls.
205    pub fn calls_from(&self, source: &Mfa, transitive: bool) -> CalleesOf {
206        let src_set: VertexSet = iter::once(Vertex::Function(source.clone())).collect();
207        let direct;
208        let relation: &Relation = if transitive {
209            self.all_calls_closure()
210        } else {
211            direct = self.graph().all_calls();
212            &direct
213        };
214        let targets = relation.image(&src_set);
215        let entries = targets
216            .iter()
217            .filter_map(|v| {
218                if let Vertex::Function(mfa) = v {
219                    Some(CalleeEntry {
220                        callee: FunctionRef::Concrete(mfa.clone()),
221                        kind: CallKind::Direct,
222                        locations: vec![],
223                    })
224                } else {
225                    None
226                }
227            })
228            .collect();
229        CalleesOf {
230            source: source.clone(),
231            entries,
232        }
233    }
234
235    /// All recorded unresolved call sites.
236    pub fn unresolved_calls(&self) -> UnresolvedCalls {
237        UnresolvedCalls {
238            entries: self.graph().unresolved_calls().iter().cloned().collect(),
239        }
240    }
241}
242
243impl<M: Mode> Xref<M> {
244    /// Modules that depend on `target` (one-hop, module-level).
245    pub fn module_called_by(&self, target: &ModuleName) -> ModuleCallers {
246        let target_set: VertexSet = iter::once(Vertex::Module(target.clone())).collect();
247        let mods = self.graph().module_edges().preimage(&target_set);
248        let entries = mods.iter().filter_map(|v| v.as_module().cloned()).collect();
249        ModuleCallers {
250            target: target.clone(),
251            entries,
252        }
253    }
254
255    pub fn module_call(&self, source: &ModuleName) -> ModuleDependencies {
256        let src_set: VertexSet = iter::once(Vertex::Module(source.clone())).collect();
257        let mods = self.graph().module_edges().image(&src_set);
258        let entries = mods.iter().filter_map(|v| v.as_module().cloned()).collect();
259        ModuleDependencies {
260            source: source.clone(),
261            entries,
262        }
263    }
264
265    pub fn implementers_of(&self, behaviour: &BehaviourName) -> BTreeSet<ModuleName> {
266        let set: VertexSet = iter::once(Vertex::Behaviour(behaviour.clone())).collect();
267        self.graph()
268            .behaviour_edges()
269            .preimage(&set)
270            .iter()
271            .filter_map(|v| v.as_module().cloned())
272            .collect()
273    }
274
275    /// Strongly connected components of the module call graph that contain
276    /// more than one module.
277    pub fn module_cycles(&self) -> Vec<Vec<ModuleName>> {
278        let closure = self.graph().module_edges().transitive_closure();
279        let mut groups: BTreeMap<ModuleName, BTreeSet<ModuleName>> = BTreeMap::new();
280        for (s, t) in closure.iter() {
281            if s == t {
282                continue;
283            }
284            if closure.contains(t, s) {
285                if let (Some(sm), Some(tm)) = (s.as_module(), t.as_module()) {
286                    groups.entry(sm.clone()).or_default().insert(tm.clone());
287                }
288            }
289        }
290        let mut seen: BTreeSet<ModuleName> = BTreeSet::new();
291        let mut out = Vec::new();
292        for (m, members) in groups {
293            if seen.contains(&m) {
294                continue;
295            }
296            let mut full = members;
297            full.insert(m);
298            for x in &full {
299                seen.insert(x.clone());
300            }
301            let mut cycle: Vec<ModuleName> = full.into_iter().collect();
302            cycle.sort();
303            out.push(cycle);
304        }
305        out
306    }
307}