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    CppComparableSlot, CppParameterType, cpp_callable_parameter_type_identities,
10    cpp_callable_return_type_identity, cpp_comparable_parameter_shapes, cpp_function_declarator_at,
11    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,
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#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct CppExternalType {
59    pub name: String,
60    pub source_name: String,
61    pub is_type_alias: bool,
62    pub underlying_type: Option<StructuredTypeIdentity>,
63    /// Structured template declaration metadata, when the class declaration
64    /// was reached through a template declaration.  Keeping this alongside
65    /// the neutral record lets a consumer prove a primary template's exact
66    /// arity/defaults without re-reading source text.
67    pub template_metadata: Option<CppTemplateMetadata>,
68    pub visibility: CppExternalVisibility,
69    pub source_path: PathBuf,
70    pub direct_bases: Vec<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CppExternalMember {
75    pub owner: Option<String>,
76    pub name: String,
77    pub qualified_name: String,
78    pub kind: CppExternalMemberKind,
79    pub visibility: CppExternalVisibility,
80    pub is_constructor: bool,
81    pub signature: Option<String>,
82    /// The structured type of each invocation parameter, or `None` when the
83    /// declaration is not a callable or its parameter list was not reached.
84    ///
85    /// These are structured identities rather than rendered spellings because
86    /// the consumer publishes them into a type model, and a spelling such as
87    /// `const T&` is a source text, not a type name.
88    pub parameter_types: Option<Vec<CppParameterType>>,
89    /// Parser-derived parameter shapes retaining C++ cv-qualification.
90    pub parameter_shapes: Option<Vec<CppComparableSlot>>,
91    /// Invocation arity, including default and repeated parameters, when the
92    /// declaration is callable and its parameter list was reached.
93    pub callable_arity: Option<CallableArity>,
94    /// Whether a constructor is available to implicit conversion.
95    pub explicitness: Option<CppCallableExplicitness>,
96    pub return_type: Option<StructuredTypeIdentity>,
97    pub source_path: PathBuf,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CppCallableExplicitness {
102    Implicit,
103    Explicit,
104    Conditional,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct CppExternalDeclarationDiagnostic {
109    pub code: &'static str,
110    pub message: String,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct CppExternalDeclarationSet {
115    pub types: Vec<CppExternalType>,
116    pub members: Vec<CppExternalMember>,
117    pub completeness: CppExternalDeclarationCompleteness,
118    pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
119}
120
121/// Return the literal paths from angle-bracket includes in one C++ source.
122///
123/// Quoted and computed includes are not external-root evidence. Conditional
124/// includes are retained because compile-context resolution still proves the
125/// root, while pack completeness records preprocessor uncertainty separately.
126pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
127    let mut parser = Parser::new();
128    parser
129        .set_language(&tree_sitter_cpp::LANGUAGE.into())
130        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
131    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
132    external_angle_include_paths_from_root(source, tree.root_node())
133}
134
135/// Return literal unconditional angle includes from an existing C++ syntax tree.
136///
137/// The source and root must describe the same immutable snapshot. Analyzer
138/// callers use this form to reuse prepared workspace syntax; standalone
139/// external headers use [`external_angle_include_paths`].
140pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
141    let mut paths = Vec::new();
142    let mut stack = vec![root];
143    while let Some(node) = stack.pop() {
144        if node.kind() == "preproc_include"
145            && !has_conditional_preprocessor_ancestor(node)
146            && let Some(path) = node.child_by_field_name("path")
147            && path.kind() == "system_lib_string"
148            && let Some(path) = node_text(path, source)
149                .strip_prefix('<')
150                .and_then(|path| path.strip_suffix('>'))
151                .filter(|path| !path.is_empty())
152        {
153            paths.push(PathBuf::from(path));
154        }
155        let mut cursor = node.walk();
156        stack.extend(node.named_children(&mut cursor));
157    }
158    paths.sort();
159    paths.dedup();
160    paths
161}
162
163fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
164    while let Some(parent) = node.parent() {
165        if matches!(
166            parent.kind(),
167            "preproc_if" | "preproc_ifdef" | "preproc_ifndef" | "preproc_elif" | "preproc_else"
168        ) {
169            return true;
170        }
171        node = parent;
172    }
173    false
174}
175
176/// Extract declarations from one exact header source entry.
177///
178/// `source_path` is relative to `source_set_root`. The caller owns source-set
179/// containment, byte limits, cancellation, and stable hashing. This function
180/// owns only C++ syntax interpretation and its record limit.
181pub fn extract_external_declarations(
182    source_set_root: &Path,
183    source_path: &Path,
184    source: &str,
185    limits: CppExternalDeclarationLimits,
186) -> CppExternalDeclarationSet {
187    let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
188    let mut parser = Parser::new();
189    parser
190        .set_language(&tree_sitter_cpp::LANGUAGE.into())
191        .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
192    let tree = parser.parse(source, None).expect("uncancelled C++ parse");
193
194    let mut diagnostics = Vec::new();
195    let mut completeness = CppExternalDeclarationCompleteness::Complete;
196    let mut parse_errors = Vec::new();
197    collect_parse_errors(tree.root_node(), &mut parse_errors);
198    if !parse_errors.is_empty() {
199        completeness = CppExternalDeclarationCompleteness::Partial;
200        diagnostics.push(CppExternalDeclarationDiagnostic {
201            code: "cpp.external.parse_error",
202            message: format!(
203                "external header `{}` has parse errors",
204                source_path.display()
205            ),
206        });
207    }
208    if has_unsupported_preprocessing(tree.root_node()) {
209        completeness = CppExternalDeclarationCompleteness::Partial;
210        diagnostics.push(CppExternalDeclarationDiagnostic {
211            code: "cpp.external.preprocessor_partial",
212            message: format!(
213                "external header `{}` has conditional or generated declarations",
214                source_path.display()
215            ),
216        });
217    }
218
219    let parsed = parse_cpp_file(&file, source, &tree);
220    let mut parent_by_child = HashMap::default();
221    for (parent, children) in &parsed.children {
222        for child in children {
223            parent_by_child.insert(child.clone(), parent.clone());
224        }
225    }
226
227    let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
228    declarations.sort_by_key(|declaration| {
229        (
230            declaration.fq_name(),
231            declaration.kind(),
232            declaration.signature().map(str::to_owned),
233        )
234    });
235
236    // One index for the file's tree rather than one ancestor walk per
237    // declaration: a large generated header has tens of thousands of them.
238    let ancestry = ParentIndex::new(tree.root_node());
239    let mut types = Vec::new();
240    let mut members = Vec::new();
241    for declaration in declarations {
242        if types.len().saturating_add(members.len()) >= limits.max_records {
243            completeness = CppExternalDeclarationCompleteness::Partial;
244            diagnostics.push(CppExternalDeclarationDiagnostic {
245                code: "cpp.external.record_limit",
246                message: format!(
247                    "external header `{}` exceeded the declaration record limit",
248                    source_path.display()
249                ),
250            });
251            break;
252        }
253        match declaration.kind() {
254            CodeUnitType::Class => types.push(CppExternalType {
255                name: declaration.fq_name(),
256                source_name: cpp_name_for(&declaration),
257                is_type_alias: parsed.type_aliases.contains(&declaration),
258                underlying_type: parsed
259                    .signature_metadata
260                    .get(&declaration)
261                    .and_then(|records| records.first())
262                    .and_then(|metadata| metadata.underlying_type_identity())
263                    .cloned(),
264                template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
265                visibility: parsed
266                    .ranges
267                    .get(&declaration)
268                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
269                    .map(|start| cpp_member_visibility(tree.root_node(), source, start))
270                    .unwrap_or(CppExternalVisibility::Private),
271                source_path: source_path.to_path_buf(),
272                direct_bases: parsed
273                    .raw_supertypes
274                    .get(&declaration)
275                    .cloned()
276                    .unwrap_or_default(),
277            }),
278            CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
279                let metadata = parsed
280                    .signature_metadata
281                    .get(&declaration)
282                    .and_then(|records| records.first());
283                let declaration_start = parsed
284                    .ranges
285                    .get(&declaration)
286                    .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
287                let function_declarator = (declaration.kind() == CodeUnitType::Function)
288                    .then(|| {
289                        declaration_start
290                            .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
291                    })
292                    .flatten();
293                let parameter_types = function_declarator.map(|declarator| {
294                    cpp_callable_parameter_type_identities(declarator, source, &ancestry)
295                });
296                let parameter_shapes = function_declarator.map(|declarator| {
297                    cpp_comparable_parameter_shapes(declarator, source, &ancestry)
298                });
299                let return_type = metadata
300                    .and_then(|metadata| metadata.return_type_identity())
301                    .cloned()
302                    .or_else(|| {
303                        function_declarator.and_then(|declarator| {
304                            cpp_callable_return_type_identity(declarator, source, &ancestry)
305                        })
306                    });
307                members.push(CppExternalMember {
308                    owner: nearest_type_owner(&declaration, &parent_by_child),
309                    name: declaration.terminal_name().to_owned(),
310                    qualified_name: cpp_name_for(&declaration),
311                    kind: match declaration.kind() {
312                        CodeUnitType::Function => CppExternalMemberKind::Function,
313                        CodeUnitType::Field => CppExternalMemberKind::Field,
314                        CodeUnitType::Macro => CppExternalMemberKind::Macro,
315                        _ => unreachable!("the outer match admits exactly member kinds"),
316                    },
317                    visibility: declaration_start
318                        .map(|start| cpp_member_visibility(tree.root_node(), source, start))
319                        .unwrap_or(CppExternalVisibility::Private),
320                    is_constructor: metadata
321                        .is_some_and(|metadata| metadata.callable_is_constructor()),
322                    signature: declaration.signature().map(str::to_owned),
323                    parameter_types,
324                    parameter_shapes,
325                    callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
326                    explicitness: function_declarator.and_then(cpp_callable_explicitness),
327                    return_type,
328                    source_path: source_path.to_path_buf(),
329                });
330            }
331            CodeUnitType::Module | CodeUnitType::FileScope => {}
332        }
333    }
334
335    CppExternalDeclarationSet {
336        types,
337        members,
338        completeness,
339        diagnostics,
340    }
341}
342
343fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
344    let mut current = root.descendant_for_byte_range(start_byte, start_byte);
345    while let Some(node) = current {
346        let Some(parent) = node.parent() else {
347            break;
348        };
349        if parent.kind() == "field_declaration_list" {
350            let default = match parent.parent().map(|owner| owner.kind()) {
351                Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
352                _ => CppExternalVisibility::Private,
353            };
354            let mut visibility = default;
355            let mut cursor = parent.walk();
356            for child in parent.named_children(&mut cursor) {
357                if child.start_byte() > start_byte {
358                    break;
359                }
360                if child.kind() == "access_specifier" {
361                    visibility = match node_text(child, source).trim_end_matches(':').trim() {
362                        "public" => CppExternalVisibility::Public,
363                        "protected" => CppExternalVisibility::Protected,
364                        "private" => CppExternalVisibility::Private,
365                        _ => CppExternalVisibility::Private,
366                    };
367                }
368            }
369            return visibility;
370        }
371        current = Some(parent);
372    }
373    CppExternalVisibility::Public
374}
375
376fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
377    while !matches!(
378        declarator.kind(),
379        "declaration" | "field_declaration" | "function_definition"
380    ) {
381        declarator = declarator.parent()?;
382    }
383    if declarator.has_error() {
384        return None;
385    }
386    let mut stack = vec![declarator];
387    let mut explicit = None;
388    while let Some(node) = stack.pop() {
389        if node.kind() == "explicit_function_specifier" {
390            if explicit.is_some() {
391                return None;
392            }
393            explicit = Some(if node.named_child_count() == 0 {
394                CppCallableExplicitness::Explicit
395            } else {
396                CppCallableExplicitness::Conditional
397            });
398            continue;
399        }
400        let mut cursor = node.walk();
401        stack.extend(node.named_children(&mut cursor));
402    }
403    Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
404}
405
406fn nearest_type_owner(
407    declaration: &CodeUnit,
408    parent_by_child: &HashMap<CodeUnit, CodeUnit>,
409) -> Option<String> {
410    let mut current = declaration;
411    while let Some(parent) = parent_by_child.get(current) {
412        if parent.kind() == CodeUnitType::Class {
413            return Some(parent.fq_name());
414        }
415        current = parent;
416    }
417    None
418}
419
420fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
421    let mut stack = vec![root];
422    while let Some(node) = stack.pop() {
423        if matches!(
424            node.kind(),
425            "preproc_def"
426                | "preproc_function_def"
427                | "preproc_if"
428                | "preproc_ifdef"
429                | "preproc_ifndef"
430                | "preproc_elif"
431                | "preproc_else"
432        ) {
433            return true;
434        }
435        let mut cursor = node.walk();
436        stack.extend(node.named_children(&mut cursor));
437    }
438    false
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use brokk_bifrost_core::analyzer::model::CallableArity;
445    use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
446
447    fn extract(source: &str) -> CppExternalDeclarationSet {
448        let temp = tempfile::tempdir().expect("temp root");
449        extract_external_declarations(
450            temp.path(),
451            Path::new("vector"),
452            source,
453            CppExternalDeclarationLimits::default(),
454        )
455    }
456
457    #[test]
458    fn extracts_only_literal_angle_include_paths() {
459        let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
460        assert_eq!(
461            vec![PathBuf::from("vector")],
462            external_angle_include_paths(source)
463        );
464        let mut parser = Parser::new();
465        parser
466            .set_language(&tree_sitter_cpp::LANGUAGE.into())
467            .expect("C++ grammar");
468        let tree = parser.parse(source, None).expect("tree");
469        assert_eq!(
470            vec![PathBuf::from("vector")],
471            external_angle_include_paths_from_root(source, tree.root_node())
472        );
473    }
474
475    #[test]
476    fn extracts_namespaced_template_type_and_owned_members() {
477        let declarations = extract(
478            r#"
479            namespace std {
480            template <typename T> class vector : public sequence<T> {
481            public:
482                vector();
483                void push_back(const T& value);
484                T size;
485            };
486            }
487            "#,
488        );
489
490        assert_eq!(
491            CppExternalDeclarationCompleteness::Complete,
492            declarations.completeness
493        );
494        assert!(
495            declarations.types.iter().any(|record| {
496                record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
497            }),
498            "{declarations:#?}"
499        );
500        assert!(
501            declarations.members.iter().any(|record| {
502                record.owner.as_deref() == Some("std.vector")
503                    && record.name == "push_back"
504                    && record.visibility == CppExternalVisibility::Public
505            }),
506            "{declarations:#?}"
507        );
508        assert!(
509            declarations.members.iter().any(|record| {
510                record.owner.as_deref() == Some("std.vector") && record.name == "size"
511            }),
512            "{declarations:#?}"
513        );
514    }
515
516    #[test]
517    fn preserves_copy_and_move_reference_parameter_shapes() {
518        let source = r#"
519            class Widget {
520            public:
521                Widget(const Widget&);
522                Widget(Widget&&);
523            };
524        "#;
525        let declarations = extract(source);
526        let constructors = declarations
527            .members
528            .iter()
529            .filter(|member| member.owner.as_deref() == Some("Widget") && member.name == "Widget")
530            .collect::<Vec<_>>();
531
532        assert_eq!(constructors.len(), 2, "{declarations:#?}");
533        let shapes = constructors
534            .iter()
535            .map(|constructor| {
536                let parameters = constructor
537                    .parameter_types
538                    .as_ref()
539                    .expect("constructor parameters");
540                assert_eq!(parameters.len(), 1, "{constructor:#?}");
541                let CppParameterType::Structured(identity) = &parameters[0] else {
542                    panic!("constructor parameter shape: {constructor:#?}");
543                };
544                (
545                    identity.clone(),
546                    matches!(
547                        identity.view(identity.root_id()),
548                        Some(StructuredTypeNodeView::Reference(_))
549                    ),
550                    matches!(
551                        identity.view(identity.root_id()),
552                        Some(StructuredTypeNodeView::RvalueReference(_))
553                    ),
554                )
555            })
556            .collect::<Vec<_>>();
557
558        assert_ne!(shapes[0].0, shapes[1].0);
559        assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
560        assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
561        assert_eq!(declarations, extract(source));
562    }
563
564    #[test]
565    fn preserves_callable_arities_for_defaulted_and_required_parameters() {
566        let declarations = extract(
567            r#"
568            namespace std {
569            template <class C, class Alloc> class basic_string {
570            public:
571                basic_string(const C*, const Alloc& = Alloc());
572                basic_string(const C&, const Alloc&);
573            };
574            }
575            "#,
576        );
577        let constructors = declarations
578            .members
579            .iter()
580            .filter(|member| {
581                member.owner.as_deref() == Some("std.basic_string") && member.name == "basic_string"
582            })
583            .collect::<Vec<_>>();
584
585        assert_eq!(constructors.len(), 2, "{declarations:#?}");
586        assert!(
587            constructors
588                .iter()
589                .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
590            "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
591        );
592        assert!(
593            constructors
594                .iter()
595                .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
596            "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
597        );
598    }
599
600    #[test]
601    fn preserves_callable_explicitness_for_implicit_binding() {
602        for (declaration, expected) in [
603            ("Widget(const char*);", CppCallableExplicitness::Implicit),
604            (
605                "explicit Widget(const char*);",
606                CppCallableExplicitness::Explicit,
607            ),
608            (
609                "explicit(true) Widget(const char*);",
610                CppCallableExplicitness::Conditional,
611            ),
612        ] {
613            let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
614            let member = declarations
615                .members
616                .first()
617                .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
618            assert_eq!(Some(expected), member.explicitness, "{member:#?}");
619        }
620    }
621
622    #[test]
623    fn preserves_callable_return_reference_shapes() {
624        let declarations = extract(
625            r#"
626            namespace std {
627            class basic_string {
628            public:
629                basic_string& operator=(const basic_string&);
630                basic_string&& operator=(basic_string&&);
631                void operator=(int);
632            };
633            }
634            "#,
635        );
636        let assignments = declarations
637            .members
638            .iter()
639            .filter(|member| member.name == "operator=")
640            .collect::<Vec<_>>();
641        assert_eq!(assignments.len(), 3, "{declarations:#?}");
642
643        let copy = assignments
644            .iter()
645            .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
646            .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
647        let Some(StructuredTypeNodeView::Reference(inner)) = copy
648            .return_type
649            .as_ref()
650            .and_then(|identity| identity.view(identity.root_id()))
651        else {
652            panic!("copy return type: {copy:#?}");
653        };
654        let Some(StructuredTypeNodeView::Named(name)) = copy
655            .return_type
656            .as_ref()
657            .and_then(|identity| identity.view(inner))
658        else {
659            panic!("copy return target: {copy:#?}");
660        };
661        assert_eq!(["basic_string"], name.path());
662        assert_eq!(["std", "basic_string"], name.lexical_scope());
663
664        let move_assignment = declarations
665            .members
666            .iter()
667            .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
668            .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
669        assert!(matches!(
670            move_assignment
671                .return_type
672                .as_ref()
673                .and_then(|identity| identity.view(identity.root_id())),
674            Some(StructuredTypeNodeView::RvalueReference(_))
675        ));
676
677        let void_assignment = declarations
678            .members
679            .iter()
680            .find(|member| member.signature.as_deref() == Some("(int)"))
681            .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
682        let Some(StructuredTypeNodeView::Named(name)) = void_assignment
683            .return_type
684            .as_ref()
685            .and_then(|identity| identity.view(identity.root_id()))
686        else {
687            panic!("void return type: {void_assignment:#?}");
688        };
689        assert_eq!(["void"], name.path());
690    }
691
692    #[test]
693    fn preserves_type_alias_underlying_structured_identity() {
694        let declarations = extract(
695            r#"
696            namespace std {
697            template<class C, class Traits, class Alloc> class basic_string;
698            using string = basic_string<char, char_traits<char>, allocator<char>>;
699            }
700            "#,
701        );
702        let alias = declarations
703            .types
704            .iter()
705            .find(|record| record.name == "std.string")
706            .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
707        assert!(alias.is_type_alias, "{alias:#?}");
708        let identity = alias
709            .underlying_type
710            .as_ref()
711            .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
712        let Some(StructuredTypeNodeView::Generic { base, arguments }) =
713            identity.view(identity.root_id())
714        else {
715            panic!("generic basic_string alias target: {identity:#?}");
716        };
717        assert_eq!(3, arguments.len());
718        let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
719            panic!("named basic_string alias base: {identity:#?}");
720        };
721        assert_eq!(["basic_string"], name.path());
722        assert_eq!(["std"], name.lexical_scope());
723    }
724
725    #[test]
726    fn keeps_same_short_names_under_distinct_owners() {
727        let declarations = extract(
728            "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
729        );
730        let mut owners = declarations
731            .members
732            .iter()
733            .filter(|member| member.name == "add")
734            .filter_map(|member| member.owner.clone())
735            .collect::<Vec<_>>();
736        owners.sort();
737
738        assert_eq!(vec!["first.box", "second.box"], owners);
739        assert!(
740            declarations
741                .members
742                .iter()
743                .filter(|member| member.name == "add")
744                .all(|member| member.visibility == CppExternalVisibility::Private)
745        );
746    }
747
748    #[test]
749    fn nested_type_visibility_follows_the_enclosing_access_section() {
750        let declarations = extract(
751            "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
752        );
753        assert!(declarations.types.iter().any(|record| {
754            record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
755        }));
756        assert!(declarations.types.iter().any(|record| {
757            record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
758        }));
759        assert!(declarations.types.iter().any(|record| {
760            record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
761        }));
762    }
763
764    #[test]
765    fn preprocessor_and_record_limits_make_the_surface_partial() {
766        let temp = tempfile::tempdir().expect("temp root");
767        let declarations = extract_external_declarations(
768            temp.path(),
769            Path::new("limited.hpp"),
770            "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
771            CppExternalDeclarationLimits { max_records: 1 },
772        );
773
774        assert_eq!(
775            CppExternalDeclarationCompleteness::Partial,
776            declarations.completeness
777        );
778        assert!(
779            declarations
780                .diagnostics
781                .iter()
782                .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
783        );
784        assert!(
785            declarations
786                .diagnostics
787                .iter()
788                .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
789        );
790    }
791}