Skip to main content

brokk_bifrost_cpp/
hierarchy.rs

1//! C++ type-hierarchy resolution: the include-visible class table and the
2//! namespace/alias search that turns a written base specifier into a `CodeUnit`.
3//!
4//! `analyzer/cpp/hierarchy.rs` in `brokk-bifrost-analysis` keeps the
5//! `TypeHierarchyProvider` impl, the two moka caches it reads through and the
6//! `test-support` build counter; every decision they memoize is a function here.
7
8use crate::declarations::normalize_cpp_whitespace;
9use crate::graph_support::CppSource;
10use crate::imports::{include_paths, resolve_include_targets_with_index};
11use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
12use brokk_bifrost_core::hash::HashSet;
13use brokk_bifrost_core::path_utils::rel_path_string;
14use brokk_bifrost_core::profiling;
15
16/// Every class-like or alias declaration reachable from `file` through its
17/// transitive `#include` closure, sorted and deduplicated.
18///
19/// This is the builder behind [`CppSource::visible_type_units`]; the
20/// analyzer memoizes the result per file and records the build for the
21/// `visible_type_units_build_count_for_test` counter before calling in.
22///
23/// `keep_going` is polled once per file popped from the pending stack, which is
24/// the natural checkpoint: one pop is one file's declarations plus one file's
25/// imports. `None` means the walk stopped short, and the caller must not
26/// memoize it (issue #1748).
27pub fn build_cpp_visible_type_units(
28    cpp: &dyn CppSource,
29    file: &ProjectFile,
30    keep_going: &dyn Fn() -> bool,
31) -> Option<Vec<CodeUnit>> {
32    let _scope =
33        profiling::scope_with(|| format!("cpp.visible_types.build[{}]", rel_path_string(file)));
34    let include_targets = cpp.include_target_index();
35    let mut visited = HashSet::default();
36    let mut declarations = Vec::new();
37    let mut pending = vec![file.clone()];
38    visited.insert(file.clone());
39
40    while let Some(current) = pending.pop() {
41        if !keep_going() {
42            return None;
43        }
44        {
45            let _decls = profiling::scope("cpp.visible_types.decls");
46            declarations.extend(
47                cpp.declarations(&current)
48                    .into_iter()
49                    .filter(|unit| unit.is_class() || cpp.is_type_alias(unit)),
50            );
51        }
52
53        let imports = {
54            let _imports = profiling::scope("cpp.visible_types.imports");
55            cpp.import_statements(&current)
56        };
57        for include in include_paths(&imports) {
58            for target in resolve_include_targets_with_index(&current, &include, include_targets) {
59                if visited.insert(target.clone()) {
60                    pending.push(target);
61                }
62            }
63        }
64    }
65
66    declarations.sort();
67    declarations.dedup();
68    profiling::note_with(|| {
69        format!(
70            "cpp.visible_types.done[{}] visited={} declarations={}",
71            rel_path_string(file),
72            visited.len(),
73            declarations.len()
74        )
75    });
76    Some(declarations)
77}
78
79/// The direct base classes of `code_unit`, resolved through the include-visible
80/// class table and canonicalized past any type-alias hops.
81///
82/// `None` means the include-closure walk this resolution needs stopped at the
83/// caller's deadline. An empty vector still means "no resolvable bases", so the
84/// two outcomes stay distinguishable.
85pub fn cpp_resolve_direct_ancestors(
86    cpp: &dyn CppSource,
87    code_unit: &CodeUnit,
88    keep_going: &dyn Fn() -> bool,
89) -> Option<Vec<CodeUnit>> {
90    if !code_unit.is_class() || cpp.is_type_alias(code_unit) {
91        return Some(Vec::new());
92    }
93
94    let visible = cpp.visible_type_units_while(code_unit.source(), keep_going)?;
95    let mut ancestors = Vec::new();
96    for raw in cpp.raw_supertypes_of(code_unit) {
97        if let Some(ancestor) = resolve_base_type(cpp, code_unit, &raw, &visible)
98            && !ancestors.iter().any(|existing| existing == &ancestor)
99        {
100            ancestors.push(ancestor);
101        }
102    }
103    Some(ancestors)
104}
105
106fn resolve_base_type(
107    cpp: &dyn CppSource,
108    code_unit: &CodeUnit,
109    raw: &str,
110    visible: &[CodeUnit],
111) -> Option<CodeUnit> {
112    let normalized = normalize_cpp_type_reference(raw)?;
113    let resolved = if normalized.name.contains("::") || normalized.global {
114        resolve_qualified_type(
115            code_unit.package_name(),
116            &normalized.name,
117            normalized.global,
118            visible,
119        )
120    } else {
121        resolve_unqualified_base(code_unit, &normalized.name, visible)
122    }?;
123    canonicalize_alias(cpp, resolved, visible, &mut HashSet::default())
124}
125
126fn resolve_unqualified_base<'a>(
127    code_unit: &CodeUnit,
128    name: &str,
129    visible: &'a [CodeUnit],
130) -> Option<&'a CodeUnit> {
131    for namespace in namespace_search_order(code_unit.package_name()) {
132        if let Some(candidate) = visible.iter().find(|candidate| {
133            candidate.identifier() == name && candidate.package_name() == namespace
134        }) {
135            return Some(candidate);
136        }
137    }
138
139    visible
140        .iter()
141        .find(|candidate| candidate.identifier() == name)
142}
143
144fn canonicalize_alias(
145    cpp: &dyn CppSource,
146    unit: &CodeUnit,
147    visible: &[CodeUnit],
148    seen: &mut HashSet<String>,
149) -> Option<CodeUnit> {
150    if !cpp.is_type_alias(unit) {
151        return Some(unit.clone());
152    }
153    if !seen.insert(unit.fq_name()) {
154        return None;
155    }
156    let target = alias_target_text(unit)?;
157    let resolved = if target.name.contains("::") || target.global {
158        resolve_qualified_type(unit.package_name(), &target.name, target.global, visible)
159    } else {
160        visible
161            .iter()
162            .find(|candidate| {
163                candidate.identifier() == target.name
164                    && candidate.package_name() == unit.package_name()
165            })
166            .or_else(|| {
167                visible
168                    .iter()
169                    .find(|candidate| candidate.identifier() == target.name)
170            })
171    }?;
172    canonicalize_alias(cpp, resolved, visible, seen)
173}
174
175fn resolve_qualified_type<'a>(
176    lexical_namespace: &str,
177    name: &str,
178    global: bool,
179    visible: &'a [CodeUnit],
180) -> Option<&'a CodeUnit> {
181    let namespaces = if global {
182        vec![""]
183    } else {
184        namespace_search_order(lexical_namespace)
185    };
186    namespaces.into_iter().find_map(|namespace| {
187        let qualified = if namespace.is_empty() {
188            name.to_string()
189        } else {
190            format!("{namespace}::{name}")
191        };
192        visible
193            .iter()
194            .find(|candidate| cpp_name_for(candidate) == qualified)
195    })
196}
197
198fn namespace_search_order(package_name: &str) -> Vec<&str> {
199    let mut namespaces = Vec::new();
200    let mut current = package_name;
201    loop {
202        namespaces.push(current);
203        let Some((parent, _)) = current.rsplit_once("::") else {
204            if !current.is_empty() {
205                namespaces.push("");
206            }
207            return namespaces;
208        };
209        current = parent;
210    }
211}
212
213fn alias_target_text(alias: &CodeUnit) -> Option<NormalizedCppTypeReference> {
214    let signature = alias.signature()?.trim();
215    let target = signature
216        .strip_prefix("using ")
217        .and_then(|rest| rest.split_once('=').map(|(_, rhs)| rhs))
218        .or_else(|| {
219            signature
220                .strip_prefix("typedef ")
221                .and_then(|rest| rest.rsplit_once(' ').map(|(lhs, _)| lhs))
222        })?
223        .trim()
224        .trim_end_matches(';');
225    normalize_cpp_type_reference(target)
226}
227
228struct NormalizedCppTypeReference {
229    name: String,
230    global: bool,
231}
232
233fn normalize_cpp_type_reference(value: &str) -> Option<NormalizedCppTypeReference> {
234    let mut text = normalize_cpp_whitespace(value)
235        .trim_start_matches("new ")
236        .trim()
237        .to_string();
238    if let Some(index) = text.find(['(', '{']) {
239        text.truncate(index);
240    }
241    if let Some(index) = text.find('<') {
242        text.truncate(index);
243    }
244    let normalized = text
245        .trim()
246        .trim_start_matches("const ")
247        .trim_end_matches(|ch: char| ch == '*' || ch == '&' || ch.is_whitespace())
248        .trim();
249    let global = normalized.starts_with("::");
250    let normalized = normalized.trim_matches(':').trim();
251    let normalized = normalized
252        .strip_prefix("struct ")
253        .or_else(|| normalized.strip_prefix("class "))
254        .or_else(|| normalized.strip_prefix("enum "))
255        .unwrap_or(normalized)
256        .trim();
257    (!normalized.is_empty()).then(|| NormalizedCppTypeReference {
258        name: normalized.to_string(),
259        global,
260    })
261}
262
263fn cpp_name_for(unit: &CodeUnit) -> String {
264    let short = unit.short_name().replace(['.', '$'], "::");
265    if unit.package_name().is_empty() {
266        short
267    } else {
268        format!("{}::{}", unit.package_name(), short)
269    }
270}