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    CppComparableNode, CppComparableSlot, CppParameterType, cpp_active_template_type_parameter,
10    cpp_callable_parameter_names, cpp_callable_parameter_type_identities,
11    cpp_callable_return_type_identity, cpp_comparable_parameter_shapes, cpp_function_declarator_at,
12    cpp_macro_swallowed_access_label, node_text,
13};
14use crate::graph::resolver::cpp_name_for;
15use crate::structural::is_cpp_preprocessor_conditional;
16use brokk_bifrost_core::analyzer::ProjectFile;
17use brokk_bifrost_core::analyzer::model::{
18    CallableArity, CodeUnit, CodeUnitType, CppTemplateMetadata, SignatureMetadata,
19    StructuredTypeIdentity, StructuredTypeNodeView,
20};
21use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, collect_parse_errors};
22use brokk_bifrost_core::hash::HashMap;
23use std::path::{Path, PathBuf};
24use tree_sitter::{Node, Parser};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct CppExternalDeclarationLimits {
28    pub max_records: usize,
29}
30
31impl Default for CppExternalDeclarationLimits {
32    fn default() -> Self {
33        Self {
34            max_records: 250_000,
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum CppExternalDeclarationCompleteness {
41    Complete,
42    Partial,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CppExternalMemberKind {
47    Function,
48    Field,
49    Macro,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum CppExternalVisibility {
54    Public,
55    Protected,
56    Private,
57}
58
59/// What a reached external declaration set indexed at one name.
60///
61/// A namespace is kept because it is the declaration owner of every free
62/// function written inside it. Dropping it left those declarations owner-less,
63/// and a consumer that publishes a declaration model has no place to put a
64/// member whose owner it never saw.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum CppExternalTypeKind {
67    Class,
68    Namespace,
69    TypeAlias,
70}
71
72/// The nearest indexed declaration that scopes one external member.
73///
74/// A type owner and a namespace owner are deliberately distinct: only a type
75/// owner gives its members a receiver and value semantics, while a namespace
76/// scopes a declaration without either.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum CppExternalOwner {
79    Type(String),
80    Namespace(String),
81}
82
83impl CppExternalOwner {
84    pub fn name(&self) -> &str {
85        match self {
86            Self::Type(name) | Self::Namespace(name) => name,
87        }
88    }
89
90    pub fn type_name(&self) -> Option<&str> {
91        match self {
92            Self::Type(name) => Some(name),
93            Self::Namespace(_) => None,
94        }
95    }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct CppExternalType {
100    pub name: String,
101    pub source_name: String,
102    pub kind: CppExternalTypeKind,
103    pub underlying_type: Option<StructuredTypeIdentity>,
104    /// Structured template declaration metadata, when the class declaration
105    /// was reached through a template declaration.  Keeping this alongside
106    /// the neutral record lets a consumer prove a primary template's exact
107    /// arity/defaults without re-reading source text.
108    pub template_metadata: Option<CppTemplateMetadata>,
109    pub visibility: CppExternalVisibility,
110    pub source_path: PathBuf,
111    pub direct_bases: Vec<String>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct CppExternalMember {
116    pub owner: Option<CppExternalOwner>,
117    pub name: String,
118    pub qualified_name: String,
119    pub kind: CppExternalMemberKind,
120    pub visibility: CppExternalVisibility,
121    pub is_constructor: bool,
122    pub signature: Option<String>,
123    /// The structured type of each invocation parameter, or `None` when the
124    /// declaration is not a callable or its parameter list was not reached.
125    ///
126    /// These are structured identities rather than rendered spellings because
127    /// the consumer publishes them into a type model, and a spelling such as
128    /// `const T&` is a source text, not a type name.
129    pub parameter_types: Option<Vec<CppParameterType>>,
130    /// Parser-derived parameter shapes retaining C++ cv-qualification.
131    pub parameter_shapes: Option<Vec<CppComparableSlot>>,
132    /// Invocation arity, including default and repeated parameters, when the
133    /// declaration is callable and its parameter list was reached.
134    pub callable_arity: Option<CallableArity>,
135    /// The written name of each invocation parameter, when one exists. The
136    /// positions match `parameter_types`; unnamed C parameters remain `None`.
137    pub parameter_names: Option<Vec<Option<String>>>,
138    /// Whether a constructor is available to implicit conversion.
139    pub explicitness: Option<CppCallableExplicitness>,
140    pub return_type: Option<StructuredTypeIdentity>,
141    /// The sole forwarding-reference parameter's type is returned as an
142    /// rvalue reference, directly or through `remove_reference<T>::type`.
143    /// This preserves a declaration relationship, not a function-body claim.
144    pub returns_forwarding_reference: bool,
145    pub source_path: PathBuf,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum CppCallableExplicitness {
150    Implicit,
151    Explicit,
152    Conditional,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct CppExternalDeclarationDiagnostic {
157    pub code: &'static str,
158    pub message: String,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct CppExternalDeclarationSet {
163    pub types: Vec<CppExternalType>,
164    pub members: Vec<CppExternalMember>,
165    pub completeness: CppExternalDeclarationCompleteness,
166    /// Whether every declaration the header's own syntax states was extracted:
167    /// the parse needed no error recovery and no record was dropped by the
168    /// limit.
169    ///
170    /// This is narrower than `completeness`, which also turns partial for any
171    /// preprocessing. Conditional compilation does not hide a written
172    /// declaration from this extractor: every branch is parsed and extracted,
173    /// and a macro definition is extracted under its own name. A declaration
174    /// that only a macro expansion writes is not in the header's syntax at all
175    /// and is outside what this reports.
176    pub syntax_complete: bool,
177    pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
178}
179
180/// Return literal include paths and their delimiter kind from one C++ source.
181///
182/// The boolean is `true` for a quoted include and `false` for an angle include.
183/// Computed and conditional includes are not external-root evidence; pack
184/// completeness records preprocessor uncertainty separately.
185pub fn external_include_paths(source: &str) -> Vec<(PathBuf, bool)> {
186    let mut parser = Parser::new();
187    parser
188        .set_language(&tree_sitter_cpp::LANGUAGE.into())
189        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
190    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
191    external_include_paths_from_root(source, tree.root_node())
192}
193
194/// Return literal include paths and their delimiter kind from an existing C++
195/// syntax tree.
196pub fn external_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<(PathBuf, bool)> {
197    let mut paths = Vec::new();
198    let mut stack = vec![root];
199    while let Some(node) = stack.pop() {
200        if node.kind() == "preproc_include"
201            && !has_conditional_preprocessor_ancestor(node)
202            && let Some(path) = node.child_by_field_name("path")
203            && let Some((path, quoted)) = match path.kind() {
204                "string_literal" => node_text(path, source)
205                    .strip_prefix('"')
206                    .and_then(|path| path.strip_suffix('"'))
207                    .filter(|path| !path.is_empty())
208                    .map(|path| (PathBuf::from(path), true)),
209                "system_lib_string" => node_text(path, source)
210                    .strip_prefix('<')
211                    .and_then(|path| path.strip_suffix('>'))
212                    .filter(|path| !path.is_empty())
213                    .map(|path| (PathBuf::from(path), false)),
214                _ => None,
215            }
216        {
217            paths.push((path, quoted));
218        }
219        let mut cursor = node.walk();
220        stack.extend(node.named_children(&mut cursor));
221    }
222    paths.sort();
223    paths.dedup();
224    paths
225}
226
227/// Return the literal paths from angle-bracket includes in one C++ source.
228///
229/// Quoted includes are intentionally omitted from this compatibility helper;
230/// callers that resolve compiler search semantics should use
231/// [`external_include_paths`] instead.
232pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
233    external_include_paths(source)
234        .into_iter()
235        .filter_map(|(path, quoted)| (!quoted).then_some(path))
236        .collect()
237}
238
239/// Return literal unconditional angle includes from an existing C++ syntax tree.
240///
241/// The source and root must describe the same immutable snapshot. Analyzer
242/// callers use this form to reuse prepared workspace syntax; standalone
243/// external headers use [`external_angle_include_paths`].
244pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
245    external_include_paths_from_root(source, root)
246        .into_iter()
247        .filter_map(|(path, quoted)| (!quoted).then_some(path))
248        .collect()
249}
250
251fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
252    while let Some(parent) = node.parent() {
253        if is_cpp_preprocessor_conditional(parent) {
254            return true;
255        }
256        node = parent;
257    }
258    false
259}
260
261/// Extract declarations from one exact header source entry.
262///
263/// `source_path` is relative to `source_set_root`. The caller owns source-set
264/// containment, byte limits, cancellation, and stable hashing. This function
265/// owns only C++ syntax interpretation and its record limit.
266pub fn extract_external_declarations(
267    source_set_root: &Path,
268    source_path: &Path,
269    source: &str,
270    limits: CppExternalDeclarationLimits,
271) -> CppExternalDeclarationSet {
272    let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
273    let mut parser = Parser::new();
274    parser
275        .set_language(&tree_sitter_cpp::LANGUAGE.into())
276        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
277    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
278
279    let mut diagnostics = Vec::new();
280    let mut completeness = CppExternalDeclarationCompleteness::Complete;
281    let mut parse_errors = Vec::new();
282    collect_parse_errors(tree.root_node(), &mut parse_errors);
283    if !parse_errors.is_empty() {
284        completeness = CppExternalDeclarationCompleteness::Partial;
285        diagnostics.push(CppExternalDeclarationDiagnostic {
286            code: "cpp.external.parse_error",
287            message: format!(
288                "external header `{}` has parse errors",
289                source_path.display()
290            ),
291        });
292    }
293    if has_unsupported_preprocessing(tree.root_node()) {
294        completeness = CppExternalDeclarationCompleteness::Partial;
295        diagnostics.push(CppExternalDeclarationDiagnostic {
296            code: "cpp.external.preprocessor_partial",
297            message: format!(
298                "external header `{}` has conditional or generated declarations",
299                source_path.display()
300            ),
301        });
302    }
303
304    let mut syntax_complete = parse_errors.is_empty();
305
306    let parsed = parse_cpp_file(&file, source, &tree);
307    let mut parent_by_child = HashMap::default();
308    for (parent, children) in &parsed.children {
309        for child in children {
310            parent_by_child.insert(child.clone(), parent.clone());
311        }
312    }
313
314    let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
315    declarations.sort_by_key(|declaration| {
316        (
317            declaration.fq_name(),
318            declaration.kind(),
319            declaration.signature().map(str::to_owned),
320        )
321    });
322
323    // One index for the file's tree rather than one ancestor walk per
324    // declaration: a large generated header has tens of thousands of them.
325    let ancestry = ParentIndex::new(tree.root_node());
326    let mut types = Vec::new();
327    let mut members = Vec::new();
328    for declaration in declarations {
329        if types.len().saturating_add(members.len()) >= limits.max_records {
330            completeness = CppExternalDeclarationCompleteness::Partial;
331            syntax_complete = false;
332            diagnostics.push(CppExternalDeclarationDiagnostic {
333                code: "cpp.external.record_limit",
334                message: format!(
335                    "external header `{}` exceeded the declaration record limit",
336                    source_path.display()
337                ),
338            });
339            break;
340        }
341        match declaration.kind() {
342            CodeUnitType::Module => types.push(CppExternalType {
343                name: declaration.fq_name(),
344                source_name: cpp_name_for(&declaration),
345                kind: CppExternalTypeKind::Namespace,
346                underlying_type: None,
347                template_metadata: None,
348                visibility: CppExternalVisibility::Public,
349                source_path: source_path.to_path_buf(),
350                direct_bases: Vec::new(),
351            }),
352            CodeUnitType::Class => types.push(CppExternalType {
353                name: declaration.fq_name(),
354                source_name: cpp_name_for(&declaration),
355                kind: if parsed.type_aliases.contains(&declaration) {
356                    CppExternalTypeKind::TypeAlias
357                } else {
358                    CppExternalTypeKind::Class
359                },
360                underlying_type: parsed
361                    .signature_metadata
362                    .get(&declaration)
363                    .and_then(|records| records.first())
364                    .and_then(|metadata| metadata.underlying_type_identity())
365                    .cloned(),
366                template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
367                visibility: parsed
368                    .ranges
369                    .get(&declaration)
370                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
371                    .map(|start| cpp_member_visibility(tree.root_node(), source, start))
372                    .unwrap_or(CppExternalVisibility::Private),
373                source_path: source_path.to_path_buf(),
374                direct_bases: parsed
375                    .raw_supertypes
376                    .get(&declaration)
377                    .cloned()
378                    .unwrap_or_default(),
379            }),
380            CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
381                let metadata = parsed
382                    .signature_metadata
383                    .get(&declaration)
384                    .and_then(|records| records.first());
385                let declaration_start = parsed
386                    .ranges
387                    .get(&declaration)
388                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
389                let function_declarator = (declaration.kind() == CodeUnitType::Function)
390                    .then(|| {
391                        declaration_start
392                            .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
393                    })
394                    .flatten();
395                let parameter_types = function_declarator.map(|declarator| {
396                    cpp_callable_parameter_type_identities(declarator, source, &ancestry)
397                });
398                let parameter_names = function_declarator.and_then(|declarator| {
399                    declarator
400                        .child_by_field_name("parameters")
401                        .map(|parameters| cpp_callable_parameter_names(parameters, source))
402                });
403                let parameter_shapes = function_declarator.map(|declarator| {
404                    cpp_comparable_parameter_shapes(declarator, source, &ancestry)
405                });
406                let return_type = metadata
407                    .and_then(|metadata| metadata.return_type_identity())
408                    .cloned()
409                    .or_else(|| {
410                        function_declarator.and_then(|declarator| {
411                            cpp_callable_return_type_identity(declarator, source, &ancestry)
412                        })
413                    });
414                let returns_forwarding_reference = function_declarator.is_some_and(|declarator| {
415                    cpp_returns_forwarding_reference(
416                        declarator,
417                        source,
418                        &ancestry,
419                        parameter_types.as_deref(),
420                        parameter_shapes.as_deref(),
421                        return_type.as_ref(),
422                    )
423                });
424                members.push(CppExternalMember {
425                    owner: nearest_declaration_owner(&declaration, &parent_by_child),
426                    name: declaration.terminal_name().to_owned(),
427                    qualified_name: cpp_name_for(&declaration),
428                    kind: match declaration.kind() {
429                        CodeUnitType::Function => CppExternalMemberKind::Function,
430                        CodeUnitType::Field => CppExternalMemberKind::Field,
431                        CodeUnitType::Macro => CppExternalMemberKind::Macro,
432                        _ => unreachable!("the outer match admits exactly member kinds"),
433                    },
434                    visibility: declaration_start
435                        .map(|start| cpp_member_visibility(tree.root_node(), source, start))
436                        .unwrap_or(CppExternalVisibility::Private),
437                    is_constructor: metadata
438                        .is_some_and(|metadata| metadata.callable_is_constructor()),
439                    signature: declaration.signature().map(str::to_owned),
440                    parameter_types,
441                    parameter_shapes,
442                    callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
443                    parameter_names,
444                    explicitness: function_declarator.and_then(cpp_callable_explicitness),
445                    return_type,
446                    returns_forwarding_reference,
447                    source_path: source_path.to_path_buf(),
448                });
449            }
450            CodeUnitType::FileScope => {}
451        }
452    }
453
454    CppExternalDeclarationSet {
455        types,
456        members,
457        completeness,
458        syntax_complete,
459        diagnostics,
460    }
461}
462
463fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
464    let mut current = root.descendant_for_byte_range(start_byte, start_byte);
465    while let Some(node) = current {
466        let Some(parent) = node.parent() else {
467            break;
468        };
469        if parent.kind() == "field_declaration_list" {
470            let default = match parent.parent().map(|owner| owner.kind()) {
471                Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
472                _ => CppExternalVisibility::Private,
473            };
474            let mut visibility = default;
475            let mut cursor = parent.walk();
476            for child in parent.named_children(&mut cursor) {
477                if child.start_byte() > start_byte {
478                    break;
479                }
480                let access = if child.kind() == "access_specifier" {
481                    Some(node_text(child, source).trim_end_matches(':').trim())
482                } else {
483                    cpp_macro_swallowed_access_label(child, source).map(|label| label.access)
484                };
485                if let Some(access) = access {
486                    visibility = match access {
487                        "public" => CppExternalVisibility::Public,
488                        "protected" => CppExternalVisibility::Protected,
489                        "private" => CppExternalVisibility::Private,
490                        _ => CppExternalVisibility::Private,
491                    };
492                }
493            }
494            return visibility;
495        }
496        current = Some(parent);
497    }
498    CppExternalVisibility::Public
499}
500
501fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
502    while !matches!(
503        declarator.kind(),
504        "declaration" | "field_declaration" | "function_definition"
505    ) {
506        declarator = declarator.parent()?;
507    }
508    if declarator.has_error() {
509        return None;
510    }
511    let mut stack = vec![declarator];
512    let mut explicit = None;
513    while let Some(node) = stack.pop() {
514        if node.kind() == "explicit_function_specifier" {
515            if explicit.is_some() {
516                return None;
517            }
518            explicit = Some(if node.named_child_count() == 0 {
519                CppCallableExplicitness::Explicit
520            } else {
521                CppCallableExplicitness::Conditional
522            });
523            continue;
524        }
525        let mut cursor = node.walk();
526        stack.extend(node.named_children(&mut cursor));
527    }
528    Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
529}
530
531fn cpp_returns_forwarding_reference<'tree>(
532    declarator: Node<'tree>,
533    source: &str,
534    ancestry: &ParentIndex<'tree>,
535    parameters: Option<&[CppParameterType]>,
536    shapes: Option<&[CppComparableSlot]>,
537    returns: Option<&StructuredTypeIdentity>,
538) -> bool {
539    let (
540        Some([CppParameterType::Structured(parameter)]),
541        Some([CppComparableSlot::Shape(shape)]),
542        Some(returns),
543    ) = (parameters, shapes, returns)
544    else {
545        return false;
546    };
547    let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
548    else {
549        return false;
550    };
551    let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
552        return false;
553    };
554    let [parameter_name] = name.path() else {
555        return false;
556    };
557    let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
558        return false;
559    };
560    if !matches!(
561        shape.node(*inner),
562        CppComparableNode::Named {
563            konst: false,
564            volatil: false,
565            ..
566        }
567    ) || name.is_absolute()
568        || !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
569        || !matches!(
570            returns.view(returns.root_id()),
571            Some(StructuredTypeNodeView::RvalueReference(_))
572        )
573    {
574        return false;
575    }
576
577    // Keep the dependent return's template argument. Its nominal structured
578    // name alone flattens `remove_reference<T>::type` and loses the `T`.
579    let mut current = declarator;
580    while let Some(parent) = ancestry.parent(current) {
581        if matches!(
582            parent.kind(),
583            "function_definition" | "declaration" | "field_declaration"
584        ) {
585            let mut cursor = parent.walk();
586            if parent.named_children(&mut cursor).any(|child| {
587                child.kind() == "type_qualifier"
588                    && matches!(node_text(child, source), "const" | "volatile")
589            }) {
590                return false;
591            }
592            return parent
593                .child_by_field_name("type")
594                .is_some_and(|return_node| {
595                    cpp_forwarded_return_type(return_node, source, parameter_name)
596                });
597        }
598        current = parent;
599    }
600    false
601}
602
603fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
604    let mut components = Vec::new();
605    let mut removals = 0;
606    let mut stack = vec![node];
607    while let Some(current) = stack.pop() {
608        match current.kind() {
609            "identifier" | "type_identifier" | "namespace_identifier" => {
610                components.push(node_text(current, source));
611            }
612            "dependent_type" => {
613                if current.named_child_count() != 1 {
614                    return false;
615                }
616                stack.push(current.named_child(0).expect("one named child"));
617            }
618            "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
619                let Some(name) = current.child_by_field_name("name") else {
620                    return false;
621                };
622                stack.push(name);
623                if let Some(scope) = current.child_by_field_name("scope") {
624                    stack.push(scope);
625                }
626            }
627            "template_type" => {
628                let (Some(name), Some(arguments)) = (
629                    current.child_by_field_name("name"),
630                    current.child_by_field_name("arguments"),
631                ) else {
632                    return false;
633                };
634                if !matches!(name.kind(), "identifier" | "type_identifier")
635                    || node_text(name, source) != "remove_reference"
636                    || arguments.named_child_count() != 1
637                {
638                    return false;
639                }
640                let mut argument = arguments.named_child(0).expect("one named argument");
641                if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
642                    argument = argument.named_child(0).expect("one named type");
643                }
644                if !matches!(argument.kind(), "identifier" | "type_identifier")
645                    || node_text(argument, source) != parameter
646                {
647                    return false;
648                }
649                removals += 1;
650                stack.push(name);
651            }
652            _ => return false,
653        }
654    }
655    (removals == 0 && components == [parameter])
656        || (removals == 1
657            && (components == ["std", "remove_reference", "type"]
658                || components == ["remove_reference", "type"]))
659}
660
661fn nearest_declaration_owner(
662    declaration: &CodeUnit,
663    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
664) -> Option<CppExternalOwner> {
665    let mut current = declaration;
666    while let Some(parent) = parent_by_child.get(current) {
667        match parent.kind() {
668            CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
669            CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
670            _ => {}
671        }
672        current = parent;
673    }
674    None
675}
676
677fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
678    let mut stack = vec![root];
679    while let Some(node) = stack.pop() {
680        if matches!(
681            node.kind(),
682            "preproc_def"
683                | "preproc_function_def"
684                | "preproc_if"
685                | "preproc_ifdef"
686                | "preproc_ifndef"
687                | "preproc_elif"
688                | "preproc_else"
689        ) {
690            return true;
691        }
692        let mut cursor = node.walk();
693        stack.extend(node.named_children(&mut cursor));
694    }
695    false
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use brokk_bifrost_core::analyzer::model::CallableArity;
702    use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
703
704    fn extract(source: &str) -> CppExternalDeclarationSet {
705        let temp = tempfile::tempdir().expect("temp root");
706        extract_external_declarations(
707            temp.path(),
708            Path::new("vector"),
709            source,
710            CppExternalDeclarationLimits::default(),
711        )
712    }
713
714    #[test]
715    fn extracts_literal_include_paths_with_delimiters() {
716        let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
717        assert_eq!(
718            vec![
719                (PathBuf::from("local.hpp"), true),
720                (PathBuf::from("vector"), false),
721            ],
722            external_include_paths(source)
723        );
724        assert_eq!(
725            vec![PathBuf::from("vector")],
726            external_angle_include_paths(source)
727        );
728        let mut parser = Parser::new();
729        parser
730            .set_language(&tree_sitter_cpp::LANGUAGE.into())
731            .expect("C++ grammar");
732        let tree = parser.parse(source, None).expect("tree");
733        assert_eq!(
734            vec![PathBuf::from("vector")],
735            external_angle_include_paths_from_root(source, tree.root_node())
736        );
737    }
738
739    #[test]
740    fn extracts_namespaced_template_type_and_owned_members() {
741        let declarations = extract(
742            r#"
743            namespace std {
744            template <typename T> class vector : public sequence<T> {
745            public:
746                vector();
747                void push_back(const T& value);
748                T size;
749            };
750            }
751            "#,
752        );
753
754        assert_eq!(
755            CppExternalDeclarationCompleteness::Complete,
756            declarations.completeness
757        );
758        assert!(
759            declarations.types.iter().any(|record| {
760                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
761            }),
762            "{declarations:#?}"
763        );
764        assert!(
765            declarations.members.iter().any(|record| {
766                record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
767                    && record.name == "push_back"
768                    && record.visibility == CppExternalVisibility::Public
769            }),
770            "{declarations:#?}"
771        );
772        assert!(
773            declarations.members.iter().any(|record| {
774                record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
775                    && record.name == "size"
776            }),
777            "{declarations:#?}"
778        );
779    }
780
781    #[test]
782    fn preserves_copy_and_move_reference_parameter_shapes() {
783        let source = r#"
784            class Widget {
785            public:
786                Widget(const Widget&);
787                Widget(Widget&&);
788            };
789        "#;
790        let declarations = extract(source);
791        let constructors = declarations
792            .members
793            .iter()
794            .filter(|member| {
795                member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
796                    && member.name == "Widget"
797            })
798            .collect::<Vec<_>>();
799
800        assert_eq!(constructors.len(), 2, "{declarations:#?}");
801        let shapes = constructors
802            .iter()
803            .map(|constructor| {
804                let parameters = constructor
805                    .parameter_types
806                    .as_ref()
807                    .expect("constructor parameters");
808                assert_eq!(parameters.len(), 1, "{constructor:#?}");
809                let CppParameterType::Structured(identity) = &parameters[0] else {
810                    panic!("constructor parameter shape: {constructor:#?}");
811                };
812                (
813                    identity.clone(),
814                    matches!(
815                        identity.view(identity.root_id()),
816                        Some(StructuredTypeNodeView::Reference(_))
817                    ),
818                    matches!(
819                        identity.view(identity.root_id()),
820                        Some(StructuredTypeNodeView::RvalueReference(_))
821                    ),
822                )
823            })
824            .collect::<Vec<_>>();
825
826        assert_ne!(shapes[0].0, shapes[1].0);
827        assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
828        assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
829        assert_eq!(declarations, extract(source));
830    }
831
832    #[test]
833    fn preserves_callable_arities_for_defaulted_and_required_parameters() {
834        let declarations = extract(
835            r#"
836            namespace std {
837            template <class C, class Alloc> class basic_string {
838            public:
839                basic_string(const C*, const Alloc& = Alloc());
840                basic_string(const C&, const Alloc&);
841            };
842            }
843            "#,
844        );
845        let constructors = declarations
846            .members
847            .iter()
848            .filter(|member| {
849                member.owner.as_ref().and_then(CppExternalOwner::type_name)
850                    == Some("std.basic_string")
851                    && member.name == "basic_string"
852            })
853            .collect::<Vec<_>>();
854
855        assert_eq!(constructors.len(), 2, "{declarations:#?}");
856        assert!(
857            constructors
858                .iter()
859                .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
860            "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
861        );
862        assert!(
863            constructors
864                .iter()
865                .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
866            "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
867        );
868    }
869
870    #[test]
871    fn preserves_callable_parameter_names() {
872        let declarations = extract("int connect(int fd, const char *address, unsigned length);\n");
873        let connect_candidates = declarations
874            .members
875            .iter()
876            .filter(|member| member.name == "connect")
877            .collect::<Vec<_>>();
878        let [connect] = connect_candidates.as_slice() else {
879            panic!("connect declaration: {declarations:#?}");
880        };
881        assert_eq!(
882            Some(vec![
883                Some("fd".to_owned()),
884                Some("address".to_owned()),
885                Some("length".to_owned()),
886            ]),
887            connect.parameter_names,
888            "formal names must remain aligned with the declaration's argument slots"
889        );
890    }
891
892    #[test]
893    fn preserves_callable_explicitness_for_implicit_binding() {
894        for (declaration, expected) in [
895            ("Widget(const char*);", CppCallableExplicitness::Implicit),
896            (
897                "explicit Widget(const char*);",
898                CppCallableExplicitness::Explicit,
899            ),
900            (
901                "explicit(true) Widget(const char*);",
902                CppCallableExplicitness::Conditional,
903            ),
904        ] {
905            let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
906            let member = declarations
907                .members
908                .first()
909                .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
910            assert_eq!(Some(expected), member.explicitness, "{member:#?}");
911        }
912    }
913
914    #[test]
915    fn preserves_callable_return_reference_shapes() {
916        let declarations = extract(
917            r#"
918            namespace std {
919            class basic_string {
920            public:
921                basic_string& operator=(const basic_string&);
922                basic_string&& operator=(basic_string&&);
923                void operator=(int);
924            };
925            }
926            "#,
927        );
928        let assignments = declarations
929            .members
930            .iter()
931            .filter(|member| member.name == "operator=")
932            .collect::<Vec<_>>();
933        assert_eq!(assignments.len(), 3, "{declarations:#?}");
934
935        let copy = assignments
936            .iter()
937            .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
938            .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
939        let Some(StructuredTypeNodeView::Reference(inner)) = copy
940            .return_type
941            .as_ref()
942            .and_then(|identity| identity.view(identity.root_id()))
943        else {
944            panic!("copy return type: {copy:#?}");
945        };
946        let Some(StructuredTypeNodeView::Named(name)) = copy
947            .return_type
948            .as_ref()
949            .and_then(|identity| identity.view(inner))
950        else {
951            panic!("copy return target: {copy:#?}");
952        };
953        assert_eq!(["basic_string"], name.path());
954        assert_eq!(["std", "basic_string"], name.lexical_scope());
955
956        let move_assignment = declarations
957            .members
958            .iter()
959            .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
960            .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
961        assert!(matches!(
962            move_assignment
963                .return_type
964                .as_ref()
965                .and_then(|identity| identity.view(identity.root_id())),
966            Some(StructuredTypeNodeView::RvalueReference(_))
967        ));
968
969        let void_assignment = declarations
970            .members
971            .iter()
972            .find(|member| member.signature.as_deref() == Some("(int)"))
973            .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
974        let Some(StructuredTypeNodeView::Named(name)) = void_assignment
975            .return_type
976            .as_ref()
977            .and_then(|identity| identity.view(identity.root_id()))
978        else {
979            panic!("void return type: {void_assignment:#?}");
980        };
981        assert_eq!(["void"], name.path());
982    }
983
984    #[test]
985    fn preserves_type_alias_underlying_structured_identity() {
986        let declarations = extract(
987            r#"
988            namespace std {
989            template<class C, class Traits, class Alloc> class basic_string;
990            using string = basic_string<char, char_traits<char>, allocator<char>>;
991            }
992            "#,
993        );
994        let alias = declarations
995            .types
996            .iter()
997            .find(|record| record.name == "std.string")
998            .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
999        assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
1000        let identity = alias
1001            .underlying_type
1002            .as_ref()
1003            .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
1004        let Some(StructuredTypeNodeView::Generic { base, arguments }) =
1005            identity.view(identity.root_id())
1006        else {
1007            panic!("generic basic_string alias target: {identity:#?}");
1008        };
1009        assert_eq!(3, arguments.len());
1010        let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
1011            panic!("named basic_string alias base: {identity:#?}");
1012        };
1013        assert_eq!(["basic_string"], name.path());
1014        assert_eq!(["std"], name.lexical_scope());
1015    }
1016
1017    #[test]
1018    fn keeps_same_short_names_under_distinct_owners() {
1019        let declarations = extract(
1020            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
1021        );
1022        let mut owners = declarations
1023            .members
1024            .iter()
1025            .filter(|member| member.name == "add")
1026            .filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
1027            .collect::<Vec<_>>();
1028        owners.sort();
1029
1030        assert_eq!(vec!["first.box", "second.box"], owners);
1031        assert!(
1032            declarations
1033                .members
1034                .iter()
1035                .filter(|member| member.name == "add")
1036                .all(|member| member.visibility == CppExternalVisibility::Private)
1037        );
1038    }
1039
1040    #[test]
1041    fn nested_type_visibility_follows_the_enclosing_access_section() {
1042        let declarations = extract(
1043            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
1044        );
1045        assert!(declarations.types.iter().any(|record| {
1046            record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
1047        }));
1048        assert!(declarations.types.iter().any(|record| {
1049            record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
1050        }));
1051        assert!(declarations.types.iter().any(|record| {
1052            record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
1053        }));
1054    }
1055
1056    #[test]
1057    fn preprocessor_and_record_limits_make_the_surface_partial() {
1058        let temp = tempfile::tempdir().expect("temp root");
1059        let declarations = extract_external_declarations(
1060            temp.path(),
1061            Path::new("limited.hpp"),
1062            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
1063            CppExternalDeclarationLimits { max_records: 1 },
1064        );
1065
1066        assert_eq!(
1067            CppExternalDeclarationCompleteness::Partial,
1068            declarations.completeness
1069        );
1070        assert!(
1071            declarations
1072                .diagnostics
1073                .iter()
1074                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
1075        );
1076        assert!(
1077            declarations
1078                .diagnostics
1079                .iter()
1080                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
1081        );
1082    }
1083}