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