Skip to main content

brokk_bifrost_cpp/
reconcile.rs

1//! Resolution-time identity reconciliation for C++ out-of-line member
2//! definitions (#1134).
3//!
4//! A method nested inside a class that is itself inside a namespace --
5//! `log4cxx::Outer::Inner::method` -- is declared in a header (indexed
6//! `log4cxx.Outer$Inner.method`: `$` joins the class-nesting chain, the
7//! enclosing namespace is the package) and defined out-of-line in a `.cpp`
8//! (`int Outer::Inner::method() const {...}`). Per-file extraction cannot see
9//! the header's class layout, so it must guess whether each qualifier segment
10//! (`Outer`, `Inner`) is a namespace or a class. Two shapes guess wrong (#1121
11//! left them documented, not masked): a file-scope definition under a
12//! `using namespace` directive, and the template-specialization twin. Both are
13//! irreducibly class-table-dependent -- the only signal that `Outer` is a class
14//! and not a namespace is the set of classes visible to the `.cpp` through its
15//! `#include` graph.
16//!
17//! This module holds the pure decision: given the ordered owner segments of a
18//! definition's qualifier, the candidate enclosing namespaces (the lexical
19//! package plus any in-scope `using namespace` targets), and a view of the
20//! include-visible class table, decide the one canonical `(package, owner-chain,
21//! member)` a visible class actually confirms -- or refuse when nothing
22//! confirms or the confirmation is ambiguous. The function is analyzer-free and
23//! I/O-free so it can be unit-tested in isolation; the analyzer wiring that
24//! feeds it the real class table lives in the surrounding modules.
25
26/// A minimal, testable view of one class visible to a file through its
27/// `#include` graph: its enclosing namespace (`::`-joined, empty at global
28/// scope) and its class-nesting chain as Bifrost's `$`-joined short name
29/// (`Outer$Inner` for `Outer::Inner`, `Klass` for a non-nested class).
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct VisibleClass<'a> {
32    pub package: &'a str,
33    pub nested_short_name: &'a str,
34}
35
36/// The canonical identity a definition should unify under: the enclosing
37/// namespace (`::`-joined), the class-nesting chain (`$`-joined short name), and
38/// the member name.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ReconciledIdentity {
41    pub package: String,
42    pub owner_chain: String,
43    pub member: String,
44}
45
46impl ReconciledIdentity {
47    /// Render this identity as a `CodeUnit`-style `fq_name`
48    /// (`package.owner_chain.member`, package omitted at global scope), so it can
49    /// be compared against and keyed alongside stored identities.
50    pub fn fq_name(&self) -> String {
51        let short = format!("{}.{}", self.owner_chain, self.member);
52        if self.package.is_empty() {
53            short
54        } else {
55            format!("{}.{}", self.package, short)
56        }
57    }
58}
59
60/// Decide the canonical identity of an out-of-line member definition from its
61/// qualifier segments and the include-visible class table.
62///
63/// `owner_segments` are the qualifier segments in source order, excluding the
64/// terminal member -- `["Outer", "Inner"]` for `Outer::Inner::method`.
65/// `namespace_candidates` are the enclosing namespaces to try, most-authoritative
66/// first: the lexical package (the `namespace {}` block the definition sits in,
67/// or empty at file scope) followed by every `using namespace` target in scope
68/// at that point. `class_table` is the include-visible class table.
69///
70/// The function partitions the segments into a namespace prefix and a
71/// class-nesting suffix at every split point, prepending the leading segments
72/// onto each candidate namespace, and keeps a partition only when some visible
73/// class confirms it exactly (same package, same `$`-joined chain). It prefers
74/// the reading with the *longest* confirmed class chain (the deepest real
75/// nesting is the most specific true identity). It returns `None` when nothing
76/// in the table confirms any reading (caller leaves the provisional identity
77/// untouched) and when two distinct identities tie at the deepest confirmed
78/// nesting (genuinely ambiguous -- never guess).
79pub fn reconcile_out_of_line_member_identity(
80    owner_segments: &[&str],
81    member: &str,
82    namespace_candidates: &[&str],
83    class_table: &[VisibleClass<'_>],
84) -> Option<ReconciledIdentity> {
85    if owner_segments.is_empty() || member.is_empty() {
86        return None;
87    }
88
89    // Split points ordered by longest class chain first (smallest `i` reads the
90    // fewest leading segments as namespace, so the most as class nesting). The
91    // class suffix must be non-empty, so `i` never reaches `owner_segments.len()`.
92    for split in 0..owner_segments.len() {
93        let chain = owner_segments[split..].join("$");
94        let namespace_prefix = &owner_segments[..split];
95
96        let mut confirmed: Option<ReconciledIdentity> = None;
97        for namespace in namespace_candidates {
98            let package = join_namespace(namespace, namespace_prefix);
99            let matches = class_table
100                .iter()
101                .any(|visible| visible.package == package && visible.nested_short_name == chain);
102            if !matches {
103                continue;
104            }
105            let candidate = ReconciledIdentity {
106                package,
107                owner_chain: chain.clone(),
108                member: member.to_string(),
109            };
110            match &confirmed {
111                // A second, *distinct* reading at the same (deepest) nesting is
112                // genuinely ambiguous -- two visible classes with the same chain
113                // in different namespaces both match. Refuse rather than guess.
114                Some(existing) if existing != &candidate => return None,
115                Some(_) => {}
116                None => confirmed = Some(candidate),
117            }
118        }
119
120        if let Some(identity) = confirmed {
121            return Some(identity);
122        }
123    }
124
125    None
126}
127
128/// Join an enclosing namespace with the leading owner segments that are being
129/// read as further namespace nesting, in Bifrost's `::`-joined package form.
130fn join_namespace(namespace: &str, extra_segments: &[&str]) -> String {
131    let extra = extra_segments.join("::");
132    match (namespace.is_empty(), extra.is_empty()) {
133        (true, true) => String::new(),
134        (true, false) => extra,
135        (false, true) => namespace.to_string(),
136        (false, false) => format!("{namespace}::{extra}"),
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn identity(package: &str, owner_chain: &str, member: &str) -> ReconciledIdentity {
145        ReconciledIdentity {
146            package: package.to_string(),
147            owner_chain: owner_chain.to_string(),
148            member: member.to_string(),
149        }
150    }
151
152    /// File-scope definition under `using namespace log4cxx;`:
153    /// `int Outer::Inner::method()` at file scope. The lexical package is empty;
154    /// the using-directive contributes `log4cxx`. The include-visible class
155    /// table confirms `log4cxx::Outer::Inner`, so the whole qualifier is a class
156    /// chain under `log4cxx`, not a namespace path.
157    #[test]
158    fn file_scope_using_directive_shape_recovers_namespace_and_chain() {
159        let table = [
160            VisibleClass {
161                package: "log4cxx",
162                nested_short_name: "Outer",
163            },
164            VisibleClass {
165                package: "log4cxx",
166                nested_short_name: "Outer$Inner",
167            },
168        ];
169        let reconciled = reconcile_out_of_line_member_identity(
170            &["Outer", "Inner"],
171            "method",
172            &["", "log4cxx"],
173            &table,
174        );
175        assert_eq!(
176            reconciled,
177            Some(identity("log4cxx", "Outer$Inner", "method"))
178        );
179    }
180
181    /// Template-specialization twin inside `namespace ns {}`:
182    /// `Outer::Inner<int>::method`. The lexical package is `ns`; the templated
183    /// splitter mis-reads `Outer` as a namespace. The class table confirms
184    /// `ns::Outer::Inner`, folding `Outer` back into the class chain.
185    #[test]
186    fn template_shape_inside_namespace_block_folds_outer_into_chain() {
187        let table = [VisibleClass {
188            package: "ns",
189            nested_short_name: "Outer$Inner",
190        }];
191        let reconciled =
192            reconcile_out_of_line_member_identity(&["Outer", "Inner"], "method", &["ns"], &table);
193        assert_eq!(reconciled, Some(identity("ns", "Outer$Inner", "method")));
194    }
195
196    /// Genuine namespace chain `ns1::ns2::Klass::method` written out-of-line at
197    /// file scope: the class table contains the real class
198    /// (`package ns1::ns2`, `Klass`) but no nested-class reading of the leading
199    /// segments. Reconciliation confirms the namespace reading unchanged -- it
200    /// must never corrupt the owner into `ns1$ns2$Klass`.
201    #[test]
202    fn genuine_namespace_chain_keeps_namespace_reading() {
203        let table = [VisibleClass {
204            package: "ns1::ns2",
205            nested_short_name: "Klass",
206        }];
207        let reconciled = reconcile_out_of_line_member_identity(
208            &["ns1", "ns2", "Klass"],
209            "method",
210            &[""],
211            &table,
212        );
213        assert_eq!(reconciled, Some(identity("ns1::ns2", "Klass", "method")));
214    }
215
216    /// Deepest confirmed nesting wins: when both a shallow and a deep reading are
217    /// visible, the deeper class chain is the more specific true identity.
218    #[test]
219    fn prefers_longest_confirmed_class_chain() {
220        let table = [
221            VisibleClass {
222                package: "a::Outer",
223                nested_short_name: "Inner",
224            },
225            VisibleClass {
226                package: "a",
227                nested_short_name: "Outer$Inner",
228            },
229        ];
230        let reconciled =
231            reconcile_out_of_line_member_identity(&["Outer", "Inner"], "method", &["a"], &table);
232        assert_eq!(reconciled, Some(identity("a", "Outer$Inner", "method")));
233    }
234
235    /// Nothing visible confirms any reading: leave the provisional identity
236    /// untouched (caller keeps today's behavior).
237    #[test]
238    fn no_visible_class_returns_none() {
239        let reconciled = reconcile_out_of_line_member_identity(
240            &["Outer", "Inner"],
241            "method",
242            &["", "log4cxx"],
243            &[],
244        );
245        assert_eq!(reconciled, None);
246    }
247
248    /// Two visible classes confirm distinct readings at the same (deepest)
249    /// nesting depth -- genuinely ambiguous, so refuse rather than guess.
250    #[test]
251    fn ambiguous_equal_depth_readings_return_none() {
252        let table = [
253            VisibleClass {
254                package: "one",
255                nested_short_name: "Outer$Inner",
256            },
257            VisibleClass {
258                package: "two",
259                nested_short_name: "Outer$Inner",
260            },
261        ];
262        let reconciled = reconcile_out_of_line_member_identity(
263            &["Outer", "Inner"],
264            "method",
265            &["one", "two"],
266            &table,
267        );
268        assert_eq!(reconciled, None);
269    }
270
271    /// Empty owner segments or empty member name cannot name a member.
272    #[test]
273    fn degenerate_inputs_return_none() {
274        assert_eq!(
275            reconcile_out_of_line_member_identity(&[], "method", &["ns"], &[]),
276            None
277        );
278        assert_eq!(
279            reconcile_out_of_line_member_identity(&["Outer"], "", &["ns"], &[]),
280            None
281        );
282    }
283}