Skip to main content

backhopper_core/jigsaw/
references.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
5use std::cmp::Ordering;
6use std::collections::{BTreeMap, BTreeSet};
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::jigsaw::plugin_set::PluginFile;
12use crate::model::macro_decl::MacroKey;
13use crate::model::names::{
14    ApplicationName, Arity, BehaviourName, ConfKey, FieldName, FunctionName, Mfa, ModuleName,
15    RecordName, TypeName, ValidatorName, vocabulary,
16};
17use crate::model::schema_files::PartialRef;
18use crate::model::snapshot::{FunArity, HeaderRef};
19use crate::model::verdict::IndirectCallForm;
20
21vocabulary!(
22    /// What a run did not compare, counted instead of reported as a finding.
23    #[derive(PartialOrd, Ord, Hash)]
24    pub enum NotChecked: "not-checked reason" {
25        Internal => "internal",
26        Dynamic => "dynamic",
27        UnresolvedAtBuild => "unresolved_at_build",
28        Elixir => "elixir",
29        Untracked => "untracked",
30        UnresolvedInclude => "unresolved_include",
31        ConditionalSite => "conditional_site",
32    }
33);
34
35/// How a call site spells the call. It is on the site, so one function
36/// called two ways is one reference.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub enum CallForm {
39    Direct,
40    FunRef,
41    Apply,
42    Indirect(IndirectCallForm),
43}
44
45/// The fields a `#name{...}` construction leaves to the record's default.
46#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct RecordConstruction {
48    pub unset: BTreeSet<FieldName>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub enum SiteDetail {
53    Plain,
54    Call(CallForm),
55    Construction(RecordConstruction),
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct ReferenceSite {
60    pub file: PluginFile,
61    pub line: u32,
62    pub detail: SiteDetail,
63}
64
65/// Something the plugin set uses that it does not define itself.
66#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
67pub enum Reference {
68    Call(Mfa),
69    Record {
70        includer: PluginFile,
71        name: RecordName,
72    },
73    Macro {
74        includer: PluginFile,
75        key: MacroKey,
76    },
77    Header(HeaderRef),
78    Behaviour {
79        implementer: ModuleName,
80        behaviour: BehaviourName,
81    },
82    Type {
83        module: ModuleName,
84        name: TypeName,
85        arity: Arity,
86    },
87    Partial {
88        partial: PartialRef,
89        keys: BTreeSet<ConfKey>,
90    },
91    Validator(ValidatorName),
92    App(ApplicationName),
93}
94
95impl fmt::Display for Reference {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            Self::Call(mfa) => write!(f, "{mfa}"),
99            Self::Record { name, .. } => write!(f, "#{name}{{}}"),
100            Self::Macro { key, .. } => write!(f, "?{key}"),
101            Self::Header(header) => write!(f, "{header}"),
102            Self::Behaviour { behaviour, .. } => write!(f, "{behaviour}"),
103            Self::Type {
104                module,
105                name,
106                arity,
107            } => write!(f, "{module}:{name}/{arity}"),
108            Self::Partial { partial, .. } => write!(f, "{partial}"),
109            Self::Validator(name) => write!(f, "{name}"),
110            Self::App(app) => write!(f, "{app}"),
111        }
112    }
113}
114
115/// A call whose module, function or arity the source does not spell out.
116/// A part that is not known prints as `_`, and an unknown arity as `?`.
117#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
118pub struct DynamicCall {
119    pub module: Option<ModuleName>,
120    pub function: Option<FunctionName>,
121    pub arity: Option<Arity>,
122}
123
124impl fmt::Display for DynamicCall {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match &self.module {
127            Some(module) => write!(f, "{module}")?,
128            None => f.write_str("_")?,
129        }
130        f.write_str(":")?;
131        match &self.function {
132            Some(function) => write!(f, "{function}")?,
133            None => f.write_str("_")?,
134        }
135        f.write_str("/")?;
136        match self.arity {
137            Some(arity) => write!(f, "{arity}"),
138            None => f.write_str("?"),
139        }
140    }
141}
142
143/// What an unchecked site is about. The variant decides the reason, so a
144/// site cannot be listed under a reason that does not fit it.
145#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
146pub enum UncheckedSymbol {
147    UnresolvedAtBuild(Reference),
148    Dynamic(DynamicCall),
149}
150
151impl UncheckedSymbol {
152    #[must_use]
153    pub fn reason(&self) -> NotChecked {
154        match self {
155            Self::UnresolvedAtBuild(_) => NotChecked::UnresolvedAtBuild,
156            Self::Dynamic(_) => NotChecked::Dynamic,
157        }
158    }
159}
160
161impl fmt::Display for UncheckedSymbol {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::UnresolvedAtBuild(reference) => write!(f, "{reference}"),
165            Self::Dynamic(call) => write!(f, "{call}"),
166        }
167    }
168}
169
170/// A place in the plugin set that a run did not compare, and what is
171/// there. Sites order by reason, file, line and symbol.
172#[derive(Debug, Clone, PartialEq, Eq, Hash)]
173pub struct UncheckedSite {
174    pub symbol: UncheckedSymbol,
175    pub file: PluginFile,
176    pub line: u32,
177}
178
179impl UncheckedSite {
180    #[must_use]
181    pub fn reason(&self) -> NotChecked {
182        self.symbol.reason()
183    }
184}
185
186impl Ord for UncheckedSite {
187    fn cmp(&self, other: &Self) -> Ordering {
188        (self.reason(), &self.file, self.line, &self.symbol).cmp(&(
189            other.reason(),
190            &other.file,
191            other.line,
192            &other.symbol,
193        ))
194    }
195}
196
197impl PartialOrd for UncheckedSite {
198    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
199        Some(self.cmp(other))
200    }
201}
202
203/// Every external reference of a plugin set with the places it is used,
204/// plus what collection already set aside as not checked.
205#[derive(Debug, Clone, Default)]
206pub struct ReferenceSet {
207    external: BTreeMap<Reference, BTreeSet<ReferenceSite>>,
208    host_includes: BTreeMap<PluginFile, BTreeSet<HeaderRef>>,
209    exports: BTreeMap<ModuleName, BTreeSet<FunArity>>,
210    unresolved_includes: BTreeSet<PluginFile>,
211    not_checked: BTreeMap<NotChecked, usize>,
212    unchecked: BTreeSet<UncheckedSite>,
213}
214
215impl ReferenceSet {
216    #[must_use]
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    pub fn add(&mut self, reference: Reference, site: ReferenceSite) {
222        self.external.entry(reference).or_default().insert(site);
223    }
224
225    pub fn count(&mut self, reason: NotChecked, n: usize) {
226        if n > 0 {
227            *self.not_checked.entry(reason).or_default() += n;
228        }
229    }
230
231    pub fn add_unchecked(&mut self, site: UncheckedSite) {
232        self.unchecked.insert(site);
233    }
234
235    pub fn set_host_includes(&mut self, file: PluginFile, headers: BTreeSet<HeaderRef>) {
236        if !headers.is_empty() {
237            self.host_includes.insert(file, headers);
238        }
239    }
240
241    pub fn set_exports(&mut self, module: ModuleName, exports: BTreeSet<FunArity>) {
242        self.exports.insert(module, exports);
243    }
244
245    pub fn mark_unresolved_include(&mut self, file: PluginFile) {
246        self.unresolved_includes.insert(file);
247    }
248
249    #[must_use]
250    pub fn external(&self) -> &BTreeMap<Reference, BTreeSet<ReferenceSite>> {
251        &self.external
252    }
253
254    #[must_use]
255    pub fn not_checked(&self) -> &BTreeMap<NotChecked, usize> {
256        &self.not_checked
257    }
258
259    #[must_use]
260    pub fn unchecked(&self) -> &BTreeSet<UncheckedSite> {
261        &self.unchecked
262    }
263
264    /// The host headers a file includes, directly or through the plugin
265    /// set's own headers.
266    #[must_use]
267    pub fn host_includes(&self, file: &PluginFile) -> Option<&BTreeSet<HeaderRef>> {
268        self.host_includes.get(file)
269    }
270
271    #[must_use]
272    pub fn exports_of(&self, module: &ModuleName) -> Option<&BTreeSet<FunArity>> {
273        self.exports.get(module)
274    }
275
276    #[must_use]
277    pub fn has_unresolved_include(&self, file: &PluginFile) -> bool {
278        self.unresolved_includes.contains(file)
279    }
280}