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