Skip to main content

brokk_bifrost_cpp/
external_declarations.rs

1//! Structured declaration extraction for external C++ headers.
2//!
3//! This module is the language-only scanner shared by dependency-pack
4//! production and external-boundary evidence. It emits neutral records so this
5//! crate stays below `brokk-bifrost-analysis` in the dependency graph.
6
7use crate::adapter::parse_cpp_file;
8use crate::declarations::node_text;
9use crate::graph::resolver::cpp_name_for;
10use brokk_bifrost_core::analyzer::ProjectFile;
11use brokk_bifrost_core::analyzer::model::{CodeUnit, CodeUnitType};
12use brokk_bifrost_core::analyzer::tree_walk::collect_parse_errors;
13use brokk_bifrost_core::hash::HashMap;
14use std::path::{Path, PathBuf};
15use tree_sitter::{Node, Parser};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct CppExternalDeclarationLimits {
19    pub max_records: usize,
20}
21
22impl Default for CppExternalDeclarationLimits {
23    fn default() -> Self {
24        Self {
25            max_records: 250_000,
26        }
27    }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CppExternalDeclarationCompleteness {
32    Complete,
33    Partial,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum CppExternalMemberKind {
38    Function,
39    Field,
40    Macro,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CppExternalVisibility {
45    Public,
46    Protected,
47    Private,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct CppExternalType {
52    pub name: String,
53    pub source_name: String,
54    pub visibility: CppExternalVisibility,
55    pub source_path: PathBuf,
56    pub direct_bases: Vec<String>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct CppExternalMember {
61    pub owner: Option<String>,
62    pub name: String,
63    pub qualified_name: String,
64    pub kind: CppExternalMemberKind,
65    pub visibility: CppExternalVisibility,
66    pub is_constructor: bool,
67    pub signature: Option<String>,
68    pub parameter_types: Option<Vec<String>>,
69    pub return_type: Option<String>,
70    pub source_path: PathBuf,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CppExternalDeclarationDiagnostic {
75    pub code: &'static str,
76    pub message: String,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct CppExternalDeclarationSet {
81    pub types: Vec<CppExternalType>,
82    pub members: Vec<CppExternalMember>,
83    pub completeness: CppExternalDeclarationCompleteness,
84    pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
85}
86
87/// Return the literal paths from angle-bracket includes in one C++ source.
88///
89/// Quoted and computed includes are not external-root evidence. Conditional
90/// includes are retained because compile-context resolution still proves the
91/// root, while pack completeness records preprocessor uncertainty separately.
92pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
93    let mut parser = Parser::new();
94    parser
95        .set_language(&tree_sitter_cpp::LANGUAGE.into())
96        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
97    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
98    let mut paths = Vec::new();
99    let mut stack = vec![tree.root_node()];
100    while let Some(node) = stack.pop() {
101        if node.kind() == "preproc_include"
102            && !has_conditional_preprocessor_ancestor(node)
103            && let Some(path) = node.child_by_field_name("path")
104            && path.kind() == "system_lib_string"
105            && let Some(path) = node_text(path, source)
106                .strip_prefix('<')
107                .and_then(|path| path.strip_suffix('>'))
108                .filter(|path| !path.is_empty())
109        {
110            paths.push(PathBuf::from(path));
111        }
112        let mut cursor = node.walk();
113        stack.extend(node.named_children(&mut cursor));
114    }
115    paths.sort();
116    paths.dedup();
117    paths
118}
119
120fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
121    while let Some(parent) = node.parent() {
122        if matches!(
123            parent.kind(),
124            "preproc_if" | "preproc_ifdef" | "preproc_ifndef" | "preproc_elif" | "preproc_else"
125        ) {
126            return true;
127        }
128        node = parent;
129    }
130    false
131}
132
133/// Extract declarations from one exact header source entry.
134///
135/// `source_path` is relative to `source_set_root`. The caller owns source-set
136/// containment, byte limits, cancellation, and stable hashing. This function
137/// owns only C++ syntax interpretation and its record limit.
138pub fn extract_external_declarations(
139    source_set_root: &Path,
140    source_path: &Path,
141    source: &str,
142    limits: CppExternalDeclarationLimits,
143) -> CppExternalDeclarationSet {
144    let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
145    let mut parser = Parser::new();
146    parser
147        .set_language(&tree_sitter_cpp::LANGUAGE.into())
148        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
149    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
150
151    let mut diagnostics = Vec::new();
152    let mut completeness = CppExternalDeclarationCompleteness::Complete;
153    let mut parse_errors = Vec::new();
154    collect_parse_errors(tree.root_node(), &mut parse_errors);
155    if !parse_errors.is_empty() {
156        completeness = CppExternalDeclarationCompleteness::Partial;
157        diagnostics.push(CppExternalDeclarationDiagnostic {
158            code: "cpp.external.parse_error",
159            message: format!(
160                "external header `{}` has parse errors",
161                source_path.display()
162            ),
163        });
164    }
165    if has_unsupported_preprocessing(tree.root_node()) {
166        completeness = CppExternalDeclarationCompleteness::Partial;
167        diagnostics.push(CppExternalDeclarationDiagnostic {
168            code: "cpp.external.preprocessor_partial",
169            message: format!(
170                "external header `{}` has conditional or generated declarations",
171                source_path.display()
172            ),
173        });
174    }
175
176    let parsed = parse_cpp_file(&file, source, &tree);
177    let mut parent_by_child = HashMap::default();
178    for (parent, children) in &parsed.children {
179        for child in children {
180            parent_by_child.insert(child.clone(), parent.clone());
181        }
182    }
183
184    let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
185    declarations.sort_by_key(|declaration| {
186        (
187            declaration.fq_name(),
188            declaration.kind(),
189            declaration.signature().map(str::to_owned),
190        )
191    });
192
193    let mut types = Vec::new();
194    let mut members = Vec::new();
195    for declaration in declarations {
196        if types.len().saturating_add(members.len()) >= limits.max_records {
197            completeness = CppExternalDeclarationCompleteness::Partial;
198            diagnostics.push(CppExternalDeclarationDiagnostic {
199                code: "cpp.external.record_limit",
200                message: format!(
201                    "external header `{}` exceeded the declaration record limit",
202                    source_path.display()
203                ),
204            });
205            break;
206        }
207        match declaration.kind() {
208            CodeUnitType::Class => types.push(CppExternalType {
209                name: declaration.fq_name(),
210                source_name: cpp_name_for(&declaration),
211                visibility: parsed
212                    .ranges
213                    .get(&declaration)
214                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
215                    .map(|start| cpp_member_visibility(tree.root_node(), source, start))
216                    .unwrap_or(CppExternalVisibility::Private),
217                source_path: source_path.to_path_buf(),
218                direct_bases: parsed
219                    .raw_supertypes
220                    .get(&declaration)
221                    .cloned()
222                    .unwrap_or_default(),
223            }),
224            CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
225                let metadata = parsed
226                    .signature_metadata
227                    .get(&declaration)
228                    .and_then(|records| records.first());
229                members.push(CppExternalMember {
230                    owner: nearest_type_owner(&declaration, &parent_by_child),
231                    name: declaration.terminal_name().to_owned(),
232                    qualified_name: cpp_name_for(&declaration),
233                    kind: match declaration.kind() {
234                        CodeUnitType::Function => CppExternalMemberKind::Function,
235                        CodeUnitType::Field => CppExternalMemberKind::Field,
236                        CodeUnitType::Macro => CppExternalMemberKind::Macro,
237                        _ => unreachable!("the outer match admits exactly member kinds"),
238                    },
239                    visibility: parsed
240                        .ranges
241                        .get(&declaration)
242                        .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
243                        .map(|start| cpp_member_visibility(tree.root_node(), source, start))
244                        .unwrap_or(CppExternalVisibility::Private),
245                    is_constructor: metadata
246                        .is_some_and(|metadata| metadata.callable_is_constructor()),
247                    signature: declaration.signature().map(str::to_owned),
248                    parameter_types: metadata
249                        .and_then(|metadata| metadata.callable_parameter_types())
250                        .map(<[String]>::to_vec),
251                    return_type: metadata
252                        .and_then(|metadata| metadata.return_type_text())
253                        .map(str::to_owned),
254                    source_path: source_path.to_path_buf(),
255                });
256            }
257            CodeUnitType::Module | CodeUnitType::FileScope => {}
258        }
259    }
260
261    CppExternalDeclarationSet {
262        types,
263        members,
264        completeness,
265        diagnostics,
266    }
267}
268
269fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
270    let mut current = root.descendant_for_byte_range(start_byte, start_byte);
271    while let Some(node) = current {
272        let Some(parent) = node.parent() else {
273            break;
274        };
275        if parent.kind() == "field_declaration_list" {
276            let default = match parent.parent().map(|owner| owner.kind()) {
277                Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
278                _ => CppExternalVisibility::Private,
279            };
280            let mut visibility = default;
281            let mut cursor = parent.walk();
282            for child in parent.named_children(&mut cursor) {
283                if child.start_byte() > start_byte {
284                    break;
285                }
286                if child.kind() == "access_specifier" {
287                    visibility = match node_text(child, source).trim_end_matches(':').trim() {
288                        "public" => CppExternalVisibility::Public,
289                        "protected" => CppExternalVisibility::Protected,
290                        "private" => CppExternalVisibility::Private,
291                        _ => CppExternalVisibility::Private,
292                    };
293                }
294            }
295            return visibility;
296        }
297        current = Some(parent);
298    }
299    CppExternalVisibility::Public
300}
301
302fn nearest_type_owner(
303    declaration: &CodeUnit,
304    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
305) -> Option<String> {
306    let mut current = declaration;
307    while let Some(parent) = parent_by_child.get(current) {
308        if parent.kind() == CodeUnitType::Class {
309            return Some(parent.fq_name());
310        }
311        current = parent;
312    }
313    None
314}
315
316fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
317    let mut stack = vec![root];
318    while let Some(node) = stack.pop() {
319        if matches!(
320            node.kind(),
321            "preproc_def"
322                | "preproc_function_def"
323                | "preproc_if"
324                | "preproc_ifdef"
325                | "preproc_ifndef"
326                | "preproc_elif"
327                | "preproc_else"
328        ) {
329            return true;
330        }
331        let mut cursor = node.walk();
332        stack.extend(node.named_children(&mut cursor));
333    }
334    false
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    fn extract(source: &str) -> CppExternalDeclarationSet {
342        let temp = tempfile::tempdir().expect("temp root");
343        extract_external_declarations(
344            temp.path(),
345            Path::new("vector"),
346            source,
347            CppExternalDeclarationLimits::default(),
348        )
349    }
350
351    #[test]
352    fn extracts_only_literal_angle_include_paths() {
353        assert_eq!(
354            vec![PathBuf::from("vector")],
355            external_angle_include_paths(
356                "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n"
357            )
358        );
359    }
360
361    #[test]
362    fn extracts_namespaced_template_type_and_owned_members() {
363        let declarations = extract(
364            r#"
365            namespace std {
366            template <typename T> class vector : public sequence<T> {
367            public:
368                vector();
369                void push_back(const T& value);
370                T size;
371            };
372            }
373            "#,
374        );
375
376        assert_eq!(
377            CppExternalDeclarationCompleteness::Complete,
378            declarations.completeness
379        );
380        assert!(
381            declarations.types.iter().any(|record| {
382                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
383            }),
384            "{declarations:#?}"
385        );
386        assert!(
387            declarations.members.iter().any(|record| {
388                record.owner.as_deref() == Some("std.vector")
389                    && record.name == "push_back"
390                    && record.visibility == CppExternalVisibility::Public
391            }),
392            "{declarations:#?}"
393        );
394        assert!(
395            declarations.members.iter().any(|record| {
396                record.owner.as_deref() == Some("std.vector") && record.name == "size"
397            }),
398            "{declarations:#?}"
399        );
400    }
401
402    #[test]
403    fn keeps_same_short_names_under_distinct_owners() {
404        let declarations = extract(
405            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
406        );
407        let mut owners = declarations
408            .members
409            .iter()
410            .filter(|member| member.name == "add")
411            .filter_map(|member| member.owner.clone())
412            .collect::<Vec<_>>();
413        owners.sort();
414
415        assert_eq!(vec!["first.box", "second.box"], owners);
416        assert!(
417            declarations
418                .members
419                .iter()
420                .filter(|member| member.name == "add")
421                .all(|member| member.visibility == CppExternalVisibility::Private)
422        );
423    }
424
425    #[test]
426    fn nested_type_visibility_follows_the_enclosing_access_section() {
427        let declarations = extract(
428            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
429        );
430
431        assert!(declarations.types.iter().any(|record| {
432            record.name == "Outer.Hidden" && record.visibility == CppExternalVisibility::Private
433        }));
434        assert!(declarations.types.iter().any(|record| {
435            record.name == "Outer.Visible" && record.visibility == CppExternalVisibility::Public
436        }));
437        assert!(declarations.types.iter().any(|record| {
438            record.name == "Outer.Guarded" && record.visibility == CppExternalVisibility::Protected
439        }));
440    }
441
442    #[test]
443    fn preprocessor_and_record_limits_make_the_surface_partial() {
444        let temp = tempfile::tempdir().expect("temp root");
445        let declarations = extract_external_declarations(
446            temp.path(),
447            Path::new("limited.hpp"),
448            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
449            CppExternalDeclarationLimits { max_records: 1 },
450        );
451
452        assert_eq!(
453            CppExternalDeclarationCompleteness::Partial,
454            declarations.completeness
455        );
456        assert!(
457            declarations
458                .diagnostics
459                .iter()
460                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
461        );
462        assert!(
463            declarations
464                .diagnostics
465                .iter()
466                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
467        );
468    }
469}