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    external_angle_include_paths_from_root(source, tree.root_node())
99}
100
101/// Return literal unconditional angle includes from an existing C++ syntax tree.
102///
103/// The source and root must describe the same immutable snapshot. Analyzer
104/// callers use this form to reuse prepared workspace syntax; standalone
105/// external headers use [`external_angle_include_paths`].
106pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
107    let mut paths = Vec::new();
108    let mut stack = vec![root];
109    while let Some(node) = stack.pop() {
110        if node.kind() == "preproc_include"
111            && !has_conditional_preprocessor_ancestor(node)
112            && let Some(path) = node.child_by_field_name("path")
113            && path.kind() == "system_lib_string"
114            && let Some(path) = node_text(path, source)
115                .strip_prefix('<')
116                .and_then(|path| path.strip_suffix('>'))
117                .filter(|path| !path.is_empty())
118        {
119            paths.push(PathBuf::from(path));
120        }
121        let mut cursor = node.walk();
122        stack.extend(node.named_children(&mut cursor));
123    }
124    paths.sort();
125    paths.dedup();
126    paths
127}
128
129fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
130    while let Some(parent) = node.parent() {
131        if matches!(
132            parent.kind(),
133            "preproc_if" | "preproc_ifdef" | "preproc_ifndef" | "preproc_elif" | "preproc_else"
134        ) {
135            return true;
136        }
137        node = parent;
138    }
139    false
140}
141
142/// Extract declarations from one exact header source entry.
143///
144/// `source_path` is relative to `source_set_root`. The caller owns source-set
145/// containment, byte limits, cancellation, and stable hashing. This function
146/// owns only C++ syntax interpretation and its record limit.
147pub fn extract_external_declarations(
148    source_set_root: &Path,
149    source_path: &Path,
150    source: &str,
151    limits: CppExternalDeclarationLimits,
152) -> CppExternalDeclarationSet {
153    let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
154    let mut parser = Parser::new();
155    parser
156        .set_language(&tree_sitter_cpp::LANGUAGE.into())
157        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
158    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
159
160    let mut diagnostics = Vec::new();
161    let mut completeness = CppExternalDeclarationCompleteness::Complete;
162    let mut parse_errors = Vec::new();
163    collect_parse_errors(tree.root_node(), &mut parse_errors);
164    if !parse_errors.is_empty() {
165        completeness = CppExternalDeclarationCompleteness::Partial;
166        diagnostics.push(CppExternalDeclarationDiagnostic {
167            code: "cpp.external.parse_error",
168            message: format!(
169                "external header `{}` has parse errors",
170                source_path.display()
171            ),
172        });
173    }
174    if has_unsupported_preprocessing(tree.root_node()) {
175        completeness = CppExternalDeclarationCompleteness::Partial;
176        diagnostics.push(CppExternalDeclarationDiagnostic {
177            code: "cpp.external.preprocessor_partial",
178            message: format!(
179                "external header `{}` has conditional or generated declarations",
180                source_path.display()
181            ),
182        });
183    }
184
185    let parsed = parse_cpp_file(&file, source, &tree);
186    let mut parent_by_child = HashMap::default();
187    for (parent, children) in &parsed.children {
188        for child in children {
189            parent_by_child.insert(child.clone(), parent.clone());
190        }
191    }
192
193    let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
194    declarations.sort_by_key(|declaration| {
195        (
196            declaration.fq_name(),
197            declaration.kind(),
198            declaration.signature().map(str::to_owned),
199        )
200    });
201
202    let mut types = Vec::new();
203    let mut members = Vec::new();
204    for declaration in declarations {
205        if types.len().saturating_add(members.len()) >= limits.max_records {
206            completeness = CppExternalDeclarationCompleteness::Partial;
207            diagnostics.push(CppExternalDeclarationDiagnostic {
208                code: "cpp.external.record_limit",
209                message: format!(
210                    "external header `{}` exceeded the declaration record limit",
211                    source_path.display()
212                ),
213            });
214            break;
215        }
216        match declaration.kind() {
217            CodeUnitType::Class => types.push(CppExternalType {
218                name: declaration.fq_name(),
219                source_name: cpp_name_for(&declaration),
220                visibility: parsed
221                    .ranges
222                    .get(&declaration)
223                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
224                    .map(|start| cpp_member_visibility(tree.root_node(), source, start))
225                    .unwrap_or(CppExternalVisibility::Private),
226                source_path: source_path.to_path_buf(),
227                direct_bases: parsed
228                    .raw_supertypes
229                    .get(&declaration)
230                    .cloned()
231                    .unwrap_or_default(),
232            }),
233            CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
234                let metadata = parsed
235                    .signature_metadata
236                    .get(&declaration)
237                    .and_then(|records| records.first());
238                members.push(CppExternalMember {
239                    owner: nearest_type_owner(&declaration, &parent_by_child),
240                    name: declaration.terminal_name().to_owned(),
241                    qualified_name: cpp_name_for(&declaration),
242                    kind: match declaration.kind() {
243                        CodeUnitType::Function => CppExternalMemberKind::Function,
244                        CodeUnitType::Field => CppExternalMemberKind::Field,
245                        CodeUnitType::Macro => CppExternalMemberKind::Macro,
246                        _ => unreachable!("the outer match admits exactly member kinds"),
247                    },
248                    visibility: parsed
249                        .ranges
250                        .get(&declaration)
251                        .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
252                        .map(|start| cpp_member_visibility(tree.root_node(), source, start))
253                        .unwrap_or(CppExternalVisibility::Private),
254                    is_constructor: metadata
255                        .is_some_and(|metadata| metadata.callable_is_constructor()),
256                    signature: declaration.signature().map(str::to_owned),
257                    parameter_types: metadata
258                        .and_then(|metadata| metadata.callable_parameter_types())
259                        .map(<[String]>::to_vec),
260                    return_type: metadata
261                        .and_then(|metadata| metadata.return_type_text())
262                        .map(str::to_owned),
263                    source_path: source_path.to_path_buf(),
264                });
265            }
266            CodeUnitType::Module | CodeUnitType::FileScope => {}
267        }
268    }
269
270    CppExternalDeclarationSet {
271        types,
272        members,
273        completeness,
274        diagnostics,
275    }
276}
277
278fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
279    let mut current = root.descendant_for_byte_range(start_byte, start_byte);
280    while let Some(node) = current {
281        let Some(parent) = node.parent() else {
282            break;
283        };
284        if parent.kind() == "field_declaration_list" {
285            let default = match parent.parent().map(|owner| owner.kind()) {
286                Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
287                _ => CppExternalVisibility::Private,
288            };
289            let mut visibility = default;
290            let mut cursor = parent.walk();
291            for child in parent.named_children(&mut cursor) {
292                if child.start_byte() > start_byte {
293                    break;
294                }
295                if child.kind() == "access_specifier" {
296                    visibility = match node_text(child, source).trim_end_matches(':').trim() {
297                        "public" => CppExternalVisibility::Public,
298                        "protected" => CppExternalVisibility::Protected,
299                        "private" => CppExternalVisibility::Private,
300                        _ => CppExternalVisibility::Private,
301                    };
302                }
303            }
304            return visibility;
305        }
306        current = Some(parent);
307    }
308    CppExternalVisibility::Public
309}
310
311fn nearest_type_owner(
312    declaration: &CodeUnit,
313    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
314) -> Option<String> {
315    let mut current = declaration;
316    while let Some(parent) = parent_by_child.get(current) {
317        if parent.kind() == CodeUnitType::Class {
318            return Some(parent.fq_name());
319        }
320        current = parent;
321    }
322    None
323}
324
325fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
326    let mut stack = vec![root];
327    while let Some(node) = stack.pop() {
328        if matches!(
329            node.kind(),
330            "preproc_def"
331                | "preproc_function_def"
332                | "preproc_if"
333                | "preproc_ifdef"
334                | "preproc_ifndef"
335                | "preproc_elif"
336                | "preproc_else"
337        ) {
338            return true;
339        }
340        let mut cursor = node.walk();
341        stack.extend(node.named_children(&mut cursor));
342    }
343    false
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    fn extract(source: &str) -> CppExternalDeclarationSet {
351        let temp = tempfile::tempdir().expect("temp root");
352        extract_external_declarations(
353            temp.path(),
354            Path::new("vector"),
355            source,
356            CppExternalDeclarationLimits::default(),
357        )
358    }
359
360    #[test]
361    fn extracts_only_literal_angle_include_paths() {
362        let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
363        assert_eq!(
364            vec![PathBuf::from("vector")],
365            external_angle_include_paths(source)
366        );
367        let mut parser = Parser::new();
368        parser
369            .set_language(&tree_sitter_cpp::LANGUAGE.into())
370            .expect("C++ grammar");
371        let tree = parser.parse(source, None).expect("tree");
372        assert_eq!(
373            vec![PathBuf::from("vector")],
374            external_angle_include_paths_from_root(source, tree.root_node())
375        );
376    }
377
378    #[test]
379    fn extracts_namespaced_template_type_and_owned_members() {
380        let declarations = extract(
381            r#"
382            namespace std {
383            template <typename T> class vector : public sequence<T> {
384            public:
385                vector();
386                void push_back(const T& value);
387                T size;
388            };
389            }
390            "#,
391        );
392
393        assert_eq!(
394            CppExternalDeclarationCompleteness::Complete,
395            declarations.completeness
396        );
397        assert!(
398            declarations.types.iter().any(|record| {
399                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
400            }),
401            "{declarations:#?}"
402        );
403        assert!(
404            declarations.members.iter().any(|record| {
405                record.owner.as_deref() == Some("std.vector")
406                    && record.name == "push_back"
407                    && record.visibility == CppExternalVisibility::Public
408            }),
409            "{declarations:#?}"
410        );
411        assert!(
412            declarations.members.iter().any(|record| {
413                record.owner.as_deref() == Some("std.vector") && record.name == "size"
414            }),
415            "{declarations:#?}"
416        );
417    }
418
419    #[test]
420    fn keeps_same_short_names_under_distinct_owners() {
421        let declarations = extract(
422            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
423        );
424        let mut owners = declarations
425            .members
426            .iter()
427            .filter(|member| member.name == "add")
428            .filter_map(|member| member.owner.clone())
429            .collect::<Vec<_>>();
430        owners.sort();
431
432        assert_eq!(vec!["first.box", "second.box"], owners);
433        assert!(
434            declarations
435                .members
436                .iter()
437                .filter(|member| member.name == "add")
438                .all(|member| member.visibility == CppExternalVisibility::Private)
439        );
440    }
441
442    #[test]
443    fn nested_type_visibility_follows_the_enclosing_access_section() {
444        let declarations = extract(
445            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
446        );
447        assert!(declarations.types.iter().any(|record| {
448            record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
449        }));
450        assert!(declarations.types.iter().any(|record| {
451            record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
452        }));
453        assert!(declarations.types.iter().any(|record| {
454            record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
455        }));
456    }
457
458    #[test]
459    fn preprocessor_and_record_limits_make_the_surface_partial() {
460        let temp = tempfile::tempdir().expect("temp root");
461        let declarations = extract_external_declarations(
462            temp.path(),
463            Path::new("limited.hpp"),
464            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
465            CppExternalDeclarationLimits { max_records: 1 },
466        );
467
468        assert_eq!(
469            CppExternalDeclarationCompleteness::Partial,
470            declarations.completeness
471        );
472        assert!(
473            declarations
474                .diagnostics
475                .iter()
476                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
477        );
478        assert!(
479            declarations
480                .diagnostics
481                .iter()
482                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
483        );
484    }
485}