1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum CppExternalTypeKind {
65 Class,
66 Namespace,
67 TypeAlias,
68}
69
70#[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 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 pub parameter_types: Option<Vec<CppParameterType>>,
128 pub parameter_shapes: Option<Vec<CppComparableSlot>>,
130 pub callable_arity: Option<CallableArity>,
133 pub explicitness: Option<CppCallableExplicitness>,
135 pub return_type: Option<StructuredTypeIdentity>,
136 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
164pub 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
178pub 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
219pub 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 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 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) = ¶meters[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}