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