Skip to main content

backhopper_xref/
builder.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//! `XrefBuilder`: collects modules and produces an [`Xref`].
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::mem;
9use std::path::PathBuf;
10
11use backhopper_core::{ApplicationName, Mfa, ModuleName};
12use backhopper_xref_graph::{
13    Building, CallGraph, CallTarget, FunctionRef, Functions, LocalFunctionRef, ModuleSummary,
14    VertexSet,
15};
16use backhopper_xref_reader::{ModuleData, ProjectLayout, ReadOutput, SourceReader};
17
18use crate::errors::XrefError;
19use crate::xref::Xref;
20
21#[derive(Debug, Clone, Default)]
22pub struct XrefBuilder {
23    layout: ProjectLayout,
24    modules: BTreeMap<ModuleName, ModuleData>,
25    applications: BTreeSet<ApplicationName>,
26    builtins: VertexSet,
27    warnings: Vec<backhopper_xref_reader::ReadWarning>,
28}
29
30impl XrefBuilder {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    #[must_use]
36    pub fn with_layout(mut self, layout: ProjectLayout) -> Self {
37        self.layout = layout;
38        self
39    }
40
41    #[must_use]
42    pub fn with_builtins(mut self, b: VertexSet) -> Self {
43        self.builtins = b;
44        self
45    }
46
47    /// Add an application from in-memory `(path, bytes)` pairs.
48    pub fn add_application<I>(
49        &mut self,
50        app: ApplicationName,
51        files: I,
52    ) -> Result<&mut Self, XrefError>
53    where
54        I: IntoIterator<Item = (PathBuf, Vec<u8>)>,
55    {
56        if !self.applications.insert(app.clone()) {
57            return Err(XrefError::DuplicateApplication(app));
58        }
59        let reader = SourceReader::with_layout(self.layout.clone());
60        let ReadOutput {
61            modules,
62            warnings,
63            paths: _,
64        } = reader.read_tree(files)?;
65        self.warnings.extend(warnings);
66        for m in modules {
67            let name = m.module.clone();
68            let new_app = m.application.assigned().map(|a| a.as_str().to_owned());
69            if let Some(prev) = self.modules.insert(name.clone(), m) {
70                let prev_app = prev.application.assigned().map(|a| a.as_str().to_owned());
71                self.warnings.push(
72                    backhopper_xref_reader::ReadWarning::DuplicateModuleAcrossApplications {
73                        name,
74                        first_application: prev_app,
75                        second_application: new_app,
76                    },
77                );
78            }
79        }
80        Ok(self)
81    }
82
83    pub fn add_modules<I>(&mut self, modules: I) -> &mut Self
84    where
85        I: IntoIterator<Item = ModuleData>,
86    {
87        for m in modules {
88            self.modules.insert(m.module.clone(), m);
89        }
90        self
91    }
92
93    pub fn warnings(&self) -> &[backhopper_xref_reader::ReadWarning] {
94        &self.warnings
95    }
96
97    pub fn build(self) -> Result<Xref<Functions>, XrefError> {
98        let mut graph: CallGraph<Functions, Building> = CallGraph::new();
99        graph.set_builtins(self.builtins);
100        let mut modules = self.modules;
101        for (name, data) in &modules {
102            let summary = ModuleSummary {
103                application: data.application.assigned().cloned(),
104                exports: data.exports.clone(),
105                locals: data.locals.clone(),
106                on_load: data.on_load.clone(),
107                behaviours: data.behaviours.clone(),
108                callbacks_required: data.callbacks.clone(),
109                callbacks_optional: data.optional_callbacks.clone(),
110            };
111            graph.insert_module(name.clone(), summary);
112        }
113        for (_, data) in mem::take(&mut modules) {
114            let module = data.module;
115            for (sig, loc) in data.definitions {
116                let mfa = Mfa::new(module.clone(), sig.name, sig.arity);
117                graph.record_definition(mfa, loc);
118            }
119            if let Some(sig) = data.on_load {
120                graph.record_on_load(Mfa::new(module.clone(), sig.name, sig.arity));
121            }
122            for (sig, dep) in data.deprecated {
123                graph.record_deprecation(Mfa::new(module.clone(), sig.name, sig.arity), dep);
124            }
125            for cs in data.local_calls {
126                if let CallTarget::Local(LocalFunctionRef::Concrete { function, arity }) = cs.callee
127                {
128                    let caller = Mfa::new(module.clone(), cs.caller.name, cs.caller.arity);
129                    let callee = Mfa::new(module.clone(), function, arity);
130                    graph.insert_local_call(caller, callee);
131                }
132            }
133            for cs in data.external_calls {
134                if let CallTarget::External(FunctionRef::Concrete(callee)) = cs.callee {
135                    let caller = Mfa::new(module.clone(), cs.caller.name, cs.caller.arity);
136                    graph.insert_external_call(caller, callee);
137                }
138            }
139            for u in data.unresolved {
140                graph.insert_unresolved(u);
141            }
142        }
143        Ok(Xref::from_graph(graph.finish(), self.warnings))
144    }
145}