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    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                if child.kind() == "access_specifier" {
481                    visibility = match node_text(child, source).trim_end_matches(':').trim() {
482                        "public" => CppExternalVisibility::Public,
483                        "protected" => CppExternalVisibility::Protected,
484                        "private" => CppExternalVisibility::Private,
485                        _ => CppExternalVisibility::Private,
486                    };
487                }
488            }
489            return visibility;
490        }
491        current = Some(parent);
492    }
493    CppExternalVisibility::Public
494}
495
496fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
497    while !matches!(
498        declarator.kind(),
499        "declaration" | "field_declaration" | "function_definition"
500    ) {
501        declarator = declarator.parent()?;
502    }
503    if declarator.has_error() {
504        return None;
505    }
506    let mut stack = vec![declarator];
507    let mut explicit = None;
508    while let Some(node) = stack.pop() {
509        if node.kind() == "explicit_function_specifier" {
510            if explicit.is_some() {
511                return None;
512            }
513            explicit = Some(if node.named_child_count() == 0 {
514                CppCallableExplicitness::Explicit
515            } else {
516                CppCallableExplicitness::Conditional
517            });
518            continue;
519        }
520        let mut cursor = node.walk();
521        stack.extend(node.named_children(&mut cursor));
522    }
523    Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
524}
525
526fn cpp_returns_forwarding_reference<'tree>(
527    declarator: Node<'tree>,
528    source: &str,
529    ancestry: &ParentIndex<'tree>,
530    parameters: Option<&[CppParameterType]>,
531    shapes: Option<&[CppComparableSlot]>,
532    returns: Option<&StructuredTypeIdentity>,
533) -> bool {
534    let (
535        Some([CppParameterType::Structured(parameter)]),
536        Some([CppComparableSlot::Shape(shape)]),
537        Some(returns),
538    ) = (parameters, shapes, returns)
539    else {
540        return false;
541    };
542    let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
543    else {
544        return false;
545    };
546    let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
547        return false;
548    };
549    let [parameter_name] = name.path() else {
550        return false;
551    };
552    let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
553        return false;
554    };
555    if !matches!(
556        shape.node(*inner),
557        CppComparableNode::Named {
558            konst: false,
559            volatil: false,
560            ..
561        }
562    ) || name.is_absolute()
563        || !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
564        || !matches!(
565            returns.view(returns.root_id()),
566            Some(StructuredTypeNodeView::RvalueReference(_))
567        )
568    {
569        return false;
570    }
571
572    // Keep the dependent return's template argument. Its nominal structured
573    // name alone flattens `remove_reference<T>::type` and loses the `T`.
574    let mut current = declarator;
575    while let Some(parent) = ancestry.parent(current) {
576        if matches!(
577            parent.kind(),
578            "function_definition" | "declaration" | "field_declaration"
579        ) {
580            let mut cursor = parent.walk();
581            if parent.named_children(&mut cursor).any(|child| {
582                child.kind() == "type_qualifier"
583                    && matches!(node_text(child, source), "const" | "volatile")
584            }) {
585                return false;
586            }
587            return parent
588                .child_by_field_name("type")
589                .is_some_and(|return_node| {
590                    cpp_forwarded_return_type(return_node, source, parameter_name)
591                });
592        }
593        current = parent;
594    }
595    false
596}
597
598fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
599    let mut components = Vec::new();
600    let mut removals = 0;
601    let mut stack = vec![node];
602    while let Some(current) = stack.pop() {
603        match current.kind() {
604            "identifier" | "type_identifier" | "namespace_identifier" => {
605                components.push(node_text(current, source));
606            }
607            "dependent_type" => {
608                if current.named_child_count() != 1 {
609                    return false;
610                }
611                stack.push(current.named_child(0).expect("one named child"));
612            }
613            "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
614                let Some(name) = current.child_by_field_name("name") else {
615                    return false;
616                };
617                stack.push(name);
618                if let Some(scope) = current.child_by_field_name("scope") {
619                    stack.push(scope);
620                }
621            }
622            "template_type" => {
623                let (Some(name), Some(arguments)) = (
624                    current.child_by_field_name("name"),
625                    current.child_by_field_name("arguments"),
626                ) else {
627                    return false;
628                };
629                if !matches!(name.kind(), "identifier" | "type_identifier")
630                    || node_text(name, source) != "remove_reference"
631                    || arguments.named_child_count() != 1
632                {
633                    return false;
634                }
635                let mut argument = arguments.named_child(0).expect("one named argument");
636                if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
637                    argument = argument.named_child(0).expect("one named type");
638                }
639                if !matches!(argument.kind(), "identifier" | "type_identifier")
640                    || node_text(argument, source) != parameter
641                {
642                    return false;
643                }
644                removals += 1;
645                stack.push(name);
646            }
647            _ => return false,
648        }
649    }
650    (removals == 0 && components == [parameter])
651        || (removals == 1
652            && (components == ["std", "remove_reference", "type"]
653                || components == ["remove_reference", "type"]))
654}
655
656fn nearest_declaration_owner(
657    declaration: &CodeUnit,
658    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
659) -> Option<CppExternalOwner> {
660    let mut current = declaration;
661    while let Some(parent) = parent_by_child.get(current) {
662        match parent.kind() {
663            CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
664            CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
665            _ => {}
666        }
667        current = parent;
668    }
669    None
670}
671
672fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
673    let mut stack = vec![root];
674    while let Some(node) = stack.pop() {
675        if matches!(
676            node.kind(),
677            "preproc_def"
678                | "preproc_function_def"
679                | "preproc_if"
680                | "preproc_ifdef"
681                | "preproc_ifndef"
682                | "preproc_elif"
683                | "preproc_else"
684        ) {
685            return true;
686        }
687        let mut cursor = node.walk();
688        stack.extend(node.named_children(&mut cursor));
689    }
690    false
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696    use brokk_bifrost_core::analyzer::model::CallableArity;
697    use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
698
699    fn extract(source: &str) -> CppExternalDeclarationSet {
700        let temp = tempfile::tempdir().expect("temp root");
701        extract_external_declarations(
702            temp.path(),
703            Path::new("vector"),
704            source,
705            CppExternalDeclarationLimits::default(),
706        )
707    }
708
709    #[test]
710    fn extracts_literal_include_paths_with_delimiters() {
711        let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
712        assert_eq!(
713            vec![
714                (PathBuf::from("local.hpp"), true),
715                (PathBuf::from("vector"), false),
716            ],
717            external_include_paths(source)
718        );
719        assert_eq!(
720            vec![PathBuf::from("vector")],
721            external_angle_include_paths(source)
722        );
723        let mut parser = Parser::new();
724        parser
725            .set_language(&tree_sitter_cpp::LANGUAGE.into())
726            .expect("C++ grammar");
727        let tree = parser.parse(source, None).expect("tree");
728        assert_eq!(
729            vec![PathBuf::from("vector")],
730            external_angle_include_paths_from_root(source, tree.root_node())
731        );
732    }
733
734    #[test]
735    fn extracts_namespaced_template_type_and_owned_members() {
736        let declarations = extract(
737            r#"
738            namespace std {
739            template <typename T> class vector : public sequence<T> {
740            public:
741                vector();
742                void push_back(const T& value);
743                T size;
744            };
745            }
746            "#,
747        );
748
749        assert_eq!(
750            CppExternalDeclarationCompleteness::Complete,
751            declarations.completeness
752        );
753        assert!(
754            declarations.types.iter().any(|record| {
755                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
756            }),
757            "{declarations:#?}"
758        );
759        assert!(
760            declarations.members.iter().any(|record| {
761                record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
762                    && record.name == "push_back"
763                    && record.visibility == CppExternalVisibility::Public
764            }),
765            "{declarations:#?}"
766        );
767        assert!(
768            declarations.members.iter().any(|record| {
769                record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
770                    && record.name == "size"
771            }),
772            "{declarations:#?}"
773        );
774    }
775
776    #[test]
777    fn preserves_copy_and_move_reference_parameter_shapes() {
778        let source = r#"
779            class Widget {
780            public:
781                Widget(const Widget&);
782                Widget(Widget&&);
783            };
784        "#;
785        let declarations = extract(source);
786        let constructors = declarations
787            .members
788            .iter()
789            .filter(|member| {
790                member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
791                    && member.name == "Widget"
792            })
793            .collect::<Vec<_>>();
794
795        assert_eq!(constructors.len(), 2, "{declarations:#?}");
796        let shapes = constructors
797            .iter()
798            .map(|constructor| {
799                let parameters = constructor
800                    .parameter_types
801                    .as_ref()
802                    .expect("constructor parameters");
803                assert_eq!(parameters.len(), 1, "{constructor:#?}");
804                let CppParameterType::Structured(identity) = &parameters[0] else {
805                    panic!("constructor parameter shape: {constructor:#?}");
806                };
807                (
808                    identity.clone(),
809                    matches!(
810                        identity.view(identity.root_id()),
811                        Some(StructuredTypeNodeView::Reference(_))
812                    ),
813                    matches!(
814                        identity.view(identity.root_id()),
815                        Some(StructuredTypeNodeView::RvalueReference(_))
816                    ),
817                )
818            })
819            .collect::<Vec<_>>();
820
821        assert_ne!(shapes[0].0, shapes[1].0);
822        assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
823        assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
824        assert_eq!(declarations, extract(source));
825    }
826
827    #[test]
828    fn preserves_callable_arities_for_defaulted_and_required_parameters() {
829        let declarations = extract(
830            r#"
831            namespace std {
832            template <class C, class Alloc> class basic_string {
833            public:
834                basic_string(const C*, const Alloc& = Alloc());
835                basic_string(const C&, const Alloc&);
836            };
837            }
838            "#,
839        );
840        let constructors = declarations
841            .members
842            .iter()
843            .filter(|member| {
844                member.owner.as_ref().and_then(CppExternalOwner::type_name)
845                    == Some("std.basic_string")
846                    && member.name == "basic_string"
847            })
848            .collect::<Vec<_>>();
849
850        assert_eq!(constructors.len(), 2, "{declarations:#?}");
851        assert!(
852            constructors
853                .iter()
854                .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
855            "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
856        );
857        assert!(
858            constructors
859                .iter()
860                .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
861            "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
862        );
863    }
864
865    #[test]
866    fn preserves_callable_parameter_names() {
867        let declarations = extract("int connect(int fd, const char *address, unsigned length);\n");
868        let connect_candidates = declarations
869            .members
870            .iter()
871            .filter(|member| member.name == "connect")
872            .collect::<Vec<_>>();
873        let [connect] = connect_candidates.as_slice() else {
874            panic!("connect declaration: {declarations:#?}");
875        };
876        assert_eq!(
877            Some(vec![
878                Some("fd".to_owned()),
879                Some("address".to_owned()),
880                Some("length".to_owned()),
881            ]),
882            connect.parameter_names,
883            "formal names must remain aligned with the declaration's argument slots"
884        );
885    }
886
887    #[test]
888    fn preserves_callable_explicitness_for_implicit_binding() {
889        for (declaration, expected) in [
890            ("Widget(const char*);", CppCallableExplicitness::Implicit),
891            (
892                "explicit Widget(const char*);",
893                CppCallableExplicitness::Explicit,
894            ),
895            (
896                "explicit(true) Widget(const char*);",
897                CppCallableExplicitness::Conditional,
898            ),
899        ] {
900            let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
901            let member = declarations
902                .members
903                .first()
904                .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
905            assert_eq!(Some(expected), member.explicitness, "{member:#?}");
906        }
907    }
908
909    #[test]
910    fn preserves_callable_return_reference_shapes() {
911        let declarations = extract(
912            r#"
913            namespace std {
914            class basic_string {
915            public:
916                basic_string& operator=(const basic_string&);
917                basic_string&& operator=(basic_string&&);
918                void operator=(int);
919            };
920            }
921            "#,
922        );
923        let assignments = declarations
924            .members
925            .iter()
926            .filter(|member| member.name == "operator=")
927            .collect::<Vec<_>>();
928        assert_eq!(assignments.len(), 3, "{declarations:#?}");
929
930        let copy = assignments
931            .iter()
932            .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
933            .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
934        let Some(StructuredTypeNodeView::Reference(inner)) = copy
935            .return_type
936            .as_ref()
937            .and_then(|identity| identity.view(identity.root_id()))
938        else {
939            panic!("copy return type: {copy:#?}");
940        };
941        let Some(StructuredTypeNodeView::Named(name)) = copy
942            .return_type
943            .as_ref()
944            .and_then(|identity| identity.view(inner))
945        else {
946            panic!("copy return target: {copy:#?}");
947        };
948        assert_eq!(["basic_string"], name.path());
949        assert_eq!(["std", "basic_string"], name.lexical_scope());
950
951        let move_assignment = declarations
952            .members
953            .iter()
954            .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
955            .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
956        assert!(matches!(
957            move_assignment
958                .return_type
959                .as_ref()
960                .and_then(|identity| identity.view(identity.root_id())),
961            Some(StructuredTypeNodeView::RvalueReference(_))
962        ));
963
964        let void_assignment = declarations
965            .members
966            .iter()
967            .find(|member| member.signature.as_deref() == Some("(int)"))
968            .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
969        let Some(StructuredTypeNodeView::Named(name)) = void_assignment
970            .return_type
971            .as_ref()
972            .and_then(|identity| identity.view(identity.root_id()))
973        else {
974            panic!("void return type: {void_assignment:#?}");
975        };
976        assert_eq!(["void"], name.path());
977    }
978
979    #[test]
980    fn preserves_type_alias_underlying_structured_identity() {
981        let declarations = extract(
982            r#"
983            namespace std {
984            template<class C, class Traits, class Alloc> class basic_string;
985            using string = basic_string<char, char_traits<char>, allocator<char>>;
986            }
987            "#,
988        );
989        let alias = declarations
990            .types
991            .iter()
992            .find(|record| record.name == "std.string")
993            .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
994        assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
995        let identity = alias
996            .underlying_type
997            .as_ref()
998            .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
999        let Some(StructuredTypeNodeView::Generic { base, arguments }) =
1000            identity.view(identity.root_id())
1001        else {
1002            panic!("generic basic_string alias target: {identity:#?}");
1003        };
1004        assert_eq!(3, arguments.len());
1005        let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
1006            panic!("named basic_string alias base: {identity:#?}");
1007        };
1008        assert_eq!(["basic_string"], name.path());
1009        assert_eq!(["std"], name.lexical_scope());
1010    }
1011
1012    #[test]
1013    fn keeps_same_short_names_under_distinct_owners() {
1014        let declarations = extract(
1015            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
1016        );
1017        let mut owners = declarations
1018            .members
1019            .iter()
1020            .filter(|member| member.name == "add")
1021            .filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
1022            .collect::<Vec<_>>();
1023        owners.sort();
1024
1025        assert_eq!(vec!["first.box", "second.box"], owners);
1026        assert!(
1027            declarations
1028                .members
1029                .iter()
1030                .filter(|member| member.name == "add")
1031                .all(|member| member.visibility == CppExternalVisibility::Private)
1032        );
1033    }
1034
1035    #[test]
1036    fn nested_type_visibility_follows_the_enclosing_access_section() {
1037        let declarations = extract(
1038            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
1039        );
1040        assert!(declarations.types.iter().any(|record| {
1041            record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
1042        }));
1043        assert!(declarations.types.iter().any(|record| {
1044            record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
1045        }));
1046        assert!(declarations.types.iter().any(|record| {
1047            record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
1048        }));
1049    }
1050
1051    #[test]
1052    fn preprocessor_and_record_limits_make_the_surface_partial() {
1053        let temp = tempfile::tempdir().expect("temp root");
1054        let declarations = extract_external_declarations(
1055            temp.path(),
1056            Path::new("limited.hpp"),
1057            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
1058            CppExternalDeclarationLimits { max_records: 1 },
1059        );
1060
1061        assert_eq!(
1062            CppExternalDeclarationCompleteness::Partial,
1063            declarations.completeness
1064        );
1065        assert!(
1066            declarations
1067                .diagnostics
1068                .iter()
1069                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
1070        );
1071        assert!(
1072            declarations
1073                .diagnostics
1074                .iter()
1075                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
1076        );
1077    }
1078}