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