Skip to main content

brokk_bifrost_cpp/
adapter.rs

1//! The C++ answers behind `CppAdapter`.
2//!
3//! `LanguageAdapter` is analysis-owned, so the trait impl itself stays in
4//! `analyzer/cpp/adapter.rs`; every answer it gives comes from here or from
5//! [`crate::test_detection`] and [`crate::queries`].
6
7use crate::declarations::{
8    CppVisitor, collect_cpp_identifiers, collect_cpp_includes, recover_quoted_includes,
9};
10use crate::graph::resolver::OrphanedNamespaceScopeIndex;
11use crate::graph::syntax::ObjectMacroReplacement;
12use brokk_bifrost_core::analyzer::ProjectFile;
13use brokk_bifrost_core::analyzer::cognitive_complexity;
14use brokk_bifrost_core::analyzer::model::{Language, LanguageDialect};
15use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
16use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
17use brokk_bifrost_core::hash::{HashMap, HashSet};
18use std::sync::LazyLock;
19use tree_sitter::{Node, Tree};
20
21/// A primary reading plus evidence for whether a C tag reading can differ.
22/// The witness covers the complete visitor, including recovery.
23#[derive(Debug)]
24pub struct CppPrimaryReading {
25    pub parsed: ParsedFile,
26    pub c_tag_scope_witness: bool,
27}
28
29/// The file extension `CppAdapter` reports. `Language::Cpp` also covers `.c`,
30/// `.cc`, `.cxx` and the header spellings; this is only the canonical one.
31pub const CPP_FILE_EXTENSION: &str = "cpp";
32
33/// Tree-sitter node-kind mapping used by the cognitive-complexity scorer for
34/// C++. Node names are from the tree-sitter-cpp grammar.
35pub static CPP_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
36    LazyLock::new(|| cognitive_complexity::Config {
37        if_types: &["if_statement"],
38        loop_types: &["for_statement", "while_statement", "do_statement"],
39        catch_types: &["catch_clause"],
40        conditional_types: &["conditional_expression"],
41        case_types: &["case_statement"],
42        binary_types: &["binary_expression"],
43        logical_operators: &["&&", "||", "and", "or"],
44        jump_types: &["break_statement", "continue_statement"],
45        named_function_boundary_types: &["function_definition"],
46        anonymous_function_types: &["lambda_expression"],
47        else_clause_types: &["else_clause"],
48        default_case_predicate: Some(cpp_is_default_case),
49        ..cognitive_complexity::Config::empty()
50    });
51
52fn cpp_is_default_case(node: Node<'_>, _source: &str) -> bool {
53    node.child_by_field_name("value").is_none()
54}
55
56/// Extract `file` under the dialect its own path selects.
57pub fn parse_cpp_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
58    parse_cpp_file_in_dialect(
59        file,
60        source,
61        tree,
62        LanguageDialect::for_path(Language::Cpp, file.rel_path()),
63    )
64}
65
66/// Extract a source after seeding its object-like field-list environment from
67/// include-visible declarations. The seed is consumed structurally by the
68/// ordinary declaration walk; local definitions and undef directives retain
69/// their source-order semantics.
70pub fn parse_cpp_file_with_object_macro_fields(
71    file: &ProjectFile,
72    source: &str,
73    tree: &Tree,
74    object_macro_fields: HashMap<String, ObjectMacroReplacement>,
75) -> ParsedFile {
76    let root = tree.root_node();
77    let ancestry = ParentIndex::new(root);
78    let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
79    parse_cpp_reading_with_object_macro_fields(
80        file,
81        source,
82        root,
83        LanguageDialect::for_path(Language::Cpp, file.rel_path()),
84        &ancestry,
85        object_macro_fields,
86        &orphaned_namespaces,
87    )
88    .parsed
89}
90
91/// Extract `file` under an explicitly named dialect.
92///
93/// A header carries no compilation language of its own, so its blob has two
94/// legitimate readings: under [`LanguageDialect::CppC`] a tag declared inside
95/// an aggregate member list has file scope (C17 6.2.1), under the plain C++
96/// dialect it is a nested class. Milestone 3 of
97/// `.agents/plans/c-compilation-language-tag-scope.md` stores both readings of
98/// a header when they differ, so extraction has to be reachable under a
99/// dialect the path itself does not name.
100///
101/// This entry point owns the whole tree's work, including the parent index the
102/// walk asks its ancestor questions of. A caller that wants both readings of
103/// one tree must use [`parse_cpp_file_with_ancestry`] and
104/// [`parse_cpp_c_reading`] instead, which share that index.
105pub fn parse_cpp_file_in_dialect(
106    file: &ProjectFile,
107    source: &str,
108    tree: &Tree,
109    dialect: LanguageDialect,
110) -> ParsedFile {
111    let root = tree.root_node();
112    let ancestry = ParentIndex::new(root);
113    let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
114    parse_cpp_reading(file, source, root, dialect, &ancestry, &orphaned_namespaces).parsed
115}
116
117/// Extract `file` under the dialect its own path selects, asking its ancestor
118/// questions of a caller-owned index over the same tree.
119///
120/// The index costs one hash entry per node and is a property of the tree, not
121/// of the reading, so a header that gets both readings builds it once. See
122/// [`parse_cpp_c_reading`] for the second reading.
123pub fn parse_cpp_file_with_ancestry<'tree>(
124    file: &ProjectFile,
125    source: &str,
126    root: Node<'tree>,
127    ancestry: &ParentIndex<'tree>,
128) -> ParsedFile {
129    let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
130    parse_cpp_file_with_ancestry_and_orphaned_namespaces(
131        file,
132        source,
133        root,
134        ancestry,
135        &orphaned_namespaces,
136    )
137    .parsed
138}
139
140/// Extract the primary reading and its C tag-scope execution witness using
141/// caller-owned recovery state.
142///
143/// The recovery index is a property of the tree and source, rather than of a
144/// dialect reading. Callers that need both the C++ and C readings should build
145/// it once and pass it to this function and
146/// [`parse_cpp_c_reading_with_orphaned_namespaces`]. The witness remains
147/// sticky across the root walk, all recovery work, and every queued reparse.
148pub fn parse_cpp_file_with_ancestry_and_orphaned_namespaces<'tree>(
149    file: &ProjectFile,
150    source: &str,
151    root: Node<'tree>,
152    ancestry: &ParentIndex<'tree>,
153    orphaned_namespaces: &OrphanedNamespaceScopeIndex,
154) -> CppPrimaryReading {
155    parse_cpp_reading(
156        file,
157        source,
158        root,
159        LanguageDialect::for_path(Language::Cpp, file.rel_path()),
160        ancestry,
161        orphaned_namespaces,
162    )
163}
164
165/// The C reading of a tree whose other reading is already in hand.
166///
167/// The dialect decides exactly one thing: where a struct/union/enum tag
168/// declared inside another aggregate's member list is declared (the
169/// `c_tag_semantics` readers in [`crate::declarations`]). Everything else this
170/// file's extraction produces is a property of the tree and of the source text
171/// -- the parent index, the `#include` sweep, the identifier sweep, and the
172/// quoted-include line recovery -- so the C reading takes those from `primary`
173/// rather than recomputing them over the same bytes. A debug build re-runs the
174/// sweeps and asserts they agree, which is what holds the claim honest if a
175/// future walk ever starts contributing to one of those families.
176pub fn parse_cpp_c_reading<'tree>(
177    file: &ProjectFile,
178    source: &str,
179    root: Node<'tree>,
180    ancestry: &ParentIndex<'tree>,
181    primary: &ParsedFile,
182) -> ParsedFile {
183    let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
184    parse_cpp_c_reading_with_orphaned_namespaces(
185        file,
186        source,
187        root,
188        ancestry,
189        primary,
190        &orphaned_namespaces,
191    )
192}
193
194/// Extract the C reading while reusing the primary reading's recovery index.
195///
196/// The declaration walk remains separate because C tag scope changes the
197/// identity and ownership of declarations. Only recovery state proven to be
198/// dialect-independent is shared.
199pub fn parse_cpp_c_reading_with_orphaned_namespaces<'tree>(
200    file: &ProjectFile,
201    source: &str,
202    root: Node<'tree>,
203    ancestry: &ParentIndex<'tree>,
204    primary: &ParsedFile,
205    orphaned_namespaces: &OrphanedNamespaceScopeIndex,
206) -> ParsedFile {
207    let mut parsed = ParsedFile::new(String::new());
208    parsed.imports = primary.imports.clone();
209    parsed.type_identifiers = primary.type_identifiers.clone();
210    {
211        let mut visitor = CppVisitor {
212            file,
213            source,
214            parsed: &mut parsed,
215            c_tag_semantics: true,
216            c_tag_scope_witness: false,
217            recovered_class_sibling_scopes: HashMap::default(),
218            consumed_fragment_regions: Vec::new(),
219            orphaned_namespaces,
220            partitioned_regions: Vec::new(),
221            namespace_forward_scans: HashMap::default(),
222            field_owners: None,
223            recovery_captures: Vec::new(),
224            object_macro_fields: HashMap::default(),
225            ambiguous_object_macro_fields: HashSet::default(),
226        };
227        visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
228    }
229    parsed.finalize_deferred_replacements();
230
231    #[cfg(debug_assertions)]
232    {
233        let mut recomputed = ParsedFile::new(String::new());
234        collect_cpp_includes(root, source, &mut recomputed);
235        collect_cpp_identifiers(root, source, &mut recomputed.type_identifiers);
236        recover_quoted_includes(source, &mut recomputed);
237        assert_eq!(
238            parsed.imports, recomputed.imports,
239            "the C reading's includes are the C++ reading's includes: {:?}",
240            file
241        );
242        assert_eq!(
243            parsed.type_identifiers, recomputed.type_identifiers,
244            "the C reading's identifiers are the C++ reading's identifiers: {:?}",
245            file
246        );
247    }
248
249    parsed
250}
251
252/// One complete reading of `root`, sweeps included.
253fn parse_cpp_reading<'tree>(
254    file: &ProjectFile,
255    source: &str,
256    root: Node<'tree>,
257    dialect: LanguageDialect,
258    ancestry: &ParentIndex<'tree>,
259    orphaned_namespaces: &OrphanedNamespaceScopeIndex,
260) -> CppPrimaryReading {
261    parse_cpp_reading_with_object_macro_fields(
262        file,
263        source,
264        root,
265        dialect,
266        ancestry,
267        HashMap::default(),
268        orphaned_namespaces,
269    )
270}
271
272fn parse_cpp_reading_with_object_macro_fields<'tree>(
273    file: &ProjectFile,
274    source: &str,
275    root: Node<'tree>,
276    dialect: LanguageDialect,
277    ancestry: &ParentIndex<'tree>,
278    object_macro_fields: HashMap<String, ObjectMacroReplacement>,
279    orphaned_namespaces: &OrphanedNamespaceScopeIndex,
280) -> CppPrimaryReading {
281    let mut parsed = ParsedFile::new(String::new());
282
283    collect_cpp_includes(root, source, &mut parsed);
284    collect_cpp_identifiers(root, source, &mut parsed.type_identifiers);
285
286    let c_tag_scope_witness;
287    {
288        let mut visitor = CppVisitor {
289            file,
290            source,
291            parsed: &mut parsed,
292            c_tag_semantics: dialect == LanguageDialect::CppC,
293            c_tag_scope_witness: false,
294            recovered_class_sibling_scopes: HashMap::default(),
295            consumed_fragment_regions: Vec::new(),
296            orphaned_namespaces,
297            partitioned_regions: Vec::new(),
298            namespace_forward_scans: HashMap::default(),
299            field_owners: None,
300            recovery_captures: Vec::new(),
301            object_macro_fields,
302            ambiguous_object_macro_fields: HashSet::default(),
303        };
304        visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
305        c_tag_scope_witness = visitor.c_tag_scope_witness;
306    }
307    // A line scan over the source rather than a tree walk: it recovers the
308    // quoted directives a parse error hid from the tree, skipping any snippet
309    // the sweep above already recorded.
310    recover_quoted_includes(source, &mut parsed);
311    parsed.finalize_deferred_replacements();
312    CppPrimaryReading {
313        parsed,
314        c_tag_scope_witness,
315    }
316}
317
318/// Whether two readings of one blob disagree about any identity-bearing
319/// output: which declarations exist, what they are named, which are top level
320/// or definition-lookup entries, where they start and end, what they are
321/// nested in, and how they are signed.
322///
323/// This is the "differs" test behind storing a header's C projection only when
324/// it says something the C++ projection does not (issue #1970): absence of the
325/// second row-set must unambiguously mean "identical", so anything a
326/// resolution surface can observe has to be compared here.
327pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
328    left.declarations() != right.declarations()
329        || left.top_level_declarations != right.top_level_declarations
330        || left.definition_lookup_units != right.definition_lookup_units
331        || left.children != right.children
332        || left.ranges != right.ranges
333        || left.signatures != right.signatures
334        || left.type_aliases != right.type_aliases
335}
336
337pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
338    let trimmed = reference.trim();
339    let before_args = trimmed
340        .split_once('(')
341        .map(|(head, _)| head)
342        .unwrap_or(trimmed);
343    before_args
344        .rsplit_once("::")
345        .or_else(|| before_args.rsplit_once('.'))
346        .map(|(receiver, _)| receiver.to_string())
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use tree_sitter::Parser;
353
354    fn cpp_tree(source: &str) -> tree_sitter::Tree {
355        let mut parser = Parser::new();
356        parser
357            .set_language(&tree_sitter_cpp::LANGUAGE.into())
358            .expect("C++ grammar");
359        parser.parse(source, None).expect("C++ tree")
360    }
361
362    /// Everything one reading publishes, rendered so that two readings can be
363    /// compared without depending on hash iteration order.
364    fn published_facts(parsed: &ParsedFile) -> Vec<String> {
365        let mut facts = vec![
366            format!("package={}", parsed.package_name),
367            format!("content_qualifier={}", parsed.content_qualifier),
368            format!("top_level={:?}", parsed.top_level_declarations),
369            format!("imports={:?}", parsed.imports),
370            format!("materializations={:?}", parsed.materialization_records),
371            format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
372        ];
373        let mut unordered = |label: &str, mut entries: Vec<String>| {
374            entries.sort();
375            facts.push(format!("{label}={entries:?}"));
376        };
377        unordered(
378            "declarations",
379            parsed.declarations().iter().map(debug_of).collect(),
380        );
381        unordered(
382            "definition_lookup",
383            parsed
384                .definition_lookup_units
385                .iter()
386                .map(debug_of)
387                .collect(),
388        );
389        unordered(
390            "type_identifiers",
391            parsed.type_identifiers.iter().map(debug_of).collect(),
392        );
393        unordered(
394            "type_aliases",
395            parsed.type_aliases.iter().map(debug_of).collect(),
396        );
397        unordered(
398            "scala_traits",
399            parsed.scala_traits.iter().map(debug_of).collect(),
400        );
401        unordered(
402            "test_region_units",
403            parsed.test_region_units.iter().map(debug_of).collect(),
404        );
405        unordered(
406            "navigation_truncated",
407            parsed
408                .navigation_ranges_truncated
409                .iter()
410                .map(debug_of)
411                .collect(),
412        );
413        unordered("children", pairs(&parsed.children));
414        unordered("ranges", pairs(&parsed.ranges));
415        unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
416        unordered("signatures", pairs(&parsed.signatures));
417        unordered("signature_metadata", pairs(&parsed.signature_metadata));
418        unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
419        unordered(
420            "supertype_lookup_paths",
421            pairs(&parsed.supertype_lookup_paths),
422        );
423        unordered("scala_exports", pairs(&parsed.scala_exports));
424        unordered(
425            "cpp_template_metadata",
426            pairs(&parsed.cpp_template_metadata),
427        );
428        unordered(
429            "ruby_method_dispatch_modes",
430            pairs(&parsed.ruby_method_dispatch_modes),
431        );
432        facts
433    }
434
435    fn debug_of<T: std::fmt::Debug>(value: T) -> String {
436        format!("{value:?}")
437    }
438
439    fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
440        map: &brokk_bifrost_core::hash::HashMap<K, V>,
441    ) -> Vec<String> {
442        map.iter().map(|entry| format!("{entry:?}")).collect()
443    }
444
445    /// The two readings of one header, taken the way production takes them,
446    /// publish exactly what two independent extractions publish.
447    ///
448    /// Milestone 3b of `.agents/plans/immutable-revision-persisted-fact-reuse.md`
449    /// stopped the C reading from rebuilding the parent index, re-sweeping
450    /// includes and identifiers, and re-running the quoted-include line scan
451    /// that the C++ reading of the same tree had already produced. That is a
452    /// deduplication and nothing else: if any published fact moved, something
453    /// believed dialect-insensitive is not.
454    ///
455    /// The error-recovery shapes are the interesting half. Their walks reparse
456    /// byte regions into trees of their own and re-own sibling nodes under
457    /// recovered class scopes, so they are where a shared index would show up
458    /// if sharing were unsound.
459    #[test]
460    fn a_shared_reading_publishes_what_an_independent_one_publishes() {
461        let fixtures: &[(&str, &str, bool, bool)] = &[
462            (
463                "nested tag inside an aggregate, plus a nested include",
464                r#"
465#include <vector>
466struct outer {
467#include "member_list.def"
468    struct inner { int v; } i;
469};
470struct inner *p;
471                "#,
472                true,
473                true,
474            ),
475            (
476                "a quoted include only the line scan can recover",
477                r#"
478#include "visible.h"
479class Broken {
480    void method(
481#include "hidden.h"
482                "#,
483                false,
484                false,
485            ),
486            (
487                "forward declarations replaced by their definitions",
488                r#"
489typedef unsigned long long u64;
490namespace generated {
491struct tag0;
492struct tag1;
493struct tag1 {
494    struct nested { int v; } n;
495    u64 first;
496};
497struct tag0 { int second; };
498}
499                "#,
500                true,
501                true,
502            ),
503            (
504                "a fragmented export-macro class body",
505                r#"
506#define SIMPLECPP_LIB
507namespace simplecpp {
508using TokenString = std::string;
509struct Location { int line{}; };
510class SIMPLECPP_LIB Token {
511  TokenString prefix;
512  void prefix_method() {}
513 public:
514  Token(const TokenString &s, const Location &loc, bool wsahead = false) :
515      whitespaceahead(wsahead), location(loc), string(s)
516      {
517      flags();
518  }
519  struct Nested { int v; } nested;
520  TokenString string;
521  bool whitespaceahead;
522  Location location;
523 private:
524  void flags() {
525      whitespaceahead = true;
526  }
527};
528}
529                "#,
530                true,
531                true,
532            ),
533            (
534                "nested struct, union, and enum tags",
535                r#"
536struct StructOwner { struct StructTag { int value; }; };
537struct UnionOwner { union UnionTag { int value; }; };
538struct EnumOwner { enum EnumTag { value }; };
539"#,
540                true,
541                true,
542            ),
543            (
544                "C++ class and anonymous tag near misses",
545                r#"
546struct Owner {
547    class NestedClass { int value; };
548    struct { int anonymous_value; } anonymous_member;
549};
550struct TopLevel { int value; };
551"#,
552                false,
553                false,
554            ),
555            (
556                "tag reparsed under a recovered export class",
557                r#"
558namespace api {
559
560/**
561* Doc comment
562*/
563class PROJECT_PUBLIC_API(2, 0) RecoveryOwner : public virtual BaseKey {
564   public:
565      /** Construct from a point. */
566      RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
567
568#if defined(PROJECT_HAS_LEGACY_POINT)
569      /** Construct from a legacy point. */
570      RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
571#endif
572
573      struct RecoveredTag { int value; };
574      std::string algo_name() const override;
575      AlgorithmIdentifier algorithm_identifier() const override;
576};
577}
578"#,
579                true,
580                true,
581            ),
582        ];
583
584        for (name, source, expected_witness, expected_differs) in fixtures {
585            let file = ProjectFile::new(
586                std::env::current_dir().expect("test working directory must be available"),
587                "src/widget.h",
588            );
589            let tree = cpp_tree(source);
590            let root = tree.root_node();
591
592            let independent_primary = parse_cpp_file(&file, source, &tree);
593            let independent_c =
594                parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
595            let independent_primary_facts = published_facts(&independent_primary);
596            let independent_c_facts = published_facts(&independent_c);
597            let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
598            assert_eq!(
599                independent_differs,
600                *expected_differs,
601                "independent C/C++ projection difference for {name}; root.has_error()={}; CST={}; C++ declarations={:#?}; C declarations={:#?}; C++ facts: {independent_primary_facts:#?}; C facts: {independent_c_facts:#?}",
602                root.has_error(),
603                root.to_sexp(),
604                independent_primary.declarations(),
605                independent_c.declarations(),
606            );
607
608            let ancestry = ParentIndex::new(root);
609            let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
610            let primary = parse_cpp_file_with_ancestry_and_orphaned_namespaces(
611                &file,
612                source,
613                root,
614                &ancestry,
615                &orphaned_namespaces,
616            );
617            let c_tag_scope_witness = primary.c_tag_scope_witness;
618            let shared_primary = primary.parsed;
619
620            if *name == "tag reparsed under a recovered export class" {
621                assert!(
622                    root.has_error(),
623                    "the recovery fixture must parse with errors"
624                );
625                let recovered_owner = independent_primary
626                    .declarations()
627                    .iter()
628                    .find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
629                    .expect("the malformed export-class head must recover its owner");
630                let owner_range = independent_primary
631                    .ranges
632                    .get(recovered_owner)
633                    .and_then(|ranges| ranges.first())
634                    .expect("the recovered owner must retain its class range");
635                let mut pending = vec![root];
636                let mut recognized_recovery = false;
637                while let Some(node) = pending.pop() {
638                    if crate::declarations::recovered_fragmented_class_has_body(
639                        node,
640                        source,
641                        "RecoveryOwner",
642                        owner_range,
643                    ) {
644                        recognized_recovery = true;
645                        break;
646                    }
647                    let mut cursor = node.walk();
648                    pending.extend(node.named_children(&mut cursor));
649                }
650                assert!(
651                    recognized_recovery,
652                    "the fixture must enter the fragmented export-class recovery path"
653                );
654                let mut pending = vec![root];
655                let mut tag = None;
656                while let Some(node) = pending.pop() {
657                    if matches!(
658                        node.kind(),
659                        "struct_specifier" | "union_specifier" | "enum_specifier"
660                    ) && node.child_by_field_name("name").is_some_and(|name| {
661                        name.utf8_text(source.as_bytes())
662                            .is_ok_and(|text| text == "RecoveredTag")
663                    }) {
664                        tag = Some(node);
665                        break;
666                    }
667                    let mut cursor = node.walk();
668                    pending.extend(node.named_children(&mut cursor));
669                }
670                let tag = tag.expect("the original recovered fixture exposes its tag node");
671                let mut ancestor = ancestry.parent(tag);
672                while let Some(node) = ancestor {
673                    assert!(
674                        !matches!(
675                            node.kind(),
676                            "class_specifier" | "struct_specifier" | "union_specifier"
677                        ),
678                        "the original CST must not already give RecoveredTag an aggregate owner"
679                    );
680                    ancestor = ancestry.parent(node);
681                }
682            }
683
684            assert_eq!(
685                c_tag_scope_witness,
686                *expected_witness,
687                "unexpected C tag-scope witness for {name}; root.has_error()={}; CST={}; C++ declarations={:#?}; C declarations={:#?}; independent C++ facts: {independent_primary_facts:#?}; independent C facts: {independent_c_facts:#?}",
688                root.has_error(),
689                root.to_sexp(),
690                independent_primary.declarations(),
691                independent_c.declarations(),
692            );
693            assert_eq!(
694                independent_primary_facts,
695                published_facts(&shared_primary),
696                "C++ reading of {name}"
697            );
698            if *expected_witness {
699                let shared_c = parse_cpp_c_reading_with_orphaned_namespaces(
700                    &file,
701                    source,
702                    root,
703                    &ancestry,
704                    &shared_primary,
705                    &orphaned_namespaces,
706                );
707                assert_eq!(
708                    independent_c_facts,
709                    published_facts(&shared_c),
710                    "C reading of {name}"
711                );
712            } else {
713                assert_eq!(
714                    independent_primary_facts, independent_c_facts,
715                    "a false witness must imply identical complete readings for {name}"
716                );
717                assert!(
718                    !independent_differs,
719                    "a false witness must make the existing publication comparator agree for {name}"
720                );
721            }
722        }
723    }
724
725    /// A tag nested in an aggregate is the whole reason the second reading
726    /// exists, so the fixture above must actually produce two different
727    /// readings; otherwise the comparison would pass on two empty answers.
728    #[test]
729    fn the_nested_tag_fixture_really_has_two_readings() {
730        let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
731        let file = ProjectFile::new(
732            std::env::current_dir().expect("test working directory must be available"),
733            "src/widget.h",
734        );
735        let tree = cpp_tree(source);
736        let root = tree.root_node();
737        let ancestry = ParentIndex::new(root);
738        let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
739        let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
740        assert!(
741            cpp_projections_differ(&primary, &c_reading),
742            "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
743            primary.declarations(),
744            c_reading.declarations()
745        );
746    }
747
748    /// Every `#include` is an include claim, wherever it is written. The
749    /// declaration walk descends only through declaration scopes, so a
750    /// directive inside a function body (llama.cpp's `sycl/info/aspects.def`
751    /// inside a `switch`) or inside a class body (Eigen's
752    /// `EIGEN_DENSEBASE_PLUGIN` in `DenseBase.h`) used to be invisible.
753    ///
754    /// The nested directive here is not a quoted include, so the established
755    /// `recover_quoted_includes` line scan cannot supply it: only the preorder
756    /// sweep over the tree can.
757    #[test]
758    fn includes_are_recorded_at_every_depth() {
759        let source = r#"
760#include <vector>
761
762class Widget {
763public:
764    int value() const;
765};
766
767int run() {
768    switch (0) {
769#include <sycl/info/aspects.def>
770    default:
771        return 0;
772    }
773}
774"#;
775        let file = ProjectFile::new(
776            std::env::current_dir().expect("test working directory must be available"),
777            "src/widget.cpp",
778        );
779        let mut parser = Parser::new();
780        parser
781            .set_language(&tree_sitter_cpp::LANGUAGE.into())
782            .expect("C++ grammar");
783        let tree = parser.parse(source, None).expect("C++ tree");
784
785        let parsed = parse_cpp_file(&file, source, &tree);
786        let includes = parsed
787            .imports
788            .iter()
789            .map(|import| import.raw_snippet.clone())
790            .collect::<Vec<_>>();
791
792        assert_eq!(
793            includes,
794            vec![
795                "#include <vector>".to_string(),
796                "#include <sycl/info/aspects.def>".to_string(),
797            ]
798        );
799        assert!(!parsed.declarations().is_empty());
800    }
801}