1use crate::adapter::parse_cpp_file;
8use crate::declarations::{
9 CppComparableSlot, CppParameterType, cpp_callable_parameter_type_identities,
10 cpp_callable_return_type_identity, cpp_comparable_parameter_shapes, cpp_function_declarator_at,
11 node_text,
12};
13use crate::graph::resolver::cpp_name_for;
14use brokk_bifrost_core::analyzer::ProjectFile;
15use brokk_bifrost_core::analyzer::model::{
16 CallableArity, CodeUnit, CodeUnitType, CppTemplateMetadata, SignatureMetadata,
17 StructuredTypeIdentity,
18};
19use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, collect_parse_errors};
20use brokk_bifrost_core::hash::HashMap;
21use std::path::{Path, PathBuf};
22use tree_sitter::{Node, Parser};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct CppExternalDeclarationLimits {
26 pub max_records: usize,
27}
28
29impl Default for CppExternalDeclarationLimits {
30 fn default() -> Self {
31 Self {
32 max_records: 250_000,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum CppExternalDeclarationCompleteness {
39 Complete,
40 Partial,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CppExternalMemberKind {
45 Function,
46 Field,
47 Macro,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CppExternalVisibility {
52 Public,
53 Protected,
54 Private,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct CppExternalType {
59 pub name: String,
60 pub source_name: String,
61 pub is_type_alias: bool,
62 pub underlying_type: Option<StructuredTypeIdentity>,
63 pub template_metadata: Option<CppTemplateMetadata>,
68 pub visibility: CppExternalVisibility,
69 pub source_path: PathBuf,
70 pub direct_bases: Vec<String>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CppExternalMember {
75 pub owner: Option<String>,
76 pub name: String,
77 pub qualified_name: String,
78 pub kind: CppExternalMemberKind,
79 pub visibility: CppExternalVisibility,
80 pub is_constructor: bool,
81 pub signature: Option<String>,
82 pub parameter_types: Option<Vec<CppParameterType>>,
89 pub parameter_shapes: Option<Vec<CppComparableSlot>>,
91 pub callable_arity: Option<CallableArity>,
94 pub explicitness: Option<CppCallableExplicitness>,
96 pub return_type: Option<StructuredTypeIdentity>,
97 pub source_path: PathBuf,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CppCallableExplicitness {
102 Implicit,
103 Explicit,
104 Conditional,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct CppExternalDeclarationDiagnostic {
109 pub code: &'static str,
110 pub message: String,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct CppExternalDeclarationSet {
115 pub types: Vec<CppExternalType>,
116 pub members: Vec<CppExternalMember>,
117 pub completeness: CppExternalDeclarationCompleteness,
118 pub diagnostics: Vec<CppExternalDeclarationDiagnostic>,
119}
120
121pub fn external_angle_include_paths(source: &str) -> Vec<PathBuf> {
127 let mut parser = Parser::new();
128 parser
129 .set_language(&tree_sitter_cpp::LANGUAGE.into())
130 .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
131 let tree = parser.parse(source, None).expect("uncancelled C++ parse");
132 external_angle_include_paths_from_root(source, tree.root_node())
133}
134
135pub fn external_angle_include_paths_from_root(source: &str, root: Node<'_>) -> Vec<PathBuf> {
141 let mut paths = Vec::new();
142 let mut stack = vec![root];
143 while let Some(node) = stack.pop() {
144 if node.kind() == "preproc_include"
145 && !has_conditional_preprocessor_ancestor(node)
146 && let Some(path) = node.child_by_field_name("path")
147 && path.kind() == "system_lib_string"
148 && let Some(path) = node_text(path, source)
149 .strip_prefix('<')
150 .and_then(|path| path.strip_suffix('>'))
151 .filter(|path| !path.is_empty())
152 {
153 paths.push(PathBuf::from(path));
154 }
155 let mut cursor = node.walk();
156 stack.extend(node.named_children(&mut cursor));
157 }
158 paths.sort();
159 paths.dedup();
160 paths
161}
162
163fn has_conditional_preprocessor_ancestor(mut node: Node<'_>) -> bool {
164 while let Some(parent) = node.parent() {
165 if matches!(
166 parent.kind(),
167 "preproc_if" | "preproc_ifdef" | "preproc_ifndef" | "preproc_elif" | "preproc_else"
168 ) {
169 return true;
170 }
171 node = parent;
172 }
173 false
174}
175
176pub fn extract_external_declarations(
182 source_set_root: &Path,
183 source_path: &Path,
184 source: &str,
185 limits: CppExternalDeclarationLimits,
186) -> CppExternalDeclarationSet {
187 let file = ProjectFile::new(source_set_root.to_path_buf(), source_path.to_path_buf());
188 let mut parser = Parser::new();
189 parser
190 .set_language(&tree_sitter_cpp::LANGUAGE.into())
191 .expect("the linked tree-sitter-cpp grammar matches this tree-sitter version");
192 let tree = parser.parse(source, None).expect("uncancelled C++ parse");
193
194 let mut diagnostics = Vec::new();
195 let mut completeness = CppExternalDeclarationCompleteness::Complete;
196 let mut parse_errors = Vec::new();
197 collect_parse_errors(tree.root_node(), &mut parse_errors);
198 if !parse_errors.is_empty() {
199 completeness = CppExternalDeclarationCompleteness::Partial;
200 diagnostics.push(CppExternalDeclarationDiagnostic {
201 code: "cpp.external.parse_error",
202 message: format!(
203 "external header `{}` has parse errors",
204 source_path.display()
205 ),
206 });
207 }
208 if has_unsupported_preprocessing(tree.root_node()) {
209 completeness = CppExternalDeclarationCompleteness::Partial;
210 diagnostics.push(CppExternalDeclarationDiagnostic {
211 code: "cpp.external.preprocessor_partial",
212 message: format!(
213 "external header `{}` has conditional or generated declarations",
214 source_path.display()
215 ),
216 });
217 }
218
219 let parsed = parse_cpp_file(&file, source, &tree);
220 let mut parent_by_child = HashMap::default();
221 for (parent, children) in &parsed.children {
222 for child in children {
223 parent_by_child.insert(child.clone(), parent.clone());
224 }
225 }
226
227 let mut declarations = parsed.declarations().iter().cloned().collect::<Vec<_>>();
228 declarations.sort_by_key(|declaration| {
229 (
230 declaration.fq_name(),
231 declaration.kind(),
232 declaration.signature().map(str::to_owned),
233 )
234 });
235
236 let ancestry = ParentIndex::new(tree.root_node());
239 let mut types = Vec::new();
240 let mut members = Vec::new();
241 for declaration in declarations {
242 if types.len().saturating_add(members.len()) >= limits.max_records {
243 completeness = CppExternalDeclarationCompleteness::Partial;
244 diagnostics.push(CppExternalDeclarationDiagnostic {
245 code: "cpp.external.record_limit",
246 message: format!(
247 "external header `{}` exceeded the declaration record limit",
248 source_path.display()
249 ),
250 });
251 break;
252 }
253 match declaration.kind() {
254 CodeUnitType::Class => types.push(CppExternalType {
255 name: declaration.fq_name(),
256 source_name: cpp_name_for(&declaration),
257 is_type_alias: parsed.type_aliases.contains(&declaration),
258 underlying_type: parsed
259 .signature_metadata
260 .get(&declaration)
261 .and_then(|records| records.first())
262 .and_then(|metadata| metadata.underlying_type_identity())
263 .cloned(),
264 template_metadata: parsed.cpp_template_metadata.get(&declaration).cloned(),
265 visibility: parsed
266 .ranges
267 .get(&declaration)
268 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min())
269 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
270 .unwrap_or(CppExternalVisibility::Private),
271 source_path: source_path.to_path_buf(),
272 direct_bases: parsed
273 .raw_supertypes
274 .get(&declaration)
275 .cloned()
276 .unwrap_or_default(),
277 }),
278 CodeUnitType::Function | CodeUnitType::Field | CodeUnitType::Macro => {
279 let metadata = parsed
280 .signature_metadata
281 .get(&declaration)
282 .and_then(|records| records.first());
283 let declaration_start = parsed
284 .ranges
285 .get(&declaration)
286 .and_then(|ranges| ranges.iter().map(|range| range.start_byte).min());
287 let function_declarator = (declaration.kind() == CodeUnitType::Function)
288 .then(|| {
289 declaration_start
290 .and_then(|start| cpp_function_declarator_at(tree.root_node(), start))
291 })
292 .flatten();
293 let parameter_types = function_declarator.map(|declarator| {
294 cpp_callable_parameter_type_identities(declarator, source, &ancestry)
295 });
296 let parameter_shapes = function_declarator.map(|declarator| {
297 cpp_comparable_parameter_shapes(declarator, source, &ancestry)
298 });
299 let return_type = metadata
300 .and_then(|metadata| metadata.return_type_identity())
301 .cloned()
302 .or_else(|| {
303 function_declarator.and_then(|declarator| {
304 cpp_callable_return_type_identity(declarator, source, &ancestry)
305 })
306 });
307 members.push(CppExternalMember {
308 owner: nearest_type_owner(&declaration, &parent_by_child),
309 name: declaration.terminal_name().to_owned(),
310 qualified_name: cpp_name_for(&declaration),
311 kind: match declaration.kind() {
312 CodeUnitType::Function => CppExternalMemberKind::Function,
313 CodeUnitType::Field => CppExternalMemberKind::Field,
314 CodeUnitType::Macro => CppExternalMemberKind::Macro,
315 _ => unreachable!("the outer match admits exactly member kinds"),
316 },
317 visibility: declaration_start
318 .map(|start| cpp_member_visibility(tree.root_node(), source, start))
319 .unwrap_or(CppExternalVisibility::Private),
320 is_constructor: metadata
321 .is_some_and(|metadata| metadata.callable_is_constructor()),
322 signature: declaration.signature().map(str::to_owned),
323 parameter_types,
324 parameter_shapes,
325 callable_arity: metadata.and_then(SignatureMetadata::callable_arity),
326 explicitness: function_declarator.and_then(cpp_callable_explicitness),
327 return_type,
328 source_path: source_path.to_path_buf(),
329 });
330 }
331 CodeUnitType::Module | CodeUnitType::FileScope => {}
332 }
333 }
334
335 CppExternalDeclarationSet {
336 types,
337 members,
338 completeness,
339 diagnostics,
340 }
341}
342
343fn cpp_member_visibility(root: Node<'_>, source: &str, start_byte: usize) -> CppExternalVisibility {
344 let mut current = root.descendant_for_byte_range(start_byte, start_byte);
345 while let Some(node) = current {
346 let Some(parent) = node.parent() else {
347 break;
348 };
349 if parent.kind() == "field_declaration_list" {
350 let default = match parent.parent().map(|owner| owner.kind()) {
351 Some("struct_specifier" | "union_specifier") => CppExternalVisibility::Public,
352 _ => CppExternalVisibility::Private,
353 };
354 let mut visibility = default;
355 let mut cursor = parent.walk();
356 for child in parent.named_children(&mut cursor) {
357 if child.start_byte() > start_byte {
358 break;
359 }
360 if child.kind() == "access_specifier" {
361 visibility = match node_text(child, source).trim_end_matches(':').trim() {
362 "public" => CppExternalVisibility::Public,
363 "protected" => CppExternalVisibility::Protected,
364 "private" => CppExternalVisibility::Private,
365 _ => CppExternalVisibility::Private,
366 };
367 }
368 }
369 return visibility;
370 }
371 current = Some(parent);
372 }
373 CppExternalVisibility::Public
374}
375
376fn cpp_callable_explicitness(mut declarator: Node<'_>) -> Option<CppCallableExplicitness> {
377 while !matches!(
378 declarator.kind(),
379 "declaration" | "field_declaration" | "function_definition"
380 ) {
381 declarator = declarator.parent()?;
382 }
383 if declarator.has_error() {
384 return None;
385 }
386 let mut stack = vec![declarator];
387 let mut explicit = None;
388 while let Some(node) = stack.pop() {
389 if node.kind() == "explicit_function_specifier" {
390 if explicit.is_some() {
391 return None;
392 }
393 explicit = Some(if node.named_child_count() == 0 {
394 CppCallableExplicitness::Explicit
395 } else {
396 CppCallableExplicitness::Conditional
397 });
398 continue;
399 }
400 let mut cursor = node.walk();
401 stack.extend(node.named_children(&mut cursor));
402 }
403 Some(explicit.unwrap_or(CppCallableExplicitness::Implicit))
404}
405
406fn nearest_type_owner(
407 declaration: &CodeUnit,
408 parent_by_child: &HashMap<CodeUnit, CodeUnit>,
409) -> Option<String> {
410 let mut current = declaration;
411 while let Some(parent) = parent_by_child.get(current) {
412 if parent.kind() == CodeUnitType::Class {
413 return Some(parent.fq_name());
414 }
415 current = parent;
416 }
417 None
418}
419
420fn has_unsupported_preprocessing(root: Node<'_>) -> bool {
421 let mut stack = vec![root];
422 while let Some(node) = stack.pop() {
423 if matches!(
424 node.kind(),
425 "preproc_def"
426 | "preproc_function_def"
427 | "preproc_if"
428 | "preproc_ifdef"
429 | "preproc_ifndef"
430 | "preproc_elif"
431 | "preproc_else"
432 ) {
433 return true;
434 }
435 let mut cursor = node.walk();
436 stack.extend(node.named_children(&mut cursor));
437 }
438 false
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use brokk_bifrost_core::analyzer::model::CallableArity;
445 use brokk_bifrost_core::analyzer::model::StructuredTypeNodeView;
446
447 fn extract(source: &str) -> CppExternalDeclarationSet {
448 let temp = tempfile::tempdir().expect("temp root");
449 extract_external_declarations(
450 temp.path(),
451 Path::new("vector"),
452 source,
453 CppExternalDeclarationLimits::default(),
454 )
455 }
456
457 #[test]
458 fn extracts_only_literal_angle_include_paths() {
459 let source = "#include <vector>\n#include \"local.hpp\"\n#include HEADER\n#if FEATURE\n#include <conditional.hpp>\n#endif\n";
460 assert_eq!(
461 vec![PathBuf::from("vector")],
462 external_angle_include_paths(source)
463 );
464 let mut parser = Parser::new();
465 parser
466 .set_language(&tree_sitter_cpp::LANGUAGE.into())
467 .expect("C++ grammar");
468 let tree = parser.parse(source, None).expect("tree");
469 assert_eq!(
470 vec![PathBuf::from("vector")],
471 external_angle_include_paths_from_root(source, tree.root_node())
472 );
473 }
474
475 #[test]
476 fn extracts_namespaced_template_type_and_owned_members() {
477 let declarations = extract(
478 r#"
479 namespace std {
480 template <typename T> class vector : public sequence<T> {
481 public:
482 vector();
483 void push_back(const T& value);
484 T size;
485 };
486 }
487 "#,
488 );
489
490 assert_eq!(
491 CppExternalDeclarationCompleteness::Complete,
492 declarations.completeness
493 );
494 assert!(
495 declarations.types.iter().any(|record| {
496 record.name == "std.vector" && record.direct_bases == ["sequence<T>"]
497 }),
498 "{declarations:#?}"
499 );
500 assert!(
501 declarations.members.iter().any(|record| {
502 record.owner.as_deref() == Some("std.vector")
503 && record.name == "push_back"
504 && record.visibility == CppExternalVisibility::Public
505 }),
506 "{declarations:#?}"
507 );
508 assert!(
509 declarations.members.iter().any(|record| {
510 record.owner.as_deref() == Some("std.vector") && record.name == "size"
511 }),
512 "{declarations:#?}"
513 );
514 }
515
516 #[test]
517 fn preserves_copy_and_move_reference_parameter_shapes() {
518 let source = r#"
519 class Widget {
520 public:
521 Widget(const Widget&);
522 Widget(Widget&&);
523 };
524 "#;
525 let declarations = extract(source);
526 let constructors = declarations
527 .members
528 .iter()
529 .filter(|member| member.owner.as_deref() == Some("Widget") && member.name == "Widget")
530 .collect::<Vec<_>>();
531
532 assert_eq!(constructors.len(), 2, "{declarations:#?}");
533 let shapes = constructors
534 .iter()
535 .map(|constructor| {
536 let parameters = constructor
537 .parameter_types
538 .as_ref()
539 .expect("constructor parameters");
540 assert_eq!(parameters.len(), 1, "{constructor:#?}");
541 let CppParameterType::Structured(identity) = ¶meters[0] else {
542 panic!("constructor parameter shape: {constructor:#?}");
543 };
544 (
545 identity.clone(),
546 matches!(
547 identity.view(identity.root_id()),
548 Some(StructuredTypeNodeView::Reference(_))
549 ),
550 matches!(
551 identity.view(identity.root_id()),
552 Some(StructuredTypeNodeView::RvalueReference(_))
553 ),
554 )
555 })
556 .collect::<Vec<_>>();
557
558 assert_ne!(shapes[0].0, shapes[1].0);
559 assert!(shapes.iter().any(|(_, is_lvalue, _)| *is_lvalue));
560 assert!(shapes.iter().any(|(_, _, is_rvalue)| *is_rvalue));
561 assert_eq!(declarations, extract(source));
562 }
563
564 #[test]
565 fn preserves_callable_arities_for_defaulted_and_required_parameters() {
566 let declarations = extract(
567 r#"
568 namespace std {
569 template <class C, class Alloc> class basic_string {
570 public:
571 basic_string(const C*, const Alloc& = Alloc());
572 basic_string(const C&, const Alloc&);
573 };
574 }
575 "#,
576 );
577 let constructors = declarations
578 .members
579 .iter()
580 .filter(|member| {
581 member.owner.as_deref() == Some("std.basic_string") && member.name == "basic_string"
582 })
583 .collect::<Vec<_>>();
584
585 assert_eq!(constructors.len(), 2, "{declarations:#?}");
586 assert!(
587 constructors
588 .iter()
589 .any(|member| { member.callable_arity == Some(CallableArity::new(1, 2, false)) }),
590 "defaulted constructor arity must be required=1,total=2: {constructors:#?}"
591 );
592 assert!(
593 constructors
594 .iter()
595 .any(|member| { member.callable_arity == Some(CallableArity::new(2, 2, false)) }),
596 "non-defaulted constructor arity must be required=2,total=2: {constructors:#?}"
597 );
598 }
599
600 #[test]
601 fn preserves_callable_explicitness_for_implicit_binding() {
602 for (declaration, expected) in [
603 ("Widget(const char*);", CppCallableExplicitness::Implicit),
604 (
605 "explicit Widget(const char*);",
606 CppCallableExplicitness::Explicit,
607 ),
608 (
609 "explicit(true) Widget(const char*);",
610 CppCallableExplicitness::Conditional,
611 ),
612 ] {
613 let declarations = extract(&format!("class Widget {{ public: {declaration} }};"));
614 let member = declarations
615 .members
616 .first()
617 .unwrap_or_else(|| panic!("constructor declaration: {declarations:#?}"));
618 assert_eq!(Some(expected), member.explicitness, "{member:#?}");
619 }
620 }
621
622 #[test]
623 fn preserves_callable_return_reference_shapes() {
624 let declarations = extract(
625 r#"
626 namespace std {
627 class basic_string {
628 public:
629 basic_string& operator=(const basic_string&);
630 basic_string&& operator=(basic_string&&);
631 void operator=(int);
632 };
633 }
634 "#,
635 );
636 let assignments = declarations
637 .members
638 .iter()
639 .filter(|member| member.name == "operator=")
640 .collect::<Vec<_>>();
641 assert_eq!(assignments.len(), 3, "{declarations:#?}");
642
643 let copy = assignments
644 .iter()
645 .find(|member| member.signature.as_deref() == Some("(const basic_string &)"))
646 .unwrap_or_else(|| panic!("copy assignment: {declarations:#?}"));
647 let Some(StructuredTypeNodeView::Reference(inner)) = copy
648 .return_type
649 .as_ref()
650 .and_then(|identity| identity.view(identity.root_id()))
651 else {
652 panic!("copy return type: {copy:#?}");
653 };
654 let Some(StructuredTypeNodeView::Named(name)) = copy
655 .return_type
656 .as_ref()
657 .and_then(|identity| identity.view(inner))
658 else {
659 panic!("copy return target: {copy:#?}");
660 };
661 assert_eq!(["basic_string"], name.path());
662 assert_eq!(["std", "basic_string"], name.lexical_scope());
663
664 let move_assignment = declarations
665 .members
666 .iter()
667 .find(|member| member.signature.as_deref() == Some("(basic_string &&)"))
668 .unwrap_or_else(|| panic!("move assignment: {declarations:#?}"));
669 assert!(matches!(
670 move_assignment
671 .return_type
672 .as_ref()
673 .and_then(|identity| identity.view(identity.root_id())),
674 Some(StructuredTypeNodeView::RvalueReference(_))
675 ));
676
677 let void_assignment = declarations
678 .members
679 .iter()
680 .find(|member| member.signature.as_deref() == Some("(int)"))
681 .unwrap_or_else(|| panic!("void assignment: {declarations:#?}"));
682 let Some(StructuredTypeNodeView::Named(name)) = void_assignment
683 .return_type
684 .as_ref()
685 .and_then(|identity| identity.view(identity.root_id()))
686 else {
687 panic!("void return type: {void_assignment:#?}");
688 };
689 assert_eq!(["void"], name.path());
690 }
691
692 #[test]
693 fn preserves_type_alias_underlying_structured_identity() {
694 let declarations = extract(
695 r#"
696 namespace std {
697 template<class C, class Traits, class Alloc> class basic_string;
698 using string = basic_string<char, char_traits<char>, allocator<char>>;
699 }
700 "#,
701 );
702 let alias = declarations
703 .types
704 .iter()
705 .find(|record| record.name == "std.string")
706 .unwrap_or_else(|| panic!("string alias: {declarations:#?}"));
707 assert!(alias.is_type_alias, "{alias:#?}");
708 let identity = alias
709 .underlying_type
710 .as_ref()
711 .unwrap_or_else(|| panic!("structured alias target: {alias:#?}"));
712 let Some(StructuredTypeNodeView::Generic { base, arguments }) =
713 identity.view(identity.root_id())
714 else {
715 panic!("generic basic_string alias target: {identity:#?}");
716 };
717 assert_eq!(3, arguments.len());
718 let Some(StructuredTypeNodeView::Named(name)) = identity.view(base) else {
719 panic!("named basic_string alias base: {identity:#?}");
720 };
721 assert_eq!(["basic_string"], name.path());
722 assert_eq!(["std"], name.lexical_scope());
723 }
724
725 #[test]
726 fn keeps_same_short_names_under_distinct_owners() {
727 let declarations = extract(
728 "namespace first { class box { void add(int); }; }\nnamespace second { class box { void add(int); }; }",
729 );
730 let mut owners = declarations
731 .members
732 .iter()
733 .filter(|member| member.name == "add")
734 .filter_map(|member| member.owner.clone())
735 .collect::<Vec<_>>();
736 owners.sort();
737
738 assert_eq!(vec!["first.box", "second.box"], owners);
739 assert!(
740 declarations
741 .members
742 .iter()
743 .filter(|member| member.name == "add")
744 .all(|member| member.visibility == CppExternalVisibility::Private)
745 );
746 }
747
748 #[test]
749 fn nested_type_visibility_follows_the_enclosing_access_section() {
750 let declarations = extract(
751 "class Outer { class Hidden {}; public: struct Visible {}; protected: class Guarded {}; };",
752 );
753 assert!(declarations.types.iter().any(|record| {
754 record.name == "Outer$Hidden" && record.visibility == CppExternalVisibility::Private
755 }));
756 assert!(declarations.types.iter().any(|record| {
757 record.name == "Outer$Visible" && record.visibility == CppExternalVisibility::Public
758 }));
759 assert!(declarations.types.iter().any(|record| {
760 record.name == "Outer$Guarded" && record.visibility == CppExternalVisibility::Protected
761 }));
762 }
763
764 #[test]
765 fn preprocessor_and_record_limits_make_the_surface_partial() {
766 let temp = tempfile::tempdir().expect("temp root");
767 let declarations = extract_external_declarations(
768 temp.path(),
769 Path::new("limited.hpp"),
770 "#ifdef FEATURE\nclass Conditional {};\n#endif\nclass Always {};",
771 CppExternalDeclarationLimits { max_records: 1 },
772 );
773
774 assert_eq!(
775 CppExternalDeclarationCompleteness::Partial,
776 declarations.completeness
777 );
778 assert!(
779 declarations
780 .diagnostics
781 .iter()
782 .any(|diagnostic| diagnostic.code == "cpp.external.preprocessor_partial")
783 );
784 assert!(
785 declarations
786 .diagnostics
787 .iter()
788 .any(|diagnostic| diagnostic.code == "cpp.external.record_limit")
789 );
790 }
791}