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 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 if child.kind() == "access_specifier" {
481 visibility = match node_text(child, source).trim_end_matches(':').trim() {
482 "public" => CppExternalVisibility::Public,
483 "protected" => CppExternalVisibility::Protected,
484 "private" => CppExternalVisibility::Private,
485 _ => CppExternalVisibility::Private,
486 };
487 }
488 }
489 return visibility;
490 }
491 current = Some(parent);
492 }
493 CppExternalVisibility::Public
494}
495
496fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
497 while !matches!(
498 declarator.kind(),
499 "declaration" | "field_declaration" | "function_definition"
500 ) {
501 declarator = declarator.parent()?;
502 }
503 if declarator.has_error() {
504 return None;
505 }
506 let mut stack = vec![declarator];
507 let mut explicit = None;
508 while let Some(node) = stack.pop() {
509 if node.kind() == "explicit_function_specifier" {
510 if explicit.is_some() {
511 return None;
512 }
513 explicit = Some(if node.named_child_count() == 0 {
514 CppCallableExplicitness::Explicit
515 } else {
516 CppCallableExplicitness::Conditional
517 });
518 continue;
519 }
520 let mut cursor = node.walk();
521 stack.extend(node.named_children(&mut cursor));
522 }
523 Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
524}
525
526fn cpp_returns_forwarding_reference<'tree>(
527 declarator: Node<'tree>,
528 source: &str,
529 ancestry: &ParentIndex<'tree>,
530 parameters: Option<&[CppParameterType]>,
531 shapes: Option<&[CppComparableSlot]>,
532 returns: Option<&StructuredTypeIdentity>,
533) -> bool {
534 let (
535 Some([CppParameterType::Structured(parameter)]),
536 Some([CppComparableSlot::Shape(shape)]),
537 Some(returns),
538 ) = (parameters, shapes, returns)
539 else {
540 return false;
541 };
542 let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
543 else {
544 return false;
545 };
546 let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
547 return false;
548 };
549 let [parameter_name] = name.path() else {
550 return false;
551 };
552 let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
553 return false;
554 };
555 if !matches!(
556 shape.node(*inner),
557 CppComparableNode::Named {
558 konst: false,
559 volatil: false,
560 ..
561 }
562 ) || name.is_absolute()
563 || !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
564 || !matches!(
565 returns.view(returns.root_id()),
566 Some(StructuredTypeNodeView::RvalueReference(_))
567 )
568 {
569 return false;
570 }
571
572 let mut current = declarator;
575 while let Some(parent) = ancestry.parent(current) {
576 if matches!(
577 parent.kind(),
578 "function_definition" | "declaration" | "field_declaration"
579 ) {
580 let mut cursor = parent.walk();
581 if parent.named_children(&mut cursor).any(|child| {
582 child.kind() == "type_qualifier"
583 && matches!(node_text(child, source), "const" | "volatile")
584 }) {
585 return false;
586 }
587 return parent
588 .child_by_field_name("type")
589 .is_some_and(|return_node| {
590 cpp_forwarded_return_type(return_node, source, parameter_name)
591 });
592 }
593 current = parent;
594 }
595 false
596}
597
598fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
599 let mut components = Vec::new();
600 let mut removals = 0;
601 let mut stack = vec![node];
602 while let Some(current) = stack.pop() {
603 match current.kind() {
604 "identifier" | "type_identifier" | "namespace_identifier" => {
605 components.push(node_text(current, source));
606 }
607 "dependent_type" => {
608 if current.named_child_count() != 1 {
609 return false;
610 }
611 stack.push(current.named_child(0).expect("one named child"));
612 }
613 "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
614 let Some(name) = current.child_by_field_name("name") else {
615 return false;
616 };
617 stack.push(name);
618 if let Some(scope) = current.child_by_field_name("scope") {
619 stack.push(scope);
620 }
621 }
622 "template_type" => {
623 let (Some(name), Some(arguments)) = (
624 current.child_by_field_name("name"),
625 current.child_by_field_name("arguments"),
626 ) else {
627 return false;
628 };
629 if !matches!(name.kind(), "identifier" | "type_identifier")
630 || node_text(name, source) != "remove_reference"
631 || arguments.named_child_count() != 1
632 {
633 return false;
634 }
635 let mut argument = arguments.named_child(0).expect("one named argument");
636 if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
637 argument = argument.named_child(0).expect("one named type");
638 }
639 if !matches!(argument.kind(), "identifier" | "type_identifier")
640 || node_text(argument, source) != parameter
641 {
642 return false;
643 }
644 removals += 1;
645 stack.push(name);
646 }
647 _ => return false,
648 }
649 }
650 (removals == 0 && components == [parameter])
651 || (removals == 1
652 && (components == ["std", "remove_reference", "type"]
653 || components == ["remove_reference", "type"]))
654}
655
656fn nearest_declaration_owner(
657 declaration: &CodeUnit,
658 parent_by_child: &HashMap<CodeUnit, CodeUnit>,
659) -> Option<CppExternalOwner> {
660 let mut current = declaration;
661 while let Some(parent) = parent_by_child.get(current) {
662 match parent.kind() {
663 CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
664 CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
665 _ => {}
666 }
667 current = parent;
668 }
669 None
670}
671
672fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
673 let mut stack = vec![root];
674 while let Some(node) = stack.pop() {
675 if matches!(
676 node.kind(),
677 "preproc_def"
678 | "preproc_function_def"
679 | "preproc_if"
680 | "preproc_ifdef"
681 | "preproc_ifndef"
682 | "preproc_elif"
683 | "preproc_else"
684 ) {
685 return true;
686 }
687 let mut cursor = node.walk();
688 stack.extend(node.named_children(&mut cursor));
689 }
690 false
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use brokk_bifrost_core::analyzer::model::CallableArity;
697 use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
698
699 fn extract(source: &str) -> CppExternalDeclarationSet {
700 let temp = tempfile::tempdir().expect("temp root");
701 extract_external_declarations(
702 temp.path(),
703 Path::new("vector"),
704 source,
705 CppExternalDeclarationLimits::default(),
706 )
707 }
708
709 #[test]
710 fn extracts_literal_include_paths_with_delimiters() {
711 let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
712 assert_eq!(
713 vec![
714 (PathBuf::from("local.hpp"), true),
715 (PathBuf::from("vector"), false),
716 ],
717 external_include_paths(source)
718 );
719 assert_eq!(
720 vec![PathBuf::from("vector")],
721 external_angle_include_paths(source)
722 );
723 let mut parser = Parser::new();
724 parser
725 .set_language(&tree_sitter_cpp::LANGUAGE.into())
726 .expect("C++ grammar");
727 let tree = parser.parse(source, None).expect("tree");
728 assert_eq!(
729 vec![PathBuf::from("vector")],
730 external_angle_include_paths_from_root(source, tree.root_node())
731 );
732 }
733
734 #[test]
735 fn extracts_namespaced_template_type_and_owned_members() {
736 let declarations = extract(
737 r#"
738 namespace std {
739 template <typename T> class vector : public sequence<T> {
740 public:
741 vector();
742 void push_back(const T& value);
743 T size;
744 };
745 }
746 "#,
747 );
748
749 assert_eq!(
750 CppExternalDeclarationCompleteness::Complete,
751 declarations.completeness
752 );
753 assert!(
754 declarations.types.iter().any(|record| {
755 record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
756 }),
757 "{declarations:#?}"
758 );
759 assert!(
760 declarations.members.iter().any(|record| {
761 record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
762 && record.name == "push_back"
763 && record.visibility == CppExternalVisibility::Public
764 }),
765 "{declarations:#?}"
766 );
767 assert!(
768 declarations.members.iter().any(|record| {
769 record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
770 && record.name == "size"
771 }),
772 "{declarations:#?}"
773 );
774 }
775
776 #[test]
777 fn preserves_copy_and_move_reference_parameter_shapes() {
778 let source = r#"
779 class Widget {
780 public:
781 Widget(const Widget&);
782 Widget(Widget&&);
783 };
784 "#;
785 let declarations = extract(source);
786 let constructors = declarations
787 .members
788 .iter()
789 .filter(|member| {
790 member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
791 && member.name == "Widget"
792 })
793 .collect::<Vec<_>>();
794
795 assert_eq!(constructors.len(), 2, "{declarations:#?}");
796 let shapes = constructors
797 .iter()
798 .map(|constructor| {
799 let parameters = constructor
800 .parameter_types
801 .as_ref()
802 .expect("constructor parameters");
803 assert_eq!(parameters.len(), 1, "{constructor:#?}");
804 let CppParameterType::Structured(identity) = ¶meters[0] else {
805 panic!("constructor parameter shape: {constructor:#?}");
806 };
807 (
808 identity.clone(),
809 matches!(
810 identity.view(identity.root_id()),
811 Some(StructuredTypeNodeView::Reference(_))
812 ),
813 matches!(
814 identity.view(identity.root_id()),
815 Some(StructuredTypeNodeView::RvalueReference(_))
816 ),
817 )
818 })
819 .collect::<Vec<_>>();
820
821 assert_ne!(shapes[0].0, shapes[1].0);
822 assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
823 assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
824 assert_eq!(declarations, extract(source));
825 }
826
827 #[test]
828 fn preserves_callable_arities_for_defaulted_and_required_parameters() {
829 let declarations = extract(
830 r#"
831 namespace std {
832 template <class C, class Alloc> class basic_string {
833 public:
834 basic_string(const C*, const Alloc& = Alloc());
835 basic_string(const C&, const Alloc&);
836 };
837 }
838 "#,
839 );
840 let constructors = declarations
841 .members
842 .iter()
843 .filter(|member| {
844 member.owner.as_ref().and_then(CppExternalOwner::type_name)
845 == Some("std.basic_string")
846 && member.name == "basic_string"
847 })
848 .collect::<Vec<_>>();
849
850 assert_eq!(constructors.len(), 2, "{declarations:#?}");
851 assert!(
852 constructors
853 .iter()
854 .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
855 "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
856 );
857 assert!(
858 constructors
859 .iter()
860 .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
861 "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
862 );
863 }
864
865 #[test]
866 fn preserves_callable_parameter_names() {
867 let declarations = extract("int connect(int fd, const char *address, unsigned length);\n");
868 let connect_candidates = declarations
869 .members
870 .iter()
871 .filter(|member| member.name == "connect")
872 .collect::<Vec<_>>();
873 let [connect] = connect_candidates.as_slice() else {
874 panic!("connect declaration: {declarations:#?}");
875 };
876 assert_eq!(
877 Some(vec![
878 Some("fd".to_owned()),
879 Some("address".to_owned()),
880 Some("length".to_owned()),
881 ]),
882 connect.parameter_names,
883 "formal names must remain aligned with the declaration's argument slots"
884 );
885 }
886
887 #[test]
888 fn preserves_callable_explicitness_for_implicit_binding() {
889 for (declaration, expected) in [
890 ("Widget(const char*);", CppCallableExplicitness::Implicit),
891 (
892 "explicit Widget(const char*);",
893 CppCallableExplicitness::Explicit,
894 ),
895 (
896 "explicit(true) Widget(const char*);",
897 CppCallableExplicitness::Conditional,
898 ),
899 ] {
900 let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
901 let member = declarations
902 .members
903 .first()
904 .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
905 assert_eq!(Some(expected), member.explicitness, "{member:#?}");
906 }
907 }
908
909 #[test]
910 fn preserves_callable_return_reference_shapes() {
911 let declarations = extract(
912 r#"
913 namespace std {
914 class basic_string {
915 public:
916 basic_string& operator=(const basic_string&);
917 basic_string&& operator=(basic_string&&);
918 void operator=(int);
919 };
920 }
921 "#,
922 );
923 let assignments = declarations
924 .members
925 .iter()
926 .filter(|member| member.name == "operator=")
927 .collect::<Vec<_>>();
928 assert_eq!(assignments.len(), 3, "{declarations:#?}");
929
930 let copy = assignments
931 .iter()
932 .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
933 .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
934 let Some(StructuredTypeNodeView::Reference(inner)) = copy
935 .return_type
936 .as_ref()
937 .and_then(|identity| identity.view(identity.root_id()))
938 else {
939 panic!("copy return type: {copy:#?}");
940 };
941 let Some(StructuredTypeNodeView::Named(name)) = copy
942 .return_type
943 .as_ref()
944 .and_then(|identity| identity.view(inner))
945 else {
946 panic!("copy return target: {copy:#?}");
947 };
948 assert_eq!(["basic_string"], name.path());
949 assert_eq!(["std", "basic_string"], name.lexical_scope());
950
951 let move_assignment = declarations
952 .members
953 .iter()
954 .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
955 .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
956 assert!(matches!(
957 move_assignment
958 .return_type
959 .as_ref()
960 .and_then(|identity| identity.view(identity.root_id())),
961 Some(StructuredTypeNodeView::RvalueReference(_))
962 ));
963
964 let void_assignment = declarations
965 .members
966 .iter()
967 .find(|member| member.signature.as_deref() == Some("(int)"))
968 .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
969 let Some(StructuredTypeNodeView::Named(name)) = void_assignment
970 .return_type
971 .as_ref()
972 .and_then(|identity| identity.view(identity.root_id()))
973 else {
974 panic!("void return type: {void_assignment:#?}");
975 };
976 assert_eq!(["void"], name.path());
977 }
978
979 #[test]
980 fn preserves_type_alias_underlying_structured_identity() {
981 let declarations = extract(
982 r#"
983 namespace std {
984 template<class C, class Traits, class Alloc> class basic_string;
985 using string = basic_string<char, char_traits<char>, allocator<char>>;
986 }
987 "#,
988 );
989 let alias = declarations
990 .types
991 .iter()
992 .find(|record| record.name == "std.string")
993 .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
994 assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
995 let identity = alias
996 .underlying_type
997 .as_ref()
998 .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
999 let Some(StructuredTypeNodeView::Generic { base, arguments }) =
1000 identity.view(identity.root_id())
1001 else {
1002 panic!("generic basic_string alias target: {identity:#?}");
1003 };
1004 assert_eq!(3, arguments.len());
1005 let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
1006 panic!("named basic_string alias base: {identity:#?}");
1007 };
1008 assert_eq!(["basic_string"], name.path());
1009 assert_eq!(["std"], name.lexical_scope());
1010 }
1011
1012 #[test]
1013 fn keeps_same_short_names_under_distinct_owners() {
1014 let declarations = extract(
1015 "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
1016 );
1017 let mut owners = declarations
1018 .members
1019 .iter()
1020 .filter(|member| member.name == "add")
1021 .filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
1022 .collect::<Vec<_>>();
1023 owners.sort();
1024
1025 assert_eq!(vec!["first.box", "second.box"], owners);
1026 assert!(
1027 declarations
1028 .members
1029 .iter()
1030 .filter(|member| member.name == "add")
1031 .all(|member| member.visibility == CppExternalVisibility::Private)
1032 );
1033 }
1034
1035 #[test]
1036 fn nested_type_visibility_follows_the_enclosing_access_section() {
1037 let declarations = extract(
1038 "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
1039 );
1040 assert!(declarations.types.iter().any(|record| {
1041 record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
1042 }));
1043 assert!(declarations.types.iter().any(|record| {
1044 record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
1045 }));
1046 assert!(declarations.types.iter().any(|record| {
1047 record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
1048 }));
1049 }
1050
1051 #[test]
1052 fn preprocessor_and_record_limits_make_the_surface_partial() {
1053 let temp = tempfile::tempdir().expect("temp root");
1054 let declarations = extract_external_declarations(
1055 temp.path(),
1056 Path::new("limited.hpp"),
1057 "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
1058 CppExternalDeclarationLimits { max_records: 1 },
1059 );
1060
1061 assert_eq!(
1062 CppExternalDeclarationCompleteness::Partial,
1063 declarations.completeness
1064 );
1065 assert!(
1066 declarations
1067 .diagnostics
1068 .iter()
1069 .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
1070 );
1071 assert!(
1072 declarations
1073 .diagnostics
1074 .iter()
1075 .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
1076 );
1077 }
1078}