1use crate::adapter::parse_cpp_file_with_callable_observer;
8use crate::declarations::{
9 CppCallableObserver, CppComparableNode, CppComparableSlot, CppParameterType,
10 cpp_active_template_type_parameter, cpp_callable_parameter_names,
11 cpp_callable_parameter_type_identities, cpp_callable_return_type_identity,
12 cpp_comparable_parameter_shapes, 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::cell::RefCell;
24use std::path::{Path, PathBuf};
25use tree_sitter::{Node, Parser};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct CppExternalDeclarationLimits {
29 pub max_records: usize,
30}
31
32impl Default for CppExternalDeclarationLimits {
33 fn default() -> Self {
34 Self {
35 max_records: 250_000,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CppExternalDeclarationCompleteness {
42 Complete,
43 Partial,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum CppExternalMemberKind {
48 Function,
49 Field,
50 Macro,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum CppExternalVisibility {
55 Public,
56 Protected,
57 Private,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum CppExternalTypeKind {
68 Class,
69 Namespace,
70 TypeAlias,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum CppExternalOwner {
80 Type(String),
81 Namespace(String),
82}
83
84impl CppExternalOwner {
85 pub fn name(&self) -> &str {
86 match self {
87 Self::Type(name) | Self::Namespace(name) => name,
88 }
89 }
90
91 pub fn type_name(&self) -> Option<&str> {
92 match self {
93 Self::Type(name) => Some(name),
94 Self::Namespace(_) => None,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct CppExternalType {
101 pub name: String,
102 pub source_name: String,
103 pub kind: CppExternalTypeKind,
104 pub underlying_type: Option<StructuredTypeIdentity>,
105 pub template_metadata: Option<CppTemplateMetadata>,
110 pub visibility: CppExternalVisibility,
111 pub source_path: PathBuf,
112 pub direct_bases: Vec<String>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct CppExternalMember {
117 pub owner: Option<CppExternalOwner>,
118 pub name: String,
119 pub qualified_name: String,
120 pub kind: CppExternalMemberKind,
121 pub visibility: CppExternalVisibility,
122 pub is_constructor: bool,
123 pub signature: Option<String>,
124 pub parameter_types: Option<Vec<CppParameterType>>,
131 pub parameter_shapes: Option<Vec<CppComparableSlot>>,
133 pub callable_arity: Option<CallableArity>,
136 pub parameter_names: Option<Vec<Option<String>>>,
139 pub explicitness: Option<CppCallableExplicitness>,
141 pub return_type: Option<StructuredTypeIdentity>,
142 pub returns_forwarding_reference: bool,
146 pub source_path: PathBuf,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
152struct ObservedCallable {
153 metadata: SignatureMetadata,
154 parameter_types: Option<Vec<CppParameterType>>,
155 parameter_names: Option<Vec<Option<String>>>,
156 parameter_shapes: Option<Vec<CppComparableSlot>>,
157 explicitness: Option<CppCallableExplicitness>,
158 return_type: Option<StructuredTypeIdentity>,
159 returns_forwarding_reference: bool,
160}
161
162#[derive(Default)]
163struct ExternalCallableCollector {
164 by_occurrence: RefCell<HashMap<(CodeUnit, usize), Option<ObservedCallable>>>,
166}
167
168impl CppCallableObserver for ExternalCallableCollector {
169 fn observe(
170 &self,
171 unit: &CodeUnit,
172 declaration_start: usize,
173 metadata: &SignatureMetadata,
174 declarator: Node<'_>,
175 source: &str,
176 ancestry: &ParentIndex<'_>,
177 ) {
178 let parameter_types = cpp_callable_parameter_type_identities(declarator, source, ancestry);
179 let parameter_names = declarator
180 .child_by_field_name("parameters")
181 .and_then(|parameters| cpp_callable_parameter_names(parameters, source));
182 let parameter_shapes = cpp_comparable_parameter_shapes(declarator, source, ancestry);
183 let return_type = metadata
184 .return_type_identity()
185 .cloned()
186 .or_else(|| cpp_callable_return_type_identity(declarator, source, ancestry));
187 let returns_forwarding_reference = cpp_returns_forwarding_reference(
188 declarator,
189 source,
190 ancestry,
191 parameter_types.as_deref(),
192 parameter_shapes.as_deref(),
193 return_type.as_ref(),
194 );
195 let observed = ObservedCallable {
196 metadata: metadata.clone(),
197 parameter_types,
198 parameter_names,
199 parameter_shapes,
200 explicitness: cpp_callable_explicitness(declarator),
201 return_type,
202 returns_forwarding_reference,
203 };
204 let mut by_occurrence = self.by_occurrence.borrow_mut();
205 let key = (unit.clone(), declaration_start);
206 if let Some(existing) = by_occurrence.get_mut(&key) {
207 if existing.as_ref() != Some(&observed) {
208 *existing = None;
209 }
210 } else {
211 by_occurrence.insert(key, Some(observed));
212 }
213 }
214
215 fn observe_duplicate(
216 &self,
217 unit: &CodeUnit,
218 declaration_start: usize,
219 declarator: Node<'_>,
220 source: &str,
221 ancestry: &ParentIndex<'_>,
222 metadata: &dyn Fn() -> SignatureMetadata,
223 ) {
224 self.observe(
225 unit,
226 declaration_start,
227 &metadata(),
228 declarator,
229 source,
230 ancestry,
231 );
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum CppCallableExplicitness {
237 Implicit,
238 Explicit,
239 Conditional,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct CppExternalDeclarationDiagnostic {
244 pub code: &'static str,
245 pub message: String,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct CppExternalDeclarationSet {
250 pub types: Vec<CppExternalType>,
251 pub members: Vec<CppExternalMember>,
252 pub completeness: CppExternalDeclarationCompleteness,
253 pub syntax_complete: bool,
264 pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
265}
266
267pub fn external_include_paths(source: &str) -> Vec<(PathBuf, bool)> {
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 external_include_paths_from_root(source, tree.root_node())
279}
280
281pub fn external_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<(PathBuf, bool)> {
284 let mut paths = Vec::new();
285 let mut stack = vec![root];
286 while let Some(node) = stack.pop() {
287 if node.kind() == "preproc_include"
288 && !has_conditional_preprocessor_ancestor(node)
289 && let Some(path) = node.child_by_field_name("path")
290 && let Some((path, quoted)) = match path.kind() {
291 "string_literal" => node_text(path, source)
292 .strip_prefix('"')
293 .and_then(|path| path.strip_suffix('"'))
294 .filter(|path| !path.is_empty())
295 .map(|path| (PathBuf::from(path), true)),
296 "system_lib_string" => node_text(path, source)
297 .strip_prefix('<')
298 .and_then(|path| path.strip_suffix('>'))
299 .filter(|path| !path.is_empty())
300 .map(|path| (PathBuf::from(path), false)),
301 _ => None,
302 }
303 {
304 paths.push((path, quoted));
305 }
306 let mut cursor = node.walk();
307 stack.extend(node.named_children(&mut cursor));
308 }
309 paths.sort();
310 paths.dedup();
311 paths
312}
313
314pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
320 external_include_paths(source)
321 .into_iter()
322 .filter_map(|(path, quoted)| (!quoted).then_some(path))
323 .collect()
324}
325
326pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
332 external_include_paths_from_root(source, root)
333 .into_iter()
334 .filter_map(|(path, quoted)| (!quoted).then_some(path))
335 .collect()
336}
337
338fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
339 while let Some(parent) = node.parent() {
340 if is_cpp_preprocessor_conditional(parent) {
341 return true;
342 }
343 node = parent;
344 }
345 false
346}
347
348pub fn extract_external_declarations(
354 source_set_root: &Path,
355 source_path: &Path,
356 source: &str,
357 limits: CppExternalDeclarationLimits,
358) -> CppExternalDeclarationSet {
359 let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
360 let mut parser = Parser::new();
361 parser
362 .set_language(&tree_sitter_cpp::LANGUAGE.into())
363 .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
364 let tree = parser.parse(source, None).expect("uncancelled C++ parse");
365
366 let mut diagnostics = Vec::new();
367 let mut completeness = CppExternalDeclarationCompleteness::Complete;
368 let mut parse_errors = Vec::new();
369 collect_parse_errors(tree.root_node(), &mut parse_errors);
370 if !parse_errors.is_empty() {
371 completeness = CppExternalDeclarationCompleteness::Partial;
372 diagnostics.push(CppExternalDeclarationDiagnostic {
373 code: "cpp.external.parse_error",
374 message: format!(
375 "external header `{}` has parse errors",
376 source_path.display()
377 ),
378 });
379 }
380 if has_unsupported_preprocessing(tree.root_node()) {
381 completeness = CppExternalDeclarationCompleteness::Partial;
382 diagnostics.push(CppExternalDeclarationDiagnostic {
383 code: "cpp.external.preprocessor_partial",
384 message: format!(
385 "external header `{}` has conditional or generated declarations",
386 source_path.display()
387 ),
388 });
389 }
390
391 let mut syntax_complete = parse_errors.is_empty();
392
393 let callable_collector = ExternalCallableCollector::default();
394 let parsed = parse_cpp_file_with_callable_observer(&file, source, &tree, &callable_collector);
395 let mut parent_by_child = HashMap::default();
396 for (parent, children) in &parsed.children {
397 for child in children {
398 parent_by_child
402 .entry(child.clone())
403 .and_modify(|owner: &mut CodeUnit| {
404 if parent.is_class() && !owner.is_class() {
405 *owner = parent.clone();
406 }
407 })
408 .or_insert_with(|| parent.clone());
409 }
410 }
411
412 let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
413 declarations.sort_by_key(|declaration| {
414 (
415 declaration.fq_name(),
416 declaration.kind(),
417 declaration.signature().map(str::to_owned),
418 )
419 });
420
421 let mut types = Vec::new();
422 let mut members = Vec::new();
423 for declaration in declarations {
424 if types.len().saturating_add(members.len()) >= limits.max_records {
425 completeness = CppExternalDeclarationCompleteness::Partial;
426 syntax_complete = false;
427 diagnostics.push(CppExternalDeclarationDiagnostic {
428 code: "cpp.external.record_limit",
429 message: format!(
430 "external header `{}` exceeded the declaration record limit",
431 source_path.display()
432 ),
433 });
434 break;
435 }
436 match declaration.kind() {
437 CodeUnitType::Module => types.push(CppExternalType {
438 name: declaration.fq_name(),
439 source_name: cpp_name_for(&declaration),
440 kind: CppExternalTypeKind::Namespace,
441 underlying_type: None,
442 template_metadata: None,
443 visibility: CppExternalVisibility::Public,
444 source_path: source_path.to_path_buf(),
445 direct_bases: Vec::new(),
446 }),
447 CodeUnitType::Class => types.push(CppExternalType {
448 name: declaration.fq_name(),
449 source_name: cpp_name_for(&declaration),
450 kind: if parsed.type_aliases.contains(&declaration) {
451 CppExternalTypeKind::TypeAlias
452 } else {
453 CppExternalTypeKind::Class
454 },
455 underlying_type: parsed
456 .signature_metadata
457 .get(&declaration)
458 .and_then(|records| records.first())
459 .and_then(|metadata| metadata.underlying_type_identity())
460 .cloned(),
461 template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
462 visibility: parsed
463 .ranges
464 .get(&declaration)
465 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
466 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
467 .unwrap_or(CppExternalVisibility::Private),
468 source_path: source_path.to_path_buf(),
469 direct_bases: parsed
470 .raw_supertypes
471 .get(&declaration)
472 .cloned()
473 .unwrap_or_default(),
474 }),
475 CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
476 let declaration_start = parsed
477 .ranges
478 .get(&declaration)
479 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
480 let observed = (declaration.kind() == CodeUnitType::Function)
481 .then(|| {
482 declaration_start.and_then(|start| {
483 callable_collector
484 .by_occurrence
485 .borrow()
486 .get(&(declaration.clone(), start))
487 .cloned()
488 .flatten()
489 })
490 })
491 .flatten();
492 let metadata = if declaration.kind() == CodeUnitType::Function {
495 observed.as_ref().map(|record| &record.metadata)
496 } else {
497 parsed
498 .signature_metadata
499 .get(&declaration)
500 .and_then(|records| records.first())
501 };
502 let parameter_types = observed
503 .as_ref()
504 .and_then(|record| record.parameter_types.clone());
505 let parameter_names = observed
506 .as_ref()
507 .and_then(|record| record.parameter_names.clone());
508 let parameter_shapes = observed
509 .as_ref()
510 .and_then(|record| record.parameter_shapes.clone());
511 if declaration.kind() == CodeUnitType::Function
512 && (parameter_types.is_none()
513 || parameter_names.is_none()
514 || parameter_shapes.is_none())
515 {
516 completeness = CppExternalDeclarationCompleteness::Partial;
517 syntax_complete = false;
518 diagnostics.push(CppExternalDeclarationDiagnostic {
519 code: "cpp.external.formal_roles_unavailable",
520 message: format!(
521 "external header `{}` has unavailable formal roles for `{}`",
522 source_path.display(),
523 declaration.fq_name()
524 ),
525 });
526 }
527 let return_type = metadata
528 .and_then(|metadata| metadata.return_type_identity())
529 .cloned()
530 .or_else(|| {
531 observed
532 .as_ref()
533 .and_then(|record| record.return_type.clone())
534 });
535 let returns_forwarding_reference = observed
536 .as_ref()
537 .is_some_and(|record| record.returns_forwarding_reference);
538 members.push(CppExternalMember {
539 owner: nearest_declaration_owner(&declaration, &parent_by_child),
540 name: declaration.terminal_name().to_owned(),
541 qualified_name: cpp_name_for(&declaration),
542 kind: match declaration.kind() {
543 CodeUnitType::Function => CppExternalMemberKind::Function,
544 CodeUnitType::Field => CppExternalMemberKind::Field,
545 CodeUnitType::Macro => CppExternalMemberKind::Macro,
546 _ => unreachable!("the outer match admits exactly member kinds"),
547 },
548 visibility: declaration_start
549 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
550 .unwrap_or(CppExternalVisibility::Private),
551 is_constructor: metadata
552 .is_some_and(|metadata| metadata.callable_is_constructor()),
553 signature: declaration.signature().map(str::to_owned),
554 parameter_types,
555 parameter_shapes,
556 callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
557 parameter_names,
558 explicitness: observed.as_ref().and_then(|record| record.explicitness),
559 return_type,
560 returns_forwarding_reference,
561 source_path: source_path.to_path_buf(),
562 });
563 }
564 CodeUnitType::FileScope => {}
565 }
566 }
567
568 CppExternalDeclarationSet {
569 types,
570 members,
571 completeness,
572 syntax_complete,
573 diagnostics,
574 }
575}
576
577fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
578 let mut current = root.descendant_for_byte_range(start_byte, start_byte);
579 while let Some(node) = current {
580 let Some(parent) = node.parent() else {
581 break;
582 };
583 if parent.kind() == "field_declaration_list" {
584 let default = match parent.parent().map(|owner| owner.kind()) {
585 Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
586 _ => CppExternalVisibility::Private,
587 };
588 let mut visibility = default;
589 let mut cursor = parent.walk();
590 for child in parent.named_children(&mut cursor) {
591 if child.start_byte() > start_byte {
592 break;
593 }
594 let access = if child.kind() == "access_specifier" {
595 Some(node_text(child, source).trim_end_matches(':').trim())
596 } else {
597 cpp_macro_swallowed_access_label(child, source).map(|label| label.access)
598 };
599 if let Some(access) = access {
600 visibility = match access {
601 "public" => CppExternalVisibility::Public,
602 "protected" => CppExternalVisibility::Protected,
603 "private" => CppExternalVisibility::Private,
604 _ => CppExternalVisibility::Private,
605 };
606 }
607 }
608 return visibility;
609 }
610 current = Some(parent);
611 }
612 CppExternalVisibility::Public
613}
614
615fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
616 while !matches!(
617 declarator.kind(),
618 "declaration" | "field_declaration" | "function_definition"
619 ) {
620 declarator = declarator.parent()?;
621 }
622 if declarator.has_error() {
623 return None;
624 }
625 let mut stack = vec![declarator];
626 let mut explicit = None;
627 while let Some(node) = stack.pop() {
628 if node.kind() == "explicit_function_specifier" {
629 if explicit.is_some() {
630 return None;
631 }
632 explicit = Some(if node.named_child_count() == 0 {
633 CppCallableExplicitness::Explicit
634 } else {
635 CppCallableExplicitness::Conditional
636 });
637 continue;
638 }
639 let mut cursor = node.walk();
640 stack.extend(node.named_children(&mut cursor));
641 }
642 Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
643}
644
645fn cpp_returns_forwarding_reference<'tree>(
646 declarator: Node<'tree>,
647 source: &str,
648 ancestry: &ParentIndex<'tree>,
649 parameters: Option<&[CppParameterType]>,
650 shapes: Option<&[CppComparableSlot]>,
651 returns: Option<&StructuredTypeIdentity>,
652) -> bool {
653 let (
654 Some([CppParameterType::Structured(parameter)]),
655 Some([CppComparableSlot::Shape(shape)]),
656 Some(returns),
657 ) = (parameters, shapes, returns)
658 else {
659 return false;
660 };
661 let Some(StructuredTypeNodeView::RvalueReference(inner)) = parameter.view(parameter.root_id())
662 else {
663 return false;
664 };
665 let Some(StructuredTypeNodeView::Named(name)) = parameter.view(inner) else {
666 return false;
667 };
668 let [parameter_name] = name.path() else {
669 return false;
670 };
671 let CppComparableNode::Reference { inner } = shape.node(shape.root()) else {
672 return false;
673 };
674 if !matches!(
675 shape.node(*inner),
676 CppComparableNode::Named {
677 konst: false,
678 volatil: false,
679 ..
680 }
681 ) || name.is_absolute()
682 || !cpp_active_template_type_parameter(declarator, parameter_name, source, ancestry)
683 || !matches!(
684 returns.view(returns.root_id()),
685 Some(StructuredTypeNodeView::RvalueReference(_))
686 )
687 {
688 return false;
689 }
690
691 let mut current = declarator;
694 while let Some(parent) = ancestry.parent(current) {
695 if matches!(
696 parent.kind(),
697 "function_definition" | "declaration" | "field_declaration"
698 ) {
699 let mut cursor = parent.walk();
700 if parent.named_children(&mut cursor).any(|child| {
701 child.kind() == "type_qualifier"
702 && matches!(node_text(child, source), "const" | "volatile")
703 }) {
704 return false;
705 }
706 return parent
707 .child_by_field_name("type")
708 .is_some_and(|return_node| {
709 cpp_forwarded_return_type(return_node, source, parameter_name)
710 });
711 }
712 current = parent;
713 }
714 false
715}
716
717fn cpp_forwarded_return_type(node: Node<'_>, source: &str, parameter: &str) -> bool {
718 let mut components = Vec::new();
719 let mut removals = 0;
720 let mut stack = vec![node];
721 while let Some(current) = stack.pop() {
722 match current.kind() {
723 "identifier" | "type_identifier" | "namespace_identifier" => {
724 components.push(node_text(current, source));
725 }
726 "dependent_type" => {
727 if current.named_child_count() != 1 {
728 return false;
729 }
730 stack.push(current.named_child(0).expect("one named child"));
731 }
732 "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
733 let Some(name) = current.child_by_field_name("name") else {
734 return false;
735 };
736 stack.push(name);
737 if let Some(scope) = current.child_by_field_name("scope") {
738 stack.push(scope);
739 }
740 }
741 "template_type" => {
742 let (Some(name), Some(arguments)) = (
743 current.child_by_field_name("name"),
744 current.child_by_field_name("arguments"),
745 ) else {
746 return false;
747 };
748 if !matches!(name.kind(), "identifier" | "type_identifier")
749 || node_text(name, source) != "remove_reference"
750 || arguments.named_child_count() != 1
751 {
752 return false;
753 }
754 let mut argument = arguments.named_child(0).expect("one named argument");
755 if argument.kind() == "type_descriptor" && argument.named_child_count() == 1 {
756 argument = argument.named_child(0).expect("one named type");
757 }
758 if !matches!(argument.kind(), "identifier" | "type_identifier")
759 || node_text(argument, source) != parameter
760 {
761 return false;
762 }
763 removals += 1;
764 stack.push(name);
765 }
766 _ => return false,
767 }
768 }
769 (removals == 0 && components == [parameter])
770 || (removals == 1
771 && (components == ["std", "remove_reference", "type"]
772 || components == ["remove_reference", "type"]))
773}
774
775fn nearest_declaration_owner(
776 declaration: &CodeUnit,
777 parent_by_child: &HashMap<CodeUnit, CodeUnit>,
778) -> Option<CppExternalOwner> {
779 let mut current = declaration;
780 while let Some(parent) = parent_by_child.get(current) {
781 match parent.kind() {
782 CodeUnitType::Class => return Some(CppExternalOwner::Type(parent.fq_name())),
783 CodeUnitType::Module => return Some(CppExternalOwner::Namespace(parent.fq_name())),
784 _ => {}
785 }
786 current = parent;
787 }
788 None
789}
790
791fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
792 let mut stack = vec![root];
793 while let Some(node) = stack.pop() {
794 if matches!(
795 node.kind(),
796 "preproc_def"
797 | "preproc_function_def"
798 | "preproc_if"
799 | "preproc_ifdef"
800 | "preproc_ifndef"
801 | "preproc_elif"
802 | "preproc_else"
803 ) {
804 return true;
805 }
806 let mut cursor = node.walk();
807 stack.extend(node.named_children(&mut cursor));
808 }
809 false
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815 use brokk_bifrost_core::analyzer::model::CallableArity;
816 use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
817 use brokk_bifrost_test_macros::hardwon;
818
819 fn extract(source: &str) -> CppExternalDeclarationSet {
820 let temp = tempfile::tempdir().expect("temp root");
821 extract_external_declarations(
822 temp.path(),
823 Path::new("vector"),
824 source,
825 CppExternalDeclarationLimits::default(),
826 )
827 }
828
829 #[test]
830 fn extracts_literal_include_paths_with_delimiters() {
831 let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
832 assert_eq!(
833 vec![
834 (PathBuf::from("local.hpp"), true),
835 (PathBuf::from("vector"), false),
836 ],
837 external_include_paths(source)
838 );
839 assert_eq!(
840 vec![PathBuf::from("vector")],
841 external_angle_include_paths(source)
842 );
843 let mut parser = Parser::new();
844 parser
845 .set_language(&tree_sitter_cpp::LANGUAGE.into())
846 .expect("C++ grammar");
847 let tree = parser.parse(source, None).expect("tree");
848 assert_eq!(
849 vec![PathBuf::from("vector")],
850 external_angle_include_paths_from_root(source, tree.root_node())
851 );
852 }
853
854 #[test]
855 fn extracts_namespaced_template_type_and_owned_members() {
856 let declarations = extract(
857 r#"
858 namespace std {
859 template <typename T> class vector : public sequence<T> {
860 public:
861 vector();
862 void push_back(const T& value);
863 T size;
864 };
865 }
866 "#,
867 );
868
869 assert_eq!(
870 CppExternalDeclarationCompleteness::Complete,
871 declarations.completeness
872 );
873 assert!(
874 declarations.types.iter().any(|record| {
875 record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
876 }),
877 "{declarations:#?}"
878 );
879 assert!(
880 declarations.members.iter().any(|record| {
881 record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
882 && record.name == "push_back"
883 && record.visibility == CppExternalVisibility::Public
884 }),
885 "{declarations:#?}"
886 );
887 assert!(
888 declarations.members.iter().any(|record| {
889 record.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("std.vector")
890 && record.name == "size"
891 }),
892 "{declarations:#?}"
893 );
894 }
895
896 #[test]
897 fn preserves_copy_and_move_reference_parameter_shapes() {
898 let source = r#"
899 class Widget {
900 public:
901 Widget(const Widget&);
902 Widget(Widget&&);
903 };
904 "#;
905 let declarations = extract(source);
906 let constructors = declarations
907 .members
908 .iter()
909 .filter(|member| {
910 member.owner.as_ref().and_then(CppExternalOwner::type_name) == Some("Widget")
911 && member.name == "Widget"
912 })
913 .collect::<Vec<_>>();
914
915 assert_eq!(constructors.len(), 2, "{declarations:#?}");
916 let shapes = constructors
917 .iter()
918 .map(|constructor| {
919 let parameters = constructor
920 .parameter_types
921 .as_ref()
922 .expect("constructor parameters");
923 assert_eq!(parameters.len(), 1, "{constructor:#?}");
924 let CppParameterType::Structured(identity) = ¶meters[0] else {
925 panic!("constructor parameter shape: {constructor:#?}");
926 };
927 (
928 identity.clone(),
929 matches!(
930 identity.view(identity.root_id()),
931 Some(StructuredTypeNodeView::Reference(_))
932 ),
933 matches!(
934 identity.view(identity.root_id()),
935 Some(StructuredTypeNodeView::RvalueReference(_))
936 ),
937 )
938 })
939 .collect::<Vec<_>>();
940
941 assert_ne!(shapes[0].0, shapes[1].0);
942 assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
943 assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
944 assert_eq!(declarations, extract(source));
945 }
946
947 #[test]
948 fn preserves_callable_arities_for_defaulted_and_required_parameters() {
949 let declarations = extract(
950 r#"
951 namespace std {
952 template <class C, class Alloc> class basic_string {
953 public:
954 basic_string(const C*, const Alloc& = Alloc());
955 basic_string(const C&, const Alloc&);
956 };
957 }
958 "#,
959 );
960 let constructors = declarations
961 .members
962 .iter()
963 .filter(|member| {
964 member.owner.as_ref().and_then(CppExternalOwner::type_name)
965 == Some("std.basic_string")
966 && member.name == "basic_string"
967 })
968 .collect::<Vec<_>>();
969
970 assert_eq!(constructors.len(), 2, "{declarations:#?}");
971 assert!(
972 constructors
973 .iter()
974 .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
975 "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
976 );
977 assert!(
978 constructors
979 .iter()
980 .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
981 "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
982 );
983 }
984
985 #[test]
988 #[hardwon(
989 "6ec52f232835: quicksilver round 4 survivor review: lexical namespaces stole out-of-line exported member owners"
990 )]
991 fn recovered_external_api_manifest() {
992 let declarations = extract(
993 r#"
994#define EXPORT_MARKER
995namespace n { class Widget; int damaged = ; }
996class EXPORT_MARKER Widget { public: void first(); };
997class EXPORT(2,0) Attributed final {
998public:
999 ATTRIBUTE("old") int value();
1000 void following();
1001};
1002namespace a::a {
1003struct C { void f(); void f()const; void g()&; void g()&&; };
1004void ::a::a::C::f() {}
1005void ::a::a::C::f()const {}
1006void C::g()& {}
1007void C::g()&& {}
1008}
1009DEPRECATED /* between name and arguments */ (
1010 EXPORT_MARKER int old(int value), "hint"
1011);
1012EXPORT_MARKER int next();
1013void after() {}
1014"#,
1015 );
1016 let mut types = declarations
1017 .types
1018 .iter()
1019 .map(|record| record.name.as_str())
1020 .collect::<Vec<_>>();
1021 types.sort_unstable();
1022 assert_eq!(
1023 types,
1024 ["Attributed", "a", "a::a", "a::a.C", "n", "n.Widget"]
1025 );
1026 let mut callables = declarations
1027 .members
1028 .iter()
1029 .filter(|record| record.kind == CppExternalMemberKind::Function)
1030 .map(|record| {
1031 (
1032 record.owner.as_ref().map(CppExternalOwner::name),
1033 record.name.as_str(),
1034 record.signature.as_deref(),
1035 )
1036 })
1037 .collect::<Vec<_>>();
1038 callables.sort_unstable();
1039 let mut expected = vec![
1040 (None, "after", Some("()")),
1041 (None, "next", Some("()")),
1042 (None, "old", Some("(int)")),
1043 (Some("Attributed"), "following", Some("()")),
1044 (Some("Attributed"), "value", Some("()")),
1045 (Some("a::a.C"), "f", Some("()")),
1046 (Some("a::a.C"), "f", Some("() const")),
1047 (Some("a::a.C"), "g", Some("() &")),
1048 (Some("a::a.C"), "g", Some("() &&")),
1049 (Some("n.Widget"), "first", Some("()")),
1050 ];
1051 expected.sort_unstable();
1052 assert_eq!(callables, expected);
1053 }
1054
1055 #[test]
1059 fn preserves_callable_parameter_names() {
1060 let declarations = extract(
1061 "struct Owner {};\n\
1062 void connect(int fd, const char *address, unsigned length);\n\
1063 void callbacks(void (*callback)(int argument), int array[extent], int (Owner::*member)(int nested));\n",
1064 );
1065 for (name, expected) in [
1066 ("connect", vec![Some("fd"), Some("address"), Some("length")]),
1067 (
1068 "callbacks",
1069 vec![Some("callback"), Some("array"), Some("member")],
1070 ),
1071 ] {
1072 let candidates = declarations
1073 .members
1074 .iter()
1075 .filter(|member| member.name == name)
1076 .collect::<Vec<_>>();
1077 let [member] = candidates.as_slice() else {
1078 panic!("{name} declaration: {declarations:#?}");
1079 };
1080 assert_eq!(
1081 member.parameter_names,
1082 Some(
1083 expected
1084 .into_iter()
1085 .map(|name| name.map(str::to_owned))
1086 .collect()
1087 ),
1088 "{name}: outer parameter slots must agree with the API manifest"
1089 );
1090 }
1091 }
1092
1093 #[test]
1094 #[brokk_bifrost_test_macros::hardwon(
1095 "5e79db920891: quicksilver round 4 survivor tests: unnamed formals inherit nested names"
1096 )]
1097 fn unnamed_callable_parameter_names() {
1098 let declarations = extract("void unnamed(void (*)(int argument), int [extent]);\n");
1099 let [member] = declarations.members.as_slice() else {
1100 panic!("one exported callable: {declarations:#?}");
1101 };
1102 assert_eq!(member.parameter_names, Some(vec![None, None]));
1103 }
1104
1105 #[test]
1106 fn preserves_callable_explicitness_for_implicit_binding() {
1107 for (declaration, expected) in [
1108 ("Widget(const char*);", CppCallableExplicitness::Implicit),
1109 (
1110 "explicit Widget(const char*);",
1111 CppCallableExplicitness::Explicit,
1112 ),
1113 (
1114 "explicit(true) Widget(const char*);",
1115 CppCallableExplicitness::Conditional,
1116 ),
1117 ] {
1118 let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
1119 let member = declarations
1120 .members
1121 .first()
1122 .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
1123 assert_eq!(Some(expected), member.explicitness, "{member:#?}");
1124 }
1125 }
1126
1127 #[test]
1128 fn preserves_callable_return_reference_shapes() {
1129 let declarations = extract(
1130 r#"
1131 namespace std {
1132 class basic_string {
1133 public:
1134 basic_string& operator=(const basic_string&);
1135 basic_string&& operator=(basic_string&&);
1136 void operator=(int);
1137 };
1138 }
1139 "#,
1140 );
1141 let assignments = declarations
1142 .members
1143 .iter()
1144 .filter(|member| member.name == "operator=")
1145 .collect::<Vec<_>>();
1146 assert_eq!(assignments.len(), 3, "{declarations:#?}");
1147
1148 let copy = assignments
1149 .iter()
1150 .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
1151 .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
1152 let Some(StructuredTypeNodeView::Reference(inner)) = copy
1153 .return_type
1154 .as_ref()
1155 .and_then(|identity| identity.view(identity.root_id()))
1156 else {
1157 panic!("copy return type: {copy:#?}");
1158 };
1159 let Some(StructuredTypeNodeView::Named(name)) = copy
1160 .return_type
1161 .as_ref()
1162 .and_then(|identity| identity.view(inner))
1163 else {
1164 panic!("copy return target: {copy:#?}");
1165 };
1166 assert_eq!(["basic_string"], name.path());
1167 assert_eq!(["std", "basic_string"], name.lexical_scope());
1168
1169 let move_assignment = declarations
1170 .members
1171 .iter()
1172 .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
1173 .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
1174 assert!(matches!(
1175 move_assignment
1176 .return_type
1177 .as_ref()
1178 .and_then(|identity| identity.view(identity.root_id())),
1179 Some(StructuredTypeNodeView::RvalueReference(_))
1180 ));
1181
1182 let void_assignment = declarations
1183 .members
1184 .iter()
1185 .find(|member| member.signature.as_deref() == Some("(int)"))
1186 .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
1187 let Some(StructuredTypeNodeView::Named(name)) = void_assignment
1188 .return_type
1189 .as_ref()
1190 .and_then(|identity| identity.view(identity.root_id()))
1191 else {
1192 panic!("void return type: {void_assignment:#?}");
1193 };
1194 assert_eq!(["void"], name.path());
1195 }
1196
1197 #[test]
1198 fn preserves_type_alias_underlying_structured_identity() {
1199 let declarations = extract(
1200 r#"
1201 namespace std {
1202 template<class C, class Traits, class Alloc> class basic_string;
1203 using string = basic_string<char, char_traits<char>, allocator<char>>;
1204 }
1205 "#,
1206 );
1207 let alias = declarations
1208 .types
1209 .iter()
1210 .find(|record| record.name == "std.string")
1211 .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
1212 assert_eq!(CppExternalTypeKind::TypeAlias, alias.kind, "{alias:#?}");
1213 let identity = alias
1214 .underlying_type
1215 .as_ref()
1216 .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
1217 let Some(StructuredTypeNodeView::Generic { base, arguments }) =
1218 identity.view(identity.root_id())
1219 else {
1220 panic!("generic basic_string alias target: {identity:#?}");
1221 };
1222 assert_eq!(3, arguments.len());
1223 let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
1224 panic!("named basic_string alias base: {identity:#?}");
1225 };
1226 assert_eq!(["basic_string"], name.path());
1227 assert_eq!(["std"], name.lexical_scope());
1228 }
1229
1230 #[test]
1231 fn keeps_same_short_names_under_distinct_owners() {
1232 let declarations = extract(
1233 "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
1234 );
1235 let mut owners = declarations
1236 .members
1237 .iter()
1238 .filter(|member| member.name == "add")
1239 .filter_map(|member| member.owner.as_ref().map(|owner| owner.name().to_owned()))
1240 .collect::<Vec<_>>();
1241 owners.sort();
1242
1243 assert_eq!(vec!["first.box", "second.box"], owners);
1244 assert!(
1245 declarations
1246 .members
1247 .iter()
1248 .filter(|member| member.name == "add")
1249 .all(|member| member.visibility == CppExternalVisibility::Private)
1250 );
1251 }
1252
1253 #[test]
1254 fn nested_type_visibility_follows_the_enclosing_access_section() {
1255 let declarations = extract(
1256 "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
1257 );
1258 assert!(declarations.types.iter().any(|record| {
1259 record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
1260 }));
1261 assert!(declarations.types.iter().any(|record| {
1262 record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
1263 }));
1264 assert!(declarations.types.iter().any(|record| {
1265 record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
1266 }));
1267 }
1268
1269 #[test]
1270 fn preprocessor_and_record_limits_make_the_surface_partial() {
1271 let temp = tempfile::tempdir().expect("temp root");
1272 let declarations = extract_external_declarations(
1273 temp.path(),
1274 Path::new("limited.hpp"),
1275 "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
1276 CppExternalDeclarationLimits { max_records: 1 },
1277 );
1278
1279 assert_eq!(
1280 CppExternalDeclarationCompleteness::Partial,
1281 declarations.completeness
1282 );
1283 assert!(
1284 declarations
1285 .diagnostics
1286 .iter()
1287 .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
1288 );
1289 assert!(
1290 declarations
1291 .diagnostics
1292 .iter()
1293 .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
1294 );
1295 }
1296}