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
314 .entry(child.clone())
315 .and_modify(|owner: &mut CodeUnit| {
316 if parent.is_class() && !owner.is_class() {
317 *owner = parent.clone();
318 }
319 })
320 .or_insert_with(|| parent.clone());
321 }
322 }
323
324 let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
325 declarations.sort_by_key(|declaration| {
326 (
327 declaration.fq_name(),
328 declaration.kind(),
329 declaration.signature().map(str::to_owned),
330 )
331 });
332
333 let ancestry = ParentIndex::new(tree.root_node());
336 let mut types = Vec::new();
337 let mut members = Vec::new();
338 for declaration in declarations {
339 if types.len().saturating_add(members.len()) >= limits.max_records {
340 completeness = CppExternalDeclarationCompleteness::Partial;
341 syntax_complete = false;
342 diagnostics.push(CppExternalDeclarationDiagnostic {
343 code: "cpp.external.record_limit",
344 message: format!(
345 "external header `{}` exceeded the declaration record limit",
346 source_path.display()
347 ),
348 });
349 break;
350 }
351 match declaration.kind() {
352 CodeUnitType::Module => types.push(CppExternalType {
353 name: declaration.fq_name(),
354 source_name: cpp_name_for(&declaration),
355 kind: CppExternalTypeKind::Namespace,
356 underlying_type: None,
357 template_metadata: None,
358 visibility: CppExternalVisibility::Public,
359 source_path: source_path.to_path_buf(),
360 direct_bases: Vec::new(),
361 }),
362 CodeUnitType::Class => types.push(CppExternalType {
363 name: declaration.fq_name(),
364 source_name: cpp_name_for(&declaration),
365 kind: if parsed.type_aliases.contains(&declaration) {
366 CppExternalTypeKind::TypeAlias
367 } else {
368 CppExternalTypeKind::Class
369 },
370 underlying_type: parsed
371 .signature_metadata
372 .get(&declaration)
373 .and_then(|records| records.first())
374 .and_then(|metadata| metadata.underlying_type_identity())
375 .cloned(),
376 template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
377 visibility: parsed
378 .ranges
379 .get(&declaration)
380 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
381 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
382 .unwrap_or(CppExternalVisibility::Private),
383 source_path: source_path.to_path_buf(),
384 direct_bases: parsed
385 .raw_supertypes
386 .get(&declaration)
387 .cloned()
388 .unwrap_or_default(),
389 }),
390 CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
391 let metadata = parsed
392 .signature_metadata
393 .get(&declaration)
394 .and_then(|records| records.first());
395 let declaration_start = parsed
396 .ranges
397 .get(&declaration)
398 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
399 let function_declarator = (declaration.kind() == CodeUnitType::Function)
400 .then(|| {
401 declaration_start
402 .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
403 })
404 .flatten();
405 let parameter_types = function_declarator.map(|declarator| {
406 cpp_callable_parameter_type_identities(declarator, source, &ancestry)
407 });
408 let parameter_names = function_declarator.and_then(|declarator| {
409 declarator
410 .child_by_field_name("parameters")
411 .map(|parameters| cpp_callable_parameter_names(parameters, source))
412 });
413 let parameter_shapes = function_declarator.map(|declarator| {
414 cpp_comparable_parameter_shapes(declarator, source, &ancestry)
415 });
416 let return_type = metadata
417 .and_then(|metadata| metadata.return_type_identity())
418 .cloned()
419 .or_else(|| {
420 function_declarator.and_then(|declarator| {
421 cpp_callable_return_type_identity(declarator, source, &ancestry)
422 })
423 });
424 let returns_forwarding_reference = function_declarator.is_some_and(|declarator| {
425 cpp_returns_forwarding_reference(
426 declarator,
427 source,
428 &ancestry,
429 parameter_types.as_deref(),
430 parameter_shapes.as_deref(),
431 return_type.as_ref(),
432 )
433 });
434 members.push(CppExternalMember {
435 owner: nearest_declaration_owner(&declaration, &parent_by_child),
436 name: declaration.terminal_name().to_owned(),
437 qualified_name: cpp_name_for(&declaration),
438 kind: match declaration.kind() {
439 CodeUnitType::Function => CppExternalMemberKind::Function,
440 CodeUnitType::Field => CppExternalMemberKind::Field,
441 CodeUnitType::Macro => CppExternalMemberKind::Macro,
442 _ => unreachable!("the outer match admits exactly member kinds"),
443 },
444 visibility: declaration_start
445 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
446 .unwrap_or(CppExternalVisibility::Private),
447 is_constructor: metadata
448 .is_some_and(|metadata| metadata.callable_is_constructor()),
449 signature: declaration.signature().map(str::to_owned),
450 parameter_types,
451 parameter_shapes,
452 callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
453 parameter_names,
454 explicitness: function_declarator.and_then(cpp_callable_explicitness),
455 return_type,
456 returns_forwarding_reference,
457 source_path: source_path.to_path_buf(),
458 });
459 }
460 CodeUnitType::FileScope => {}
461 }
462 }
463
464 CppExternalDeclarationSet {
465 types,
466 members,
467 completeness,
468 syntax_complete,
469 diagnostics,
470 }
471}
472
473fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
474 let mut current = root.descendant_for_byte_range(start_byte, start_byte);
475 while let Some(node) = current {
476 let Some(parent) = node.parent() else {
477 break;
478 };
479 if parent.kind() == "field_declaration_list" {
480 let default = match parent.parent().map(|owner| owner.kind()) {
481 Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
482 _ => CppExternalVisibility::Private,
483 };
484 let mut visibility = default;
485 let mut cursor = parent.walk();
486 for child in parent.named_children(&mut cursor) {
487 if child.start_byte() > start_byte {
488 break;
489 }
490 let access = if child.kind() == "access_specifier" {
491 Some(node_text(child, source).trim_end_matches(':').trim())
492 } else {
493 cpp_macro_swallowed_access_label(child, source).map(|label| label.access)
494 };
495 if let Some(access) = access {
496 visibility = match access {
497 "public" => CppExternalVisibility::Public,
498 "protected" => CppExternalVisibility::Protected,
499 "private" => CppExternalVisibility::Private,
500 _ => CppExternalVisibility::Private,
501 };
502 }
503 }
504 return visibility;
505 }
506 current = Some(parent);
507 }
508 CppExternalVisibility::Public
509}
510
511fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
512 while !matches!(
513 declarator.kind(),
514 "declaration" | "field_declaration" | "function_definition"
515 ) {
516 declarator = declarator.parent()?;
517 }
518 if declarator.has_error() {
519 return None;
520 }
521 let mut stack = vec![declarator];
522 let mut explicit = None;
523 while let Some(node) = stack.pop() {
524 if node.kind() == "explicit_function_specifier" {
525 if explicit.is_some() {
526 return None;
527 }
528 explicit = Some(if node.named_child_count() == 0 {
529 CppCallableExplicitness::Explicit
530 } else {
531 CppCallableExplicitness::Conditional
532 });
533 continue;
534 }
535 let mut cursor = node.walk();
536 stack.extend(node.named_children(&mut cursor));
537 }
538 Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
539}
540
541fn cpp_returns_forwarding_reference<'tree>(
542 declarator: Node<'tree>,
543 source: &str,
544 ancestry: &ParentIndex<'tree>,
545 parameters: Option<&[CppParameterType]>,
546 shapes: Option<&[CppComparableSlot]>,
547 returns: Option<&StructuredTypeIdentity>,
548) -> bool {
549 let (
550 Some([CppParameterType::Structured(parameter)]),
551 Some([CppComparableSlot::Shape(shape)]),
552 Some(returns),
553 ) = (parameters, shapes, returns)
554 else {
555 return false;
556 };
557 let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
558 else {
559 return false;
560 };
561 let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
562 return false;
563 };
564 let [parameter_name] = name.path() else {
565 return false;
566 };
567 let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
568 return false;
569 };
570 if !matches!(
571 shape.node(*inner),
572 CppComparableNode::Named {
573 konst: false,
574 volatil: false,
575 ..
576 }
577 ) || name.is_absolute()
578 || !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
579 || !matches!(
580 returns.view(returns.root_id()),
581 Some(StructuredTypeNodeView::RvalueReference(_))
582 )
583 {
584 return false;
585 }
586
587 let mut current = declarator;
590 while let Some(parent) = ancestry.parent(current) {
591 if matches!(
592 parent.kind(),
593 "function_definition" | "declaration" | "field_declaration"
594 ) {
595 let mut cursor = parent.walk();
596 if parent.named_children(&mut cursor).any(|child| {
597 child.kind() == "type_qualifier"
598 && matches!(node_text(child, source), "const" | "volatile")
599 }) {
600 return false;
601 }
602 return parent
603 .child_by_field_name("type")
604 .is_some_and(|return_node| {
605 cpp_forwarded_return_type(return_node, source, parameter_name)
606 });
607 }
608 current = parent;
609 }
610 false
611}
612
613fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
614 let mut components = Vec::new();
615 let mut removals = 0;
616 let mut stack = vec![node];
617 while let Some(current) = stack.pop() {
618 match current.kind() {
619 "identifier" | "type_identifier" | "namespace_identifier" => {
620 components.push(node_text(current, source));
621 }
622 "dependent_type" => {
623 if current.named_child_count() != 1 {
624 return false;
625 }
626 stack.push(current.named_child(0).expect("one named child"));
627 }
628 "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
629 let Some(name) = current.child_by_field_name("name") else {
630 return false;
631 };
632 stack.push(name);
633 if let Some(scope) = current.child_by_field_name("scope") {
634 stack.push(scope);
635 }
636 }
637 "template_type" => {
638 let (Some(name), Some(arguments)) = (
639 current.child_by_field_name("name"),
640 current.child_by_field_name("arguments"),
641 ) else {
642 return false;
643 };
644 if !matches!(name.kind(), "identifier" | "type_identifier")
645 || node_text(name, source) != "remove_reference"
646 || arguments.named_child_count() != 1
647 {
648 return false;
649 }
650 let mut argument = arguments.named_child(0).expect("one named argument");
651 if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
652 argument = argument.named_child(0).expect("one named type");
653 }
654 if !matches!(argument.kind(), "identifier" | "type_identifier")
655 || node_text(argument, source) != parameter
656 {
657 return false;
658 }
659 removals += 1;
660 stack.push(name);
661 }
662 _ => return false,
663 }
664 }
665 (removals == 0 && components == [parameter])
666 || (removals == 1
667 && (components == ["std", "remove_reference", "type"]
668 || components == ["remove_reference", "type"]))
669}
670
671fn nearest_declaration_owner(
672 declaration: &CodeUnit,
673 parent_by_child: &HashMap<CodeUnit, CodeUnit>,
674) -> Option<CppExternalOwner> {
675 let mut current = declaration;
676 while let Some(parent) = parent_by_child.get(current) {
677 match parent.kind() {
678 CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
679 CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
680 _ => {}
681 }
682 current = parent;
683 }
684 None
685}
686
687fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
688 let mut stack = vec![root];
689 while let Some(node) = stack.pop() {
690 if matches!(
691 node.kind(),
692 "preproc_def"
693 | "preproc_function_def"
694 | "preproc_if"
695 | "preproc_ifdef"
696 | "preproc_ifndef"
697 | "preproc_elif"
698 | "preproc_else"
699 ) {
700 return true;
701 }
702 let mut cursor = node.walk();
703 stack.extend(node.named_children(&mut cursor));
704 }
705 false
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use brokk_bifrost_core::analyzer::model::CallableArity;
712 use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
713 use brokk_bifrost_test_macros::hardwon;
714
715 fn extract(source: &str) -> CppExternalDeclarationSet {
716 let temp = tempfile::tempdir().expect("temp root");
717 extract_external_declarations(
718 temp.path(),
719 Path::new("vector"),
720 source,
721 CppExternalDeclarationLimits::default(),
722 )
723 }
724
725 #[test]
726 fn extracts_literal_include_paths_with_delimiters() {
727 let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
728 assert_eq!(
729 vec![
730 (PathBuf::from("local.hpp"), true),
731 (PathBuf::from("vector"), false),
732 ],
733 external_include_paths(source)
734 );
735 assert_eq!(
736 vec![PathBuf::from("vector")],
737 external_angle_include_paths(source)
738 );
739 let mut parser = Parser::new();
740 parser
741 .set_language(&tree_sitter_cpp::LANGUAGE.into())
742 .expect("C++ grammar");
743 let tree = parser.parse(source, None).expect("tree");
744 assert_eq!(
745 vec![PathBuf::from("vector")],
746 external_angle_include_paths_from_root(source, tree.root_node())
747 );
748 }
749
750 #[test]
751 fn extracts_namespaced_template_type_and_owned_members() {
752 let declarations = extract(
753 r#"
754 namespace std {
755 template <typename T> class vector : public sequence<T> {
756 public:
757 vector();
758 void push_back(const T& value);
759 T size;
760 };
761 }
762 "#,
763 );
764
765 assert_eq!(
766 CppExternalDeclarationCompleteness::Complete,
767 declarations.completeness
768 );
769 assert!(
770 declarations.types.iter().any(|record| {
771 record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
772 }),
773 "{declarations:#?}"
774 );
775 assert!(
776 declarations.members.iter().any(|record| {
777 record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
778 && record.name == "push_back"
779 && record.visibility == CppExternalVisibility::Public
780 }),
781 "{declarations:#?}"
782 );
783 assert!(
784 declarations.members.iter().any(|record| {
785 record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
786 && record.name == "size"
787 }),
788 "{declarations:#?}"
789 );
790 }
791
792 #[test]
793 fn preserves_copy_and_move_reference_parameter_shapes() {
794 let source = r#"
795 class Widget {
796 public:
797 Widget(const Widget&);
798 Widget(Widget&&);
799 };
800 "#;
801 let declarations = extract(source);
802 let constructors = declarations
803 .members
804 .iter()
805 .filter(|member| {
806 member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
807 && member.name == "Widget"
808 })
809 .collect::<Vec<_>>();
810
811 assert_eq!(constructors.len(), 2, "{declarations:#?}");
812 let shapes = constructors
813 .iter()
814 .map(|constructor| {
815 let parameters = constructor
816 .parameter_types
817 .as_ref()
818 .expect("constructor parameters");
819 assert_eq!(parameters.len(), 1, "{constructor:#?}");
820 let CppParameterType::Structured(identity) = ¶meters[0] else {
821 panic!("constructor parameter shape: {constructor:#?}");
822 };
823 (
824 identity.clone(),
825 matches!(
826 identity.view(identity.root_id()),
827 Some(StructuredTypeNodeView::Reference(_))
828 ),
829 matches!(
830 identity.view(identity.root_id()),
831 Some(StructuredTypeNodeView::RvalueReference(_))
832 ),
833 )
834 })
835 .collect::<Vec<_>>();
836
837 assert_ne!(shapes[0].0, shapes[1].0);
838 assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
839 assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
840 assert_eq!(declarations, extract(source));
841 }
842
843 #[test]
844 fn preserves_callable_arities_for_defaulted_and_required_parameters() {
845 let declarations = extract(
846 r#"
847 namespace std {
848 template <class C, class Alloc> class basic_string {
849 public:
850 basic_string(const C*, const Alloc& = Alloc());
851 basic_string(const C&, const Alloc&);
852 };
853 }
854 "#,
855 );
856 let constructors = declarations
857 .members
858 .iter()
859 .filter(|member| {
860 member.owner.as_ref().and_then(CppExternalOwner::type_name)
861 == Some("std.basic_string")
862 && member.name == "basic_string"
863 })
864 .collect::<Vec<_>>();
865
866 assert_eq!(constructors.len(), 2, "{declarations:#?}");
867 assert!(
868 constructors
869 .iter()
870 .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
871 "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
872 );
873 assert!(
874 constructors
875 .iter()
876 .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
877 "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
878 );
879 }
880
881 #[test]
884 #[hardwon(
885 "6ec52f232835: quicksilver round 4 survivor review: lexical namespaces stole out-of-line exported member owners"
886 )]
887 fn recovered_external_api_manifest() {
888 let declarations = extract(
889 r#"
890#define EXPORT_MARKER
891namespace n { class Widget; int damaged = ; }
892class EXPORT_MARKER Widget { public: void first(); };
893class EXPORT(2,0) Attributed final {
894public:
895 ATTRIBUTE("old") int value();
896 void following();
897};
898namespace a::a {
899struct C { void f(); void f()const; void g()&; void g()&&; };
900void ::a::a::C::f() {}
901void ::a::a::C::f()const {}
902void C::g()& {}
903void C::g()&& {}
904}
905DEPRECATED /* between name and arguments */ (
906 EXPORT_MARKER int old(int value), "hint"
907);
908EXPORT_MARKER int next();
909void after() {}
910"#,
911 );
912 let mut types = declarations
913 .types
914 .iter()
915 .map(|record| record.name.as_str())
916 .collect::<Vec<_>>();
917 types.sort_unstable();
918 assert_eq!(
919 types,
920 ["Attributed", "a", "a::a", "a::a.C", "n", "n.Widget"]
921 );
922 let mut callables = declarations
923 .members
924 .iter()
925 .filter(|record| record.kind == CppExternalMemberKind::Function)
926 .map(|record| {
927 (
928 record.owner.as_ref().map(CppExternalOwner::name),
929 record.name.as_str(),
930 record.signature.as_deref(),
931 )
932 })
933 .collect::<Vec<_>>();
934 callables.sort_unstable();
935 let mut expected = vec![
936 (None, "after", Some("()")),
937 (None, "next", Some("()")),
938 (None, "old", Some("(int)")),
939 (Some("Attributed"), "following", Some("()")),
940 (Some("Attributed"), "value", Some("()")),
941 (Some("a::a.C"), "f", Some("()")),
942 (Some("a::a.C"), "f", Some("() const")),
943 (Some("a::a.C"), "g", Some("() &")),
944 (Some("a::a.C"), "g", Some("() &&")),
945 (Some("n.Widget"), "first", Some("()")),
946 ];
947 expected.sort_unstable();
948 assert_eq!(callables, expected);
949 }
950
951 #[test]
955 fn preserves_callable_parameter_names() {
956 let declarations = extract(
957 "struct Owner {};\n\
958 void connect(int fd, const char *address, unsigned length);\n\
959 void callbacks(void (*callback)(int argument), int array[extent], int (Owner::*member)(int nested));\n",
960 );
961 for (name, expected) in [
962 ("connect", vec![Some("fd"), Some("address"), Some("length")]),
963 (
964 "callbacks",
965 vec![Some("callback"), Some("array"), Some("member")],
966 ),
967 ] {
968 let candidates = declarations
969 .members
970 .iter()
971 .filter(|member| member.name == name)
972 .collect::<Vec<_>>();
973 let [member] = candidates.as_slice() else {
974 panic!("{name} declaration: {declarations:#?}");
975 };
976 assert_eq!(
977 member.parameter_names,
978 Some(
979 expected
980 .into_iter()
981 .map(|name| name.map(str::to_owned))
982 .collect()
983 ),
984 "{name}: outer parameter slots must agree with the API manifest"
985 );
986 }
987 }
988
989 #[test]
990 #[brokk_bifrost_test_macros::hardwon(
991 "5e79db920891: quicksilver round 4 survivor tests: unnamed formals inherit nested names"
992 )]
993 fn unnamed_callable_parameter_names() {
994 let declarations = extract("void unnamed(void (*)(int argument), int [extent]);\n");
995 let [member] = declarations.members.as_slice() else {
996 panic!("one exported callable: {declarations:#?}");
997 };
998 assert_eq!(member.parameter_names, Some(vec![None, None]));
999 }
1000
1001 #[test]
1002 fn preserves_callable_explicitness_for_implicit_binding() {
1003 for (declaration, expected) in [
1004 ("Widget(const char*);", CppCallableExplicitness::Implicit),
1005 (
1006 "explicit Widget(const char*);",
1007 CppCallableExplicitness::Explicit,
1008 ),
1009 (
1010 "explicit(true) Widget(const char*);",
1011 CppCallableExplicitness::Conditional,
1012 ),
1013 ] {
1014 let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
1015 let member = declarations
1016 .members
1017 .first()
1018 .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
1019 assert_eq!(Some(expected), member.explicitness, "{member:#?}");
1020 }
1021 }
1022
1023 #[test]
1024 fn preserves_callable_return_reference_shapes() {
1025 let declarations = extract(
1026 r#"
1027 namespace std {
1028 class basic_string {
1029 public:
1030 basic_string& operator=(const basic_string&);
1031 basic_string&& operator=(basic_string&&);
1032 void operator=(int);
1033 };
1034 }
1035 "#,
1036 );
1037 let assignments = declarations
1038 .members
1039 .iter()
1040 .filter(|member| member.name == "operator=")
1041 .collect::<Vec<_>>();
1042 assert_eq!(assignments.len(), 3, "{declarations:#?}");
1043
1044 let copy = assignments
1045 .iter()
1046 .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
1047 .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
1048 let Some(StructuredTypeNodeView::Reference(inner)) = copy
1049 .return_type
1050 .as_ref()
1051 .and_then(|identity| identity.view(identity.root_id()))
1052 else {
1053 panic!("copy return type: {copy:#?}");
1054 };
1055 let Some(StructuredTypeNodeView::Named(name)) = copy
1056 .return_type
1057 .as_ref()
1058 .and_then(|identity| identity.view(inner))
1059 else {
1060 panic!("copy return target: {copy:#?}");
1061 };
1062 assert_eq!(["basic_string"], name.path());
1063 assert_eq!(["std", "basic_string"], name.lexical_scope());
1064
1065 let move_assignment = declarations
1066 .members
1067 .iter()
1068 .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
1069 .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
1070 assert!(matches!(
1071 move_assignment
1072 .return_type
1073 .as_ref()
1074 .and_then(|identity| identity.view(identity.root_id())),
1075 Some(StructuredTypeNodeView::RvalueReference(_))
1076 ));
1077
1078 let void_assignment = declarations
1079 .members
1080 .iter()
1081 .find(|member| member.signature.as_deref() == Some("(int)"))
1082 .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
1083 let Some(StructuredTypeNodeView::Named(name)) = void_assignment
1084 .return_type
1085 .as_ref()
1086 .and_then(|identity| identity.view(identity.root_id()))
1087 else {
1088 panic!("void return type: {void_assignment:#?}");
1089 };
1090 assert_eq!(["void"], name.path());
1091 }
1092
1093 #[test]
1094 fn preserves_type_alias_underlying_structured_identity() {
1095 let declarations = extract(
1096 r#"
1097 namespace std {
1098 template<class C, class Traits, class Alloc> class basic_string;
1099 using string = basic_string<char, char_traits<char>, allocator<char>>;
1100 }
1101 "#,
1102 );
1103 let alias = declarations
1104 .types
1105 .iter()
1106 .find(|record| record.name == "std.string")
1107 .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
1108 assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
1109 let identity = alias
1110 .underlying_type
1111 .as_ref()
1112 .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
1113 let Some(StructuredTypeNodeView::Generic { base, arguments }) =
1114 identity.view(identity.root_id())
1115 else {
1116 panic!("generic basic_string alias target: {identity:#?}");
1117 };
1118 assert_eq!(3, arguments.len());
1119 let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
1120 panic!("named basic_string alias base: {identity:#?}");
1121 };
1122 assert_eq!(["basic_string"], name.path());
1123 assert_eq!(["std"], name.lexical_scope());
1124 }
1125
1126 #[test]
1127 fn keeps_same_short_names_under_distinct_owners() {
1128 let declarations = extract(
1129 "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
1130 );
1131 let mut owners = declarations
1132 .members
1133 .iter()
1134 .filter(|member| member.name == "add")
1135 .filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
1136 .collect::<Vec<_>>();
1137 owners.sort();
1138
1139 assert_eq!(vec!["first.box", "second.box"], owners);
1140 assert!(
1141 declarations
1142 .members
1143 .iter()
1144 .filter(|member| member.name == "add")
1145 .all(|member| member.visibility == CppExternalVisibility::Private)
1146 );
1147 }
1148
1149 #[test]
1150 fn nested_type_visibility_follows_the_enclosing_access_section() {
1151 let declarations = extract(
1152 "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
1153 );
1154 assert!(declarations.types.iter().any(|record| {
1155 record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
1156 }));
1157 assert!(declarations.types.iter().any(|record| {
1158 record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
1159 }));
1160 assert!(declarations.types.iter().any(|record| {
1161 record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
1162 }));
1163 }
1164
1165 #[test]
1166 fn preprocessor_and_record_limits_make_the_surface_partial() {
1167 let temp = tempfile::tempdir().expect("temp root");
1168 let declarations = extract_external_declarations(
1169 temp.path(),
1170 Path::new("limited.hpp"),
1171 "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
1172 CppExternalDeclarationLimits { max_records: 1 },
1173 );
1174
1175 assert_eq!(
1176 CppExternalDeclarationCompleteness::Partial,
1177 declarations.completeness
1178 );
1179 assert!(
1180 declarations
1181 .diagnostics
1182 .iter()
1183 .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
1184 );
1185 assert!(
1186 declarations
1187 .diagnostics
1188 .iter()
1189 .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
1190 );
1191 }
1192}