1use 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 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 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 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 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 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 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 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 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 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 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}