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::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    let mut types = Vec::new();
211    let mut members = Vec::new();
212    for declaration in declarations {
213        if types.len().saturating_add(members.len()) >= limits.max_records {
214            completeness = CppExternalDeclarationCompleteness::Partial;
215            diagnostics.push(CppExternalDeclarationDiagnostic {
216                code: "cpp.external.record_limit",
217                message: format!(
218                    "external header `{}` exceeded the declaration record limit",
219                    source_path.display()
220                ),
221            });
222            break;
223        }
224        match declaration.kind() {
225            CodeUnitType::Class => types.push(CppExternalType {
226                name: declaration.fq_name(),
227                source_name: cpp_name_for(&declaration),
228                visibility: parsed
229                    .ranges
230                    .get(&declaration)
231                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
232                    .map(|start| cpp_member_visibility(tree.root_node(), source, start))
233                    .unwrap_or(CppExternalVisibility::Private),
234                source_path: source_path.to_path_buf(),
235                direct_bases: parsed
236                    .raw_supertypes
237                    .get(&declaration)
238                    .cloned()
239                    .unwrap_or_default(),
240            }),
241            CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
242                let metadata = parsed
243                    .signature_metadata
244                    .get(&declaration)
245                    .and_then(|records| records.first());
246                let declaration_start = parsed
247                    .ranges
248                    .get(&declaration)
249                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
250                let parameter_types = (declaration.kind() == CodeUnitType::Function)
251                    .then(|| {
252                        declaration_start
253                            .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
254                            .map(|declarator| {
255                                cpp_callable_parameter_type_identities(declarator, source)
256                            })
257                    })
258                    .flatten();
259                members.push(CppExternalMember {
260                    owner: nearest_type_owner(&declaration, &parent_by_child),
261                    name: declaration.terminal_name().to_owned(),
262                    qualified_name: cpp_name_for(&declaration),
263                    kind: match declaration.kind() {
264                        CodeUnitType::Function => CppExternalMemberKind::Function,
265                        CodeUnitType::Field => CppExternalMemberKind::Field,
266                        CodeUnitType::Macro => CppExternalMemberKind::Macro,
267                        _ => unreachable!("the outer match admits exactly member kinds"),
268                    },
269                    visibility: declaration_start
270                        .map(|start| cpp_member_visibility(tree.root_node(), source, start))
271                        .unwrap_or(CppExternalVisibility::Private),
272                    is_constructor: metadata
273                        .is_some_and(|metadata| metadata.callable_is_constructor()),
274                    signature: declaration.signature().map(str::to_owned),
275                    parameter_types,
276                    return_type: metadata
277                        .and_then(|metadata| metadata.return_type_identity())
278                        .cloned(),
279                    source_path: source_path.to_path_buf(),
280                });
281            }
282            CodeUnitType::Module | CodeUnitType::FileScope => {}
283        }
284    }
285
286    CppExternalDeclarationSet {
287        types,
288        members,
289        completeness,
290        diagnostics,
291    }
292}
293
294fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
295    let mut current = root.descendant_for_byte_range(start_byte, start_byte);
296    while let Some(node) = current {
297        let Some(parent) = node.parent() else {
298            break;
299        };
300        if parent.kind() == "field_declaration_list" {
301            let default = match parent.parent().map(|owner| owner.kind()) {
302                Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
303                _ => CppExternalVisibility::Private,
304            };
305            let mut visibility = default;
306            let mut cursor = parent.walk();
307            for child in parent.named_children(&mut cursor) {
308                if child.start_byte() > start_byte {
309                    break;
310                }
311                if child.kind() == "access_specifier" {
312                    visibility = match node_text(child, source).trim_end_matches(':').trim() {
313                        "public" => CppExternalVisibility::Public,
314                        "protected" => CppExternalVisibility::Protected,
315                        "private" => CppExternalVisibility::Private,
316                        _ => CppExternalVisibility::Private,
317                    };
318                }
319            }
320            return visibility;
321        }
322        current = Some(parent);
323    }
324    CppExternalVisibility::Public
325}
326
327fn nearest_type_owner(
328    declaration: &CodeUnit,
329    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
330) -> Option<String> {
331    let mut current = declaration;
332    while let Some(parent) = parent_by_child.get(current) {
333        if parent.kind() == CodeUnitType::Class {
334            return Some(parent.fq_name());
335        }
336        current = parent;
337    }
338    None
339}
340
341fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
342    let mut stack = vec![root];
343    while let Some(node) = stack.pop() {
344        if matches!(
345            node.kind(),
346            "preproc_def"
347                | "preproc_function_def"
348                | "preproc_if"
349                | "preproc_ifdef"
350                | "preproc_ifndef"
351                | "preproc_elif"
352                | "preproc_else"
353        ) {
354            return true;
355        }
356        let mut cursor = node.walk();
357        stack.extend(node.named_children(&mut cursor));
358    }
359    false
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    fn extract(source: &str) -> CppExternalDeclarationSet {
367        let temp = tempfile::tempdir().expect("temp root");
368        extract_external_declarations(
369            temp.path(),
370            Path::new("vector"),
371            source,
372            CppExternalDeclarationLimits::default(),
373        )
374    }
375
376    #[test]
377    fn extracts_only_literal_angle_include_paths() {
378        let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
379        assert_eq!(
380            vec![PathBuf::from("vector")],
381            external_angle_include_paths(source)
382        );
383        let mut parser = Parser::new();
384        parser
385            .set_language(&tree_sitter_cpp::LANGUAGE.into())
386            .expect("C++ grammar");
387        let tree = parser.parse(source, None).expect("tree");
388        assert_eq!(
389            vec![PathBuf::from("vector")],
390            external_angle_include_paths_from_root(source, tree.root_node())
391        );
392    }
393
394    #[test]
395    fn extracts_namespaced_template_type_and_owned_members() {
396        let declarations = extract(
397            r#"
398            namespace std {
399            template <typename T> class vector : public sequence<T> {
400            public:
401                vector();
402                void push_back(const T& value);
403                T size;
404            };
405            }
406            "#,
407        );
408
409        assert_eq!(
410            CppExternalDeclarationCompleteness::Complete,
411            declarations.completeness
412        );
413        assert!(
414            declarations.types.iter().any(|record| {
415                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
416            }),
417            "{declarations:#?}"
418        );
419        assert!(
420            declarations.members.iter().any(|record| {
421                record.owner.as_deref() == Some("std.vector")
422                    && record.name == "push_back"
423                    && record.visibility == CppExternalVisibility::Public
424            }),
425            "{declarations:#?}"
426        );
427        assert!(
428            declarations.members.iter().any(|record| {
429                record.owner.as_deref() == Some("std.vector") && record.name == "size"
430            }),
431            "{declarations:#?}"
432        );
433    }
434
435    #[test]
436    fn keeps_same_short_names_under_distinct_owners() {
437        let declarations = extract(
438            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
439        );
440        let mut owners = declarations
441            .members
442            .iter()
443            .filter(|member| member.name == "add")
444            .filter_map(|member| member.owner.clone())
445            .collect::<Vec<_>>();
446        owners.sort();
447
448        assert_eq!(vec!["first.box", "second.box"], owners);
449        assert!(
450            declarations
451                .members
452                .iter()
453                .filter(|member| member.name == "add")
454                .all(|member| member.visibility == CppExternalVisibility::Private)
455        );
456    }
457
458    #[test]
459    fn nested_type_visibility_follows_the_enclosing_access_section() {
460        let declarations = extract(
461            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
462        );
463        assert!(declarations.types.iter().any(|record| {
464            record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
465        }));
466        assert!(declarations.types.iter().any(|record| {
467            record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
468        }));
469        assert!(declarations.types.iter().any(|record| {
470            record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
471        }));
472    }
473
474    #[test]
475    fn preprocessor_and_record_limits_make_the_surface_partial() {
476        let temp = tempfile::tempdir().expect("temp root");
477        let declarations = extract_external_declarations(
478            temp.path(),
479            Path::new("limited.hpp"),
480            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
481            CppExternalDeclarationLimits { max_records: 1 },
482        );
483
484        assert_eq!(
485            CppExternalDeclarationCompleteness::Partial,
486            declarations.completeness
487        );
488        assert!(
489            declarations
490                .diagnostics
491                .iter()
492                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
493        );
494        assert!(
495            declarations
496                .diagnostics
497                .iter()
498                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
499        );
500    }
501}