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