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