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            // An out-of-line member has both a lexical namespace placement
311            // and a class declaration edge. Its semantic owner is the class,
312            // regardless of HashMap iteration order over those placements.
313            parent_by_child
314                .entry(child.clone())
315                .and_modify(|owner: &mut CodeUnit| {
316                    if parent.is_class() && !owner.is_class() {
317                        *owner = parent.clone();
318                    }
319                })
320                .or_insert_with(|| parent.clone());
321        }
322    }
323
324    let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
325    declarations.sort_by_key(|declaration| {
326        (
327            declaration.fq_name(),
328            declaration.kind(),
329            declaration.signature().map(str::to_owned),
330        )
331    });
332
333    // One index for the file's tree rather than one ancestor walk per
334    // declaration: a large generated header has tens of thousands of them.
335    let ancestry = ParentIndex::new(tree.root_node());
336    let mut types = Vec::new();
337    let mut members = Vec::new();
338    for declaration in declarations {
339        if types.len().saturating_add(members.len()) >= limits.max_records {
340            completeness = CppExternalDeclarationCompleteness::Partial;
341            syntax_complete = false;
342            diagnostics.push(CppExternalDeclarationDiagnostic {
343                code: "cpp.external.record_limit",
344                message: format!(
345                    "external header `{}` exceeded the declaration record limit",
346                    source_path.display()
347                ),
348            });
349            break;
350        }
351        match declaration.kind() {
352            CodeUnitType::Module => types.push(CppExternalType {
353                name: declaration.fq_name(),
354                source_name: cpp_name_for(&declaration),
355                kind: CppExternalTypeKind::Namespace,
356                underlying_type: None,
357                template_metadata: None,
358                visibility: CppExternalVisibility::Public,
359                source_path: source_path.to_path_buf(),
360                direct_bases: Vec::new(),
361            }),
362            CodeUnitType::Class => types.push(CppExternalType {
363                name: declaration.fq_name(),
364                source_name: cpp_name_for(&declaration),
365                kind: if parsed.type_aliases.contains(&declaration) {
366                    CppExternalTypeKind::TypeAlias
367                } else {
368                    CppExternalTypeKind::Class
369                },
370                underlying_type: parsed
371                    .signature_metadata
372                    .get(&declaration)
373                    .and_then(|records| records.first())
374                    .and_then(|metadata| metadata.underlying_type_identity())
375                    .cloned(),
376                template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
377                visibility: parsed
378                    .ranges
379                    .get(&declaration)
380                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
381                    .map(|start| cpp_member_visibility(tree.root_node(), source, start))
382                    .unwrap_or(CppExternalVisibility::Private),
383                source_path: source_path.to_path_buf(),
384                direct_bases: parsed
385                    .raw_supertypes
386                    .get(&declaration)
387                    .cloned()
388                    .unwrap_or_default(),
389            }),
390            CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
391                let metadata = parsed
392                    .signature_metadata
393                    .get(&declaration)
394                    .and_then(|records| records.first());
395                let declaration_start = parsed
396                    .ranges
397                    .get(&declaration)
398                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
399                let function_declarator = (declaration.kind() == CodeUnitType::Function)
400                    .then(|| {
401                        declaration_start
402                            .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
403                    })
404                    .flatten();
405                let parameter_types = function_declarator.map(|declarator| {
406                    cpp_callable_parameter_type_identities(declarator, source, &ancestry)
407                });
408                let parameter_names = function_declarator.and_then(|declarator| {
409                    declarator
410                        .child_by_field_name("parameters")
411                        .map(|parameters| cpp_callable_parameter_names(parameters, source))
412                });
413                let parameter_shapes = function_declarator.map(|declarator| {
414                    cpp_comparable_parameter_shapes(declarator, source, &ancestry)
415                });
416                let return_type = metadata
417                    .and_then(|metadata| metadata.return_type_identity())
418                    .cloned()
419                    .or_else(|| {
420                        function_declarator.and_then(|declarator| {
421                            cpp_callable_return_type_identity(declarator, source, &ancestry)
422                        })
423                    });
424                let returns_forwarding_reference = function_declarator.is_some_and(|declarator| {
425                    cpp_returns_forwarding_reference(
426                        declarator,
427                        source,
428                        &ancestry,
429                        parameter_types.as_deref(),
430                        parameter_shapes.as_deref(),
431                        return_type.as_ref(),
432                    )
433                });
434                members.push(CppExternalMember {
435                    owner: nearest_declaration_owner(&declaration, &parent_by_child),
436                    name: declaration.terminal_name().to_owned(),
437                    qualified_name: cpp_name_for(&declaration),
438                    kind: match declaration.kind() {
439                        CodeUnitType::Function => CppExternalMemberKind::Function,
440                        CodeUnitType::Field => CppExternalMemberKind::Field,
441                        CodeUnitType::Macro => CppExternalMemberKind::Macro,
442                        _ => unreachable!("the outer match admits exactly member kinds"),
443                    },
444                    visibility: declaration_start
445                        .map(|start| cpp_member_visibility(tree.root_node(), source, start))
446                        .unwrap_or(CppExternalVisibility::Private),
447                    is_constructor: metadata
448                        .is_some_and(|metadata| metadata.callable_is_constructor()),
449                    signature: declaration.signature().map(str::to_owned),
450                    parameter_types,
451                    parameter_shapes,
452                    callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
453                    parameter_names,
454                    explicitness: function_declarator.and_then(cpp_callable_explicitness),
455                    return_type,
456                    returns_forwarding_reference,
457                    source_path: source_path.to_path_buf(),
458                });
459            }
460            CodeUnitType::FileScope => {}
461        }
462    }
463
464    CppExternalDeclarationSet {
465        types,
466        members,
467        completeness,
468        syntax_complete,
469        diagnostics,
470    }
471}
472
473fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
474    let mut current = root.descendant_for_byte_range(start_byte, start_byte);
475    while let Some(node) = current {
476        let Some(parent) = node.parent() else {
477            break;
478        };
479        if parent.kind() == "field_declaration_list" {
480            let default = match parent.parent().map(|owner| owner.kind()) {
481                Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
482                _ => CppExternalVisibility::Private,
483            };
484            let mut visibility = default;
485            let mut cursor = parent.walk();
486            for child in parent.named_children(&mut cursor) {
487                if child.start_byte() > start_byte {
488                    break;
489                }
490                let access = if child.kind() == "access_specifier" {
491                    Some(node_text(child, source).trim_end_matches(':').trim())
492                } else {
493                    cpp_macro_swallowed_access_label(child, source).map(|label| label.access)
494                };
495                if let Some(access) = access {
496                    visibility = match access {
497                        "public" => CppExternalVisibility::Public,
498                        "protected" => CppExternalVisibility::Protected,
499                        "private" => CppExternalVisibility::Private,
500                        _ => CppExternalVisibility::Private,
501                    };
502                }
503            }
504            return visibility;
505        }
506        current = Some(parent);
507    }
508    CppExternalVisibility::Public
509}
510
511fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
512    while !matches!(
513        declarator.kind(),
514        "declaration" | "field_declaration" | "function_definition"
515    ) {
516        declarator = declarator.parent()?;
517    }
518    if declarator.has_error() {
519        return None;
520    }
521    let mut stack = vec![declarator];
522    let mut explicit = None;
523    while let Some(node) = stack.pop() {
524        if node.kind() == "explicit_function_specifier" {
525            if explicit.is_some() {
526                return None;
527            }
528            explicit = Some(if node.named_child_count() == 0 {
529                CppCallableExplicitness::Explicit
530            } else {
531                CppCallableExplicitness::Conditional
532            });
533            continue;
534        }
535        let mut cursor = node.walk();
536        stack.extend(node.named_children(&mut cursor));
537    }
538    Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
539}
540
541fn cpp_returns_forwarding_reference<'tree>(
542    declarator: Node<'tree>,
543    source: &str,
544    ancestry: &ParentIndex<'tree>,
545    parameters: Option<&[CppParameterType]>,
546    shapes: Option<&[CppComparableSlot]>,
547    returns: Option<&StructuredTypeIdentity>,
548) -> bool {
549    let (
550        Some([CppParameterType::Structured(parameter)]),
551        Some([CppComparableSlot::Shape(shape)]),
552        Some(returns),
553    ) = (parameters, shapes, returns)
554    else {
555        return false;
556    };
557    let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
558    else {
559        return false;
560    };
561    let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
562        return false;
563    };
564    let [parameter_name] = name.path() else {
565        return false;
566    };
567    let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
568        return false;
569    };
570    if !matches!(
571        shape.node(*inner),
572        CppComparableNode::Named {
573            konst: false,
574            volatil: false,
575            ..
576        }
577    ) || name.is_absolute()
578        || !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
579        || !matches!(
580            returns.view(returns.root_id()),
581            Some(StructuredTypeNodeView::RvalueReference(_))
582        )
583    {
584        return false;
585    }
586
587    // Keep the dependent return's template argument. Its nominal structured
588    // name alone flattens `remove_reference<T>::type` and loses the `T`.
589    let mut current = declarator;
590    while let Some(parent) = ancestry.parent(current) {
591        if matches!(
592            parent.kind(),
593            "function_definition" | "declaration" | "field_declaration"
594        ) {
595            let mut cursor = parent.walk();
596            if parent.named_children(&mut cursor).any(|child| {
597                child.kind() == "type_qualifier"
598                    && matches!(node_text(child, source), "const" | "volatile")
599            }) {
600                return false;
601            }
602            return parent
603                .child_by_field_name("type")
604                .is_some_and(|return_node| {
605                    cpp_forwarded_return_type(return_node, source, parameter_name)
606                });
607        }
608        current = parent;
609    }
610    false
611}
612
613fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
614    let mut components = Vec::new();
615    let mut removals = 0;
616    let mut stack = vec![node];
617    while let Some(current) = stack.pop() {
618        match current.kind() {
619            "identifier" | "type_identifier" | "namespace_identifier" => {
620                components.push(node_text(current, source));
621            }
622            "dependent_type" => {
623                if current.named_child_count() != 1 {
624                    return false;
625                }
626                stack.push(current.named_child(0).expect("one named child"));
627            }
628            "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
629                let Some(name) = current.child_by_field_name("name") else {
630                    return false;
631                };
632                stack.push(name);
633                if let Some(scope) = current.child_by_field_name("scope") {
634                    stack.push(scope);
635                }
636            }
637            "template_type" => {
638                let (Some(name), Some(arguments)) = (
639                    current.child_by_field_name("name"),
640                    current.child_by_field_name("arguments"),
641                ) else {
642                    return false;
643                };
644                if !matches!(name.kind(), "identifier" | "type_identifier")
645                    || node_text(name, source) != "remove_reference"
646                    || arguments.named_child_count() != 1
647                {
648                    return false;
649                }
650                let mut argument = arguments.named_child(0).expect("one named argument");
651                if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
652                    argument = argument.named_child(0).expect("one named type");
653                }
654                if !matches!(argument.kind(), "identifier" | "type_identifier")
655                    || node_text(argument, source) != parameter
656                {
657                    return false;
658                }
659                removals += 1;
660                stack.push(name);
661            }
662            _ => return false,
663        }
664    }
665    (removals == 0 && components == [parameter])
666        || (removals == 1
667            && (components == ["std", "remove_reference", "type"]
668                || components == ["remove_reference", "type"]))
669}
670
671fn nearest_declaration_owner(
672    declaration: &CodeUnit,
673    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
674) -> Option<CppExternalOwner> {
675    let mut current = declaration;
676    while let Some(parent) = parent_by_child.get(current) {
677        match parent.kind() {
678            CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
679            CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
680            _ => {}
681        }
682        current = parent;
683    }
684    None
685}
686
687fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
688    let mut stack = vec![root];
689    while let Some(node) = stack.pop() {
690        if matches!(
691            node.kind(),
692            "preproc_def"
693                | "preproc_function_def"
694                | "preproc_if"
695                | "preproc_ifdef"
696                | "preproc_ifndef"
697                | "preproc_elif"
698                | "preproc_else"
699        ) {
700            return true;
701        }
702        let mut cursor = node.walk();
703        stack.extend(node.named_children(&mut cursor));
704    }
705    false
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711    use brokk_bifrost_core::analyzer::model::CallableArity;
712    use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
713    use brokk_bifrost_test_macros::hardwon;
714
715    fn extract(source: &str) -> CppExternalDeclarationSet {
716        let temp = tempfile::tempdir().expect("temp root");
717        extract_external_declarations(
718            temp.path(),
719            Path::new("vector"),
720            source,
721            CppExternalDeclarationLimits::default(),
722        )
723    }
724
725    #[test]
726    fn extracts_literal_include_paths_with_delimiters() {
727        let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
728        assert_eq!(
729            vec![
730                (PathBuf::from("local.hpp"), true),
731                (PathBuf::from("vector"), false),
732            ],
733            external_include_paths(source)
734        );
735        assert_eq!(
736            vec![PathBuf::from("vector")],
737            external_angle_include_paths(source)
738        );
739        let mut parser = Parser::new();
740        parser
741            .set_language(&tree_sitter_cpp::LANGUAGE.into())
742            .expect("C++ grammar");
743        let tree = parser.parse(source, None).expect("tree");
744        assert_eq!(
745            vec![PathBuf::from("vector")],
746            external_angle_include_paths_from_root(source, tree.root_node())
747        );
748    }
749
750    #[test]
751    fn extracts_namespaced_template_type_and_owned_members() {
752        let declarations = extract(
753            r#"
754            namespace std {
755            template <typename T> class vector : public sequence<T> {
756            public:
757                vector();
758                void push_back(const T& value);
759                T size;
760            };
761            }
762            "#,
763        );
764
765        assert_eq!(
766            CppExternalDeclarationCompleteness::Complete,
767            declarations.completeness
768        );
769        assert!(
770            declarations.types.iter().any(|record| {
771                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
772            }),
773            "{declarations:#?}"
774        );
775        assert!(
776            declarations.members.iter().any(|record| {
777                record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
778                    && record.name == "push_back"
779                    && record.visibility == CppExternalVisibility::Public
780            }),
781            "{declarations:#?}"
782        );
783        assert!(
784            declarations.members.iter().any(|record| {
785                record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
786                    && record.name == "size"
787            }),
788            "{declarations:#?}"
789        );
790    }
791
792    #[test]
793    fn preserves_copy_and_move_reference_parameter_shapes() {
794        let source = r#"
795            class Widget {
796            public:
797                Widget(const Widget&);
798                Widget(Widget&&);
799            };
800        "#;
801        let declarations = extract(source);
802        let constructors = declarations
803            .members
804            .iter()
805            .filter(|member| {
806                member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
807                    && member.name == "Widget"
808            })
809            .collect::<Vec<_>>();
810
811        assert_eq!(constructors.len(), 2, "{declarations:#?}");
812        let shapes = constructors
813            .iter()
814            .map(|constructor| {
815                let parameters = constructor
816                    .parameter_types
817                    .as_ref()
818                    .expect("constructor parameters");
819                assert_eq!(parameters.len(), 1, "{constructor:#?}");
820                let CppParameterType::Structured(identity) = &parameters[0] else {
821                    panic!("constructor parameter shape: {constructor:#?}");
822                };
823                (
824                    identity.clone(),
825                    matches!(
826                        identity.view(identity.root_id()),
827                        Some(StructuredTypeNodeView::Reference(_))
828                    ),
829                    matches!(
830                        identity.view(identity.root_id()),
831                        Some(StructuredTypeNodeView::RvalueReference(_))
832                    ),
833                )
834            })
835            .collect::<Vec<_>>();
836
837        assert_ne!(shapes[0].0, shapes[1].0);
838        assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
839        assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
840        assert_eq!(declarations, extract(source));
841    }
842
843    #[test]
844    fn preserves_callable_arities_for_defaulted_and_required_parameters() {
845        let declarations = extract(
846            r#"
847            namespace std {
848            template <class C, class Alloc> class basic_string {
849            public:
850                basic_string(const C*, const Alloc& = Alloc());
851                basic_string(const C&, const Alloc&);
852            };
853            }
854            "#,
855        );
856        let constructors = declarations
857            .members
858            .iter()
859            .filter(|member| {
860                member.owner.as_ref().and_then(CppExternalOwner::type_name)
861                    == Some("std.basic_string")
862                    && member.name == "basic_string"
863            })
864            .collect::<Vec<_>>();
865
866        assert_eq!(constructors.len(), 2, "{declarations:#?}");
867        assert!(
868            constructors
869                .iter()
870                .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
871            "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
872        );
873        assert!(
874            constructors
875                .iter()
876                .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
877            "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
878        );
879    }
880
881    // This manifest checks the neutral header records consumed by model export,
882    // independently of the workspace query layer's declaration projection.
883    #[test]
884    #[hardwon(
885        "6ec52f232835: quicksilver round 4 survivor review: lexical namespaces stole out-of-line exported member owners"
886    )]
887    fn recovered_external_api_manifest() {
888        let declarations = extract(
889            r#"
890#define EXPORT_MARKER
891namespace n { class Widget; int damaged = ; }
892class EXPORT_MARKER Widget { public: void first(); };
893class EXPORT(2,0) Attributed final {
894public:
895    ATTRIBUTE("old") int value();
896    void following();
897};
898namespace a::a {
899struct C { void f(); void f()const; void g()&; void g()&&; };
900void ::a::a::C::f() {}
901void ::a::a::C::f()const {}
902void C::g()& {}
903void C::g()&& {}
904}
905DEPRECATED /* between name and arguments */ (
906    EXPORT_MARKER int old(int value), "hint"
907);
908EXPORT_MARKER int next();
909void after() {}
910"#,
911        );
912        let mut types = declarations
913            .types
914            .iter()
915            .map(|record| record.name.as_str())
916            .collect::<Vec<_>>();
917        types.sort_unstable();
918        assert_eq!(
919            types,
920            ["Attributed", "a", "a::a", "a::a.C", "n", "n.Widget"]
921        );
922        let mut callables = declarations
923            .members
924            .iter()
925            .filter(|record| record.kind == CppExternalMemberKind::Function)
926            .map(|record| {
927                (
928                    record.owner.as_ref().map(CppExternalOwner::name),
929                    record.name.as_str(),
930                    record.signature.as_deref(),
931                )
932            })
933            .collect::<Vec<_>>();
934        callables.sort_unstable();
935        let mut expected = vec![
936            (None, "after", Some("()")),
937            (None, "next", Some("()")),
938            (None, "old", Some("(int)")),
939            (Some("Attributed"), "following", Some("()")),
940            (Some("Attributed"), "value", Some("()")),
941            (Some("a::a.C"), "f", Some("()")),
942            (Some("a::a.C"), "f", Some("() const")),
943            (Some("a::a.C"), "g", Some("() &")),
944            (Some("a::a.C"), "g", Some("() &&")),
945            (Some("n.Widget"), "first", Some("()")),
946        ];
947        expected.sort_unstable();
948        assert_eq!(callables, expected);
949    }
950
951    // The external-declaration producer feeds a separately versioned pack model.
952    // Names belong to the outer formal, never to a nested callback's formal or
953    // an array bound. This manifest comes from the header's API declarations.
954    #[test]
955    fn preserves_callable_parameter_names() {
956        let declarations = extract(
957            "struct Owner {};\n\
958             void connect(int fd, const char *address, unsigned length);\n\
959             void callbacks(void (*callback)(int argument), int array[extent], int (Owner::*member)(int nested));\n",
960        );
961        for (name, expected) in [
962            ("connect", vec![Some("fd"), Some("address"), Some("length")]),
963            (
964                "callbacks",
965                vec![Some("callback"), Some("array"), Some("member")],
966            ),
967        ] {
968            let candidates = declarations
969                .members
970                .iter()
971                .filter(|member| member.name == name)
972                .collect::<Vec<_>>();
973            let [member] = candidates.as_slice() else {
974                panic!("{name} declaration: {declarations:#?}");
975            };
976            assert_eq!(
977                member.parameter_names,
978                Some(
979                    expected
980                        .into_iter()
981                        .map(|name| name.map(str::to_owned))
982                        .collect()
983                ),
984                "{name}: outer parameter slots must agree with the API manifest"
985            );
986        }
987    }
988
989    #[test]
990    #[brokk_bifrost_test_macros::hardwon(
991        "5e79db920891: quicksilver round 4 survivor tests: unnamed formals inherit nested names"
992    )]
993    fn unnamed_callable_parameter_names() {
994        let declarations = extract("void unnamed(void (*)(int argument), int [extent]);\n");
995        let [member] = declarations.members.as_slice() else {
996            panic!("one exported callable: {declarations:#?}");
997        };
998        assert_eq!(member.parameter_names, Some(vec![None, None]));
999    }
1000
1001    #[test]
1002    fn preserves_callable_explicitness_for_implicit_binding() {
1003        for (declaration, expected) in [
1004            ("Widget(const char*);", CppCallableExplicitness::Implicit),
1005            (
1006                "explicit Widget(const char*);",
1007                CppCallableExplicitness::Explicit,
1008            ),
1009            (
1010                "explicit(true) Widget(const char*);",
1011                CppCallableExplicitness::Conditional,
1012            ),
1013        ] {
1014            let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
1015            let member = declarations
1016                .members
1017                .first()
1018                .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
1019            assert_eq!(Some(expected), member.explicitness, "{member:#?}");
1020        }
1021    }
1022
1023    #[test]
1024    fn preserves_callable_return_reference_shapes() {
1025        let declarations = extract(
1026            r#"
1027            namespace std {
1028            class basic_string {
1029            public:
1030                basic_string& operator=(const basic_string&);
1031                basic_string&& operator=(basic_string&&);
1032                void operator=(int);
1033            };
1034            }
1035            "#,
1036        );
1037        let assignments = declarations
1038            .members
1039            .iter()
1040            .filter(|member| member.name == "operator=")
1041            .collect::<Vec<_>>();
1042        assert_eq!(assignments.len(), 3, "{declarations:#?}");
1043
1044        let copy = assignments
1045            .iter()
1046            .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
1047            .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
1048        let Some(StructuredTypeNodeView::Reference(inner)) = copy
1049            .return_type
1050            .as_ref()
1051            .and_then(|identity| identity.view(identity.root_id()))
1052        else {
1053            panic!("copy return type: {copy:#?}");
1054        };
1055        let Some(StructuredTypeNodeView::Named(name)) = copy
1056            .return_type
1057            .as_ref()
1058            .and_then(|identity| identity.view(inner))
1059        else {
1060            panic!("copy return target: {copy:#?}");
1061        };
1062        assert_eq!(["basic_string"], name.path());
1063        assert_eq!(["std", "basic_string"], name.lexical_scope());
1064
1065        let move_assignment = declarations
1066            .members
1067            .iter()
1068            .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
1069            .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
1070        assert!(matches!(
1071            move_assignment
1072                .return_type
1073                .as_ref()
1074                .and_then(|identity| identity.view(identity.root_id())),
1075            Some(StructuredTypeNodeView::RvalueReference(_))
1076        ));
1077
1078        let void_assignment = declarations
1079            .members
1080            .iter()
1081            .find(|member| member.signature.as_deref() == Some("(int)"))
1082            .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
1083        let Some(StructuredTypeNodeView::Named(name)) = void_assignment
1084            .return_type
1085            .as_ref()
1086            .and_then(|identity| identity.view(identity.root_id()))
1087        else {
1088            panic!("void return type: {void_assignment:#?}");
1089        };
1090        assert_eq!(["void"], name.path());
1091    }
1092
1093    #[test]
1094    fn preserves_type_alias_underlying_structured_identity() {
1095        let declarations = extract(
1096            r#"
1097            namespace std {
1098            template<class C, class Traits, class Alloc> class basic_string;
1099            using string = basic_string<char, char_traits<char>, allocator<char>>;
1100            }
1101            "#,
1102        );
1103        let alias = declarations
1104            .types
1105            .iter()
1106            .find(|record| record.name == "std.string")
1107            .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
1108        assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
1109        let identity = alias
1110            .underlying_type
1111            .as_ref()
1112            .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
1113        let Some(StructuredTypeNodeView::Generic { base, arguments }) =
1114            identity.view(identity.root_id())
1115        else {
1116            panic!("generic basic_string alias target: {identity:#?}");
1117        };
1118        assert_eq!(3, arguments.len());
1119        let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
1120            panic!("named basic_string alias base: {identity:#?}");
1121        };
1122        assert_eq!(["basic_string"], name.path());
1123        assert_eq!(["std"], name.lexical_scope());
1124    }
1125
1126    #[test]
1127    fn keeps_same_short_names_under_distinct_owners() {
1128        let declarations = extract(
1129            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
1130        );
1131        let mut owners = declarations
1132            .members
1133            .iter()
1134            .filter(|member| member.name == "add")
1135            .filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
1136            .collect::<Vec<_>>();
1137        owners.sort();
1138
1139        assert_eq!(vec!["first.box", "second.box"], owners);
1140        assert!(
1141            declarations
1142                .members
1143                .iter()
1144                .filter(|member| member.name == "add")
1145                .all(|member| member.visibility == CppExternalVisibility::Private)
1146        );
1147    }
1148
1149    #[test]
1150    fn nested_type_visibility_follows_the_enclosing_access_section() {
1151        let declarations = extract(
1152            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
1153        );
1154        assert!(declarations.types.iter().any(|record| {
1155            record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
1156        }));
1157        assert!(declarations.types.iter().any(|record| {
1158            record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
1159        }));
1160        assert!(declarations.types.iter().any(|record| {
1161            record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
1162        }));
1163    }
1164
1165    #[test]
1166    fn preprocessor_and_record_limits_make_the_surface_partial() {
1167        let temp = tempfile::tempdir().expect("temp root");
1168        let declarations = extract_external_declarations(
1169            temp.path(),
1170            Path::new("limited.hpp"),
1171            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
1172            CppExternalDeclarationLimits { max_records: 1 },
1173        );
1174
1175        assert_eq!(
1176            CppExternalDeclarationCompleteness::Partial,
1177            declarations.completeness
1178        );
1179        assert!(
1180            declarations
1181                .diagnostics
1182                .iter()
1183                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
1184        );
1185        assert!(
1186            declarations
1187                .diagnostics
1188                .iter()
1189                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
1190        );
1191    }
1192}