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