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::{parse_cpp_readings, parse_cpp_readings_with_object_macro_fields};
8use crate::graph::syntax::ObjectMacroReplacement;
9use brokk_bifrost_core::analyzer::ProjectFile;
10use brokk_bifrost_core::analyzer::cognitive_complexity;
11use brokk_bifrost_core::analyzer::model::{Language, LanguageDialect};
12use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
13use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
14use brokk_bifrost_core::hash::HashMap;
15use std::sync::LazyLock;
16use tree_sitter::{Node, Tree};
17
18/// The file extension `CppAdapter` reports. `Language::Cpp` also covers `.c`,
19/// `.cc`, `.cxx` and the header spellings; this is only the canonical one.
20pub const CPP_FILE_EXTENSION: &str = "cpp";
21
22/// Tree-sitter node-kind mapping used by the cognitive-complexity scorer for
23/// C++. Node names are from the tree-sitter-cpp grammar.
24pub static CPP_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
25    LazyLock::new(|| cognitive_complexity::Config {
26        if_types: &["if_statement"],
27        loop_types: &["for_statement", "while_statement", "do_statement"],
28        catch_types: &["catch_clause"],
29        conditional_types: &["conditional_expression"],
30        case_types: &["case_statement"],
31        binary_types: &["binary_expression"],
32        logical_operators: &["&&", "||", "and", "or"],
33        jump_types: &["break_statement", "continue_statement"],
34        named_function_boundary_types: &["function_definition"],
35        anonymous_function_types: &["lambda_expression"],
36        else_clause_types: &["else_clause"],
37        default_case_predicate: Some(cpp_is_default_case),
38        ..cognitive_complexity::Config::empty()
39    });
40
41fn cpp_is_default_case(node: Node<'_>, _source: &str) -> bool {
42    node.child_by_field_name("value").is_none()
43}
44
45/// Extract `file` under the dialect its own path selects.
46pub fn parse_cpp_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
47    parse_cpp_file_in_dialect(
48        file,
49        source,
50        tree,
51        LanguageDialect::for_path(Language::Cpp, file.rel_path()),
52    )
53}
54
55/// Extract a source after seeding its object-like field-list environment from
56/// include-visible declarations. The seed is consumed structurally by the
57/// ordinary declaration walk; local definitions and undef directives retain
58/// their source-order semantics.
59pub fn parse_cpp_file_with_object_macro_fields(
60    file: &ProjectFile,
61    source: &str,
62    tree: &Tree,
63    object_macro_fields: HashMap<String, ObjectMacroReplacement>,
64) -> ParsedFile {
65    let root = tree.root_node();
66    let ancestry = ParentIndex::new(root);
67    parse_cpp_readings_with_object_macro_fields(
68        file,
69        source,
70        root,
71        &ancestry,
72        &[LanguageDialect::for_path(Language::Cpp, file.rel_path())],
73        object_macro_fields,
74    )
75    .pop()
76    .expect("one dialect reading")
77}
78
79/// Extract `file` under an explicitly named dialect.
80///
81/// A header carries no compilation language of its own, so its blob has two
82/// legitimate readings: under [`LanguageDialect::CppC`] a tag declared inside
83/// an aggregate member list has file scope (C17 6.2.1), under the plain C++
84/// dialect it is a nested class. The analyzer stores both readings of a header
85/// when they differ, so extraction has to be reachable under a
86/// dialect the path itself does not name.
87///
88/// This entry point owns the whole tree's work, including the parent index the
89/// walk asks its ancestor questions of.
90pub fn parse_cpp_file_in_dialect(
91    file: &ProjectFile,
92    source: &str,
93    tree: &Tree,
94    dialect: LanguageDialect,
95) -> ParsedFile {
96    let root = tree.root_node();
97    let ancestry = ParentIndex::new(root);
98    parse_cpp_file_from_root(file, source, root, &ancestry, dialect)
99}
100
101fn parse_cpp_file_from_root<'tree>(
102    file: &ProjectFile,
103    source: &str,
104    root: Node<'tree>,
105    ancestry: &ParentIndex<'tree>,
106    dialect: LanguageDialect,
107) -> ParsedFile {
108    parse_cpp_readings(file, source, root, ancestry, &[dialect])
109        .pop()
110        .expect("one dialect reading")
111}
112
113/// Extract a header's C++ and C readings together. Both declaration policies
114/// consume one primary event stream. Exact construction-ID finalization gives
115/// each dialect stable content-owned source and structural projections. The C
116/// reading is absent when the primary declaration walk proves identical tag scope.
117pub fn parse_cpp_file_with_readings<'tree>(
118    file: &ProjectFile,
119    source: &str,
120    root: Node<'tree>,
121    ancestry: &ParentIndex<'tree>,
122) -> (ParsedFile, Option<ParsedFile>) {
123    let mut readings = parse_cpp_readings(
124        file,
125        source,
126        root,
127        ancestry,
128        &[
129            LanguageDialect::for_path(Language::Cpp, file.rel_path()),
130            LanguageDialect::CppC,
131        ],
132    );
133    let c = (readings.len() == 2).then(|| readings.pop().expect("C reading"));
134    let primary = readings.pop().expect("primary reading");
135    (primary, c)
136}
137
138/// Whether two readings of one blob disagree about any identity-bearing
139/// output: which declarations exist, what they are named, which are top level
140/// or definition-lookup entries, where they start and end, what they are
141/// nested in, and how they are signed.
142///
143/// This is the "differs" test behind storing a header's C projection only when
144/// it says something the C++ projection does not (issue #1970): absence of the
145/// second row-set must unambiguously mean "identical", so anything a
146/// resolution surface can observe has to be compared here.
147pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
148    left.declarations() != right.declarations()
149        || left.top_level_declarations != right.top_level_declarations
150        || left.definition_lookup_units != right.definition_lookup_units
151        || left.children != right.children
152        || left.ranges != right.ranges
153        || left.signatures != right.signatures
154        || left.type_aliases != right.type_aliases
155        || left.resolution_facts != right.resolution_facts
156}
157
158pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
159    brokk_bifrost_core::analyzer::common::extract_call_reference_parts(reference, &["::", "."])
160        .map(|(receiver, _)| receiver.to_string())
161}
162
163/// Whether this complete subtree is numeric initializer data. No source-text
164/// classification or semantic inertness claim is involved.
165pub fn is_numeric_initializer_data(
166    node: Node<'_>,
167    should_stop: &mut dyn FnMut() -> bool,
168) -> std::ops::ControlFlow<(), bool> {
169    use std::ops::ControlFlow;
170    if node.kind() != "initializer_list" {
171        return ControlFlow::Continue(false);
172    }
173    use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_tree_preorder};
174    let mut result = ControlFlow::Continue(true);
175    walk_tree_preorder(node, true, |current| {
176        if should_stop() {
177            result = ControlFlow::Break(());
178            return WalkControl::Break;
179        }
180        if current.is_error() || current.is_missing() {
181            result = ControlFlow::Continue(false);
182            return WalkControl::Break;
183        }
184        let allowed = match current.kind() {
185            "initializer_list" => true,
186            "number_literal" => current.child_count() == 0,
187            "unary_expression" => {
188                current.child_count() == 2
189                    && current
190                        .child_by_field_name("operator")
191                        .is_some_and(|op| matches!(op.kind(), "+" | "-"))
192                    && current
193                        .child_by_field_name("argument")
194                        .is_some_and(|arg| arg.kind() == "number_literal")
195            }
196            "{" | "}" | "," => current
197                .parent()
198                .is_some_and(|parent| parent.kind() == "initializer_list"),
199            "+" | "-" => current
200                .parent()
201                .is_some_and(|parent| parent.kind() == "unary_expression"),
202            _ => false,
203        };
204        if allowed {
205            WalkControl::Continue
206        } else {
207            result = ControlFlow::Continue(false);
208            WalkControl::Break
209        }
210    });
211    result
212}
213
214#[cfg(test)]
215mod tests {
216
217    #[test]
218    fn numeric_initializer_grammar_and_cancellation() {
219        use crate::grammar_test_support::{SOURCE, at, field, nodes, tree};
220
221        use std::ops::ControlFlow;
222        let parsed = tree(SOURCE);
223        // Independent, complete preorder manifests (unnamed punctuation included).
224        // `polls` is the admissible prefix plus its first rejected node, or the
225        // whole manifest for an admitted initializer. Comments conservatively
226        // prevent numeric-data pruning; identifiers, calls and recovery do too.
227        let cases = [
228            (
229                499..517,
230                true,
231                16,
232                &[
233                    ("initializer_list", 499..517),
234                    ("{", 499..500),
235                    ("initializer_list", 500..507),
236                    ("{", 500..501),
237                    ("number_literal", 501..502),
238                    (",", 502..503),
239                    ("number_literal", 504..506),
240                    ("}", 506..507),
241                    (",", 507..508),
242                    ("initializer_list", 509..516),
243                    ("{", 509..510),
244                    ("number_literal", 510..512),
245                    (",", 512..513),
246                    ("number_literal", 514..515),
247                    ("}", 515..516),
248                    ("}", 516..517),
249                ][..],
250            ),
251            (
252                535..552,
253                false,
254                5,
255                &[
256                    ("initializer_list", 535..552),
257                    ("{", 535..536),
258                    ("number_literal", 536..537),
259                    (",", 537..538),
260                    ("comment", 539..549),
261                    ("number_literal", 550..551),
262                    ("}", 551..552),
263                ][..],
264            ),
265            (
266                568..575,
267                false,
268                3,
269                &[
270                    ("initializer_list", 568..575),
271                    ("{", 568..569),
272                    ("identifier", 569..574),
273                    ("}", 574..575),
274                ][..],
275            ),
276            (
277                591..599,
278                false,
279                3,
280                &[
281                    ("initializer_list", 591..599),
282                    ("{", 591..592),
283                    ("call_expression", 592..598),
284                    ("identifier", 592..596),
285                    ("argument_list", 596..598),
286                    ("(", 596..597),
287                    (")", 597..598),
288                    ("}", 598..599),
289                ][..],
290            ),
291            (
292                616..622,
293                false,
294                5,
295                &[
296                    ("initializer_list", 616..622),
297                    ("{", 616..617),
298                    ("number_literal", 617..618),
299                    (",", 618..619),
300                    ("ERROR", 620..621),
301                    ("ERROR", 620..621),
302                    ("}", 621..622),
303                ][..],
304            ),
305            (
306                640..645,
307                false,
308                4,
309                &[
310                    ("initializer_list", 640..645),
311                    ("{", 640..641),
312                    ("number_literal", 641..642),
313                    ("ERROR", 643..644),
314                    ("number_literal", 643..644),
315                    ("}", 644..645),
316                ][..],
317            ),
318            (
319                662..672,
320                true,
321                10,
322                &[
323                    ("initializer_list", 662..672),
324                    ("{", 662..663),
325                    ("unary_expression", 663..666),
326                    ("-", 663..664),
327                    ("number_literal", 665..666),
328                    (",", 666..667),
329                    ("unary_expression", 668..671),
330                    ("+", 668..669),
331                    ("number_literal", 670..671),
332                    ("}", 671..672),
333                ][..],
334            ),
335            (
336                696..704,
337                false,
338                3,
339                &[
340                    ("initializer_list", 696..704),
341                    ("{", 696..697),
342                    ("call_expression", 697..703),
343                    ("identifier", 697..701),
344                    ("argument_list", 701..703),
345                    ("(", 701..702),
346                    ("number_literal", 702..703),
347                    (")", 703..703),
348                    ("}", 703..704),
349                ][..],
350            ),
351        ];
352        for (span, admitted, polls, manifest) in cases {
353            let initializer = at(parsed.root_node(), "initializer_list", span.clone());
354            let actual = nodes(initializer);
355            assert_eq!(
356                actual
357                    .iter()
358                    .map(|node| (node.kind(), node.byte_range()))
359                    .collect::<Vec<_>>(),
360                manifest
361            );
362            for (index, node) in actual.iter().enumerate() {
363                if node.kind() == "unary_expression" {
364                    let (operator_kind, operator_span) = &manifest[index + 1];
365                    field(*node, "operator", operator_kind, operator_span.clone());
366                    field(
367                        *node,
368                        "argument",
369                        "number_literal",
370                        manifest[index + 2].1.clone(),
371                    );
372                    assert!(!node.child_by_field_name("operator").unwrap().is_named());
373                }
374                if node.kind() == "number_literal" && admitted {
375                    assert!(matches!(
376                        node.parent().unwrap().kind(),
377                        "initializer_list" | "unary_expression"
378                    ));
379                    assert_eq!(node.child_count(), 0);
380                }
381                if node.kind() == ")" && node.start_byte() == node.end_byte() {
382                    assert!(node.is_missing());
383                    assert_eq!(node.parent().unwrap().kind(), "argument_list");
384                }
385            }
386            let mut visited = 0;
387            assert_eq!(
388                crate::adapter::is_numeric_initializer_data(initializer, &mut || {
389                    visited += 1;
390                    false
391                }),
392                ControlFlow::Continue(admitted)
393            );
394            assert_eq!(visited, polls, "poll manifest for {span:?}");
395            for cancel_at in 1..=polls {
396                let mut visited = 0;
397                assert_eq!(
398                    crate::adapter::is_numeric_initializer_data(initializer, &mut || {
399                        visited += 1;
400                        visited == cancel_at
401                    }),
402                    ControlFlow::Break(()),
403                    "cancel at {cancel_at} in {span:?}"
404                );
405                assert_eq!(visited, cancel_at);
406            }
407        }
408    }
409
410    use super::*;
411    use brokk_bifrost_core::analyzer::resolution_facts::{ResolutionNameFact, ResolutionNameId};
412    use tree_sitter::Parser;
413
414    fn cpp_tree(source: &str) -> tree_sitter::Tree {
415        crate::diagnostics::parse_cpp_tree(source)
416    }
417
418    /// Everything one reading publishes, rendered so that two readings can be
419    /// compared without depending on hash iteration order.
420    fn published_facts(parsed: &ParsedFile) -> Vec<String> {
421        let mut facts = vec![
422            format!("package={}", parsed.package_name),
423            format!("content_qualifier={}", parsed.content_qualifier),
424            format!("top_level={:?}", parsed.top_level_declarations),
425            format!("imports={:?}", parsed.imports),
426            format!("materializations={:?}", parsed.materialization_records),
427            format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
428        ];
429        let mut unordered = |label: &str, mut entries: Vec<String>| {
430            entries.sort();
431            facts.push(format!("{label}={entries:?}"));
432        };
433        unordered(
434            "declarations",
435            parsed.declarations().iter().map(debug_of).collect(),
436        );
437        unordered(
438            "definition_lookup",
439            parsed
440                .definition_lookup_units
441                .iter()
442                .map(debug_of)
443                .collect(),
444        );
445        unordered(
446            "type_identifiers",
447            parsed.type_identifiers.iter().map(debug_of).collect(),
448        );
449        unordered(
450            "type_aliases",
451            parsed.type_aliases.iter().map(debug_of).collect(),
452        );
453        unordered(
454            "scala_traits",
455            parsed.scala_traits.iter().map(debug_of).collect(),
456        );
457        unordered(
458            "test_region_units",
459            parsed.test_region_units.iter().map(debug_of).collect(),
460        );
461        unordered(
462            "navigation_truncated",
463            parsed
464                .navigation_ranges_truncated
465                .iter()
466                .map(debug_of)
467                .collect(),
468        );
469        unordered("children", pairs(&parsed.children));
470        unordered("ranges", pairs(&parsed.ranges));
471        unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
472        unordered("signatures", pairs(&parsed.signatures));
473        unordered("signature_metadata", pairs(&parsed.signature_metadata));
474        unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
475        unordered(
476            "supertype_lookup_paths",
477            pairs(&parsed.supertype_lookup_paths),
478        );
479        unordered("scala_exports", pairs(&parsed.scala_exports));
480        unordered(
481            "cpp_template_metadata",
482            pairs(&parsed.cpp_template_metadata),
483        );
484        unordered(
485            "ruby_method_dispatch_modes",
486            pairs(&parsed.ruby_method_dispatch_modes),
487        );
488        facts
489    }
490
491    fn debug_of<T: std::fmt::Debug>(value: T) -> String {
492        format!("{value:?}")
493    }
494
495    fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
496        map: &brokk_bifrost_core::hash::HashMap<K, V>,
497    ) -> Vec<String> {
498        map.iter().map(|entry| format!("{entry:?}")).collect()
499    }
500
501    #[test]
502    fn a_shared_reading_publishes_what_an_independent_one_publishes() {
503        let fixtures: &[(&str, &str, bool, bool)] = &[
504            (
505                "nested tag inside an aggregate, plus a nested include",
506                r#"
507#include <vector>
508struct outer {
509#include "member_list.def"
510    struct inner { int v; } i;
511};
512struct inner *p;
513                "#,
514                true,
515                true,
516            ),
517            (
518                "a quoted include only the line scan can recover",
519                r#"
520#include "visible.h"
521class Broken {
522    void method(
523#include "hidden.h"
524                "#,
525                false,
526                false,
527            ),
528            (
529                "forward declarations replaced by their definitions",
530                r#"
531typedef unsigned long long u64;
532namespace generated {
533struct tag0;
534struct tag1;
535struct tag1 {
536    struct nested { int v; } n;
537    u64 first;
538};
539struct tag0 { int second; };
540}
541                "#,
542                true,
543                true,
544            ),
545            (
546                "a fragmented export-macro class body",
547                r#"
548#define SIMPLECPP_LIB
549namespace simplecpp {
550using TokenString = std::string;
551struct Location { int line{}; };
552class SIMPLECPP_LIB Token {
553  TokenString prefix;
554  void prefix_method() {}
555 public:
556  Token(const TokenString &s, const Location &loc, bool wsahead = false) :
557      whitespaceahead(wsahead), location(loc), string(s)
558      {
559      flags();
560  }
561  struct Nested { int v; } nested;
562  TokenString string;
563  bool whitespaceahead;
564  Location location;
565 private:
566  void flags() {
567      whitespaceahead = true;
568  }
569};
570}
571                "#,
572                true,
573                true,
574            ),
575            (
576                "nested struct, union, and enum tags",
577                r#"
578struct StructOwner { struct StructTag { int value; }; };
579struct UnionOwner { union UnionTag { int value; }; };
580struct EnumOwner { enum EnumTag { value }; };
581"#,
582                true,
583                true,
584            ),
585            (
586                "C++ class and anonymous tag near misses",
587                r#"
588struct Owner {
589    class NestedClass { int value; };
590    struct { int anonymous_value; } anonymous_member;
591};
592struct TopLevel { int value; };
593"#,
594                false,
595                false,
596            ),
597            (
598                "tag reparsed under a recovered export class",
599                r#"
600namespace api {
601
602/**
603* Doc comment
604*/
605class PROJECT_PUBLIC_API RecoveryOwner : public virtual BaseKey {
606   public:
607      /** Construct from a point. */
608      RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
609
610#if defined(PROJECT_HAS_LEGACY_POINT)
611      /** Construct from a legacy point. */
612      RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
613#endif
614
615      struct RecoveredTag { int value; };
616      std::string algo_name() const override;
617      AlgorithmIdentifier algorithm_identifier() const override;
618};
619}
620"#,
621                true,
622                true,
623            ),
624        ];
625
626        for (name, source, expected_witness, expected_differs) in fixtures {
627            let file = ProjectFile::new(
628                std::env::current_dir().expect("test working directory must be available"),
629                "src/widget.h",
630            );
631            let tree = cpp_tree(source);
632            let root = tree.root_node();
633
634            let independent_primary = parse_cpp_file(&file, source, &tree);
635            let independent_c =
636                parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
637            let independent_primary_facts = published_facts(&independent_primary);
638            let independent_c_facts = published_facts(&independent_c);
639            let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
640            assert_eq!(
641                independent_differs,
642                *expected_differs,
643                "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:#?}",
644                root.has_error(),
645                root.to_sexp(),
646                independent_primary.declarations(),
647                independent_c.declarations(),
648            );
649
650            let ancestry = ParentIndex::new(root);
651            let (shared_primary, shared_c) =
652                parse_cpp_file_with_readings(&file, source, root, &ancestry);
653            let c_tag_scope_witness = shared_c.is_some();
654            if *name == "tag reparsed under a recovered export class" {
655                assert!(
656                    root.has_error(),
657                    "the recovery fixture must parse with errors"
658                );
659                let recovered_owner = independent_primary
660                    .declarations()
661                    .iter()
662                    .find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
663                    .expect("the malformed export-class head must recover its owner");
664                let owner_range = independent_primary
665                    .ranges
666                    .get(recovered_owner)
667                    .and_then(|ranges| ranges.first())
668                    .expect("the recovered owner must retain its class range");
669                // Since brokk-tree-sitter-cpp 0.24.7 a function-like export
670                // macro head parses, so the fixture uses an object-like macro
671                // before a base clause, which still fragments the class. Any
672                // recovery that owns it records its provenance.
673                assert!(
674                    owner_range.start_byte < owner_range.end_byte,
675                    "the recovered owner must retain a nonempty class range"
676                );
677                assert!(
678                    independent_primary.materialization_records.iter().any(|record| matches!(
679                        record,
680                        brokk_bifrost_core::analyzer::structural::materialization::MaterializationRecord::RecoveredDeclaration { unit, .. }
681                            if unit == recovered_owner
682                    )),
683                    "the fixture must enter export-class recovery: {:#?}",
684                    independent_primary.materialization_records
685                );
686                let mut pending = vec![root];
687                let mut tag = None;
688                while let Some(node) = pending.pop() {
689                    if matches!(
690                        node.kind(),
691                        "struct_specifier" | "union_specifier" | "enum_specifier"
692                    ) && node.child_by_field_name("name").is_some_and(|name| {
693                        name.utf8_text(source.as_bytes())
694                            .is_ok_and(|text| text == "RecoveredTag")
695                    }) {
696                        tag = Some(node);
697                        break;
698                    }
699                    let mut cursor = node.walk();
700                    pending.extend(node.named_children(&mut cursor));
701                }
702                let tag = tag.expect("the original recovered fixture exposes its tag node");
703                let mut ancestor = ancestry.parent(tag);
704                while let Some(node) = ancestor {
705                    assert!(
706                        !matches!(
707                            node.kind(),
708                            "class_specifier" | "struct_specifier" | "union_specifier"
709                        ),
710                        "the original CST must not already give RecoveredTag an aggregate owner"
711                    );
712                    ancestor = ancestry.parent(node);
713                }
714            }
715
716            assert_eq!(
717                c_tag_scope_witness,
718                *expected_witness,
719                "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:#?}",
720                root.has_error(),
721                root.to_sexp(),
722                independent_primary.declarations(),
723                independent_c.declarations(),
724            );
725            assert_eq!(
726                independent_primary_facts,
727                published_facts(&shared_primary),
728                "C++ reading of {name}"
729            );
730            if *expected_witness {
731                let shared_c = shared_c.expect("a witnessed C reading is emitted");
732                assert_eq!(
733                    independent_c_facts,
734                    published_facts(&shared_c),
735                    "C reading of {name}"
736                );
737            } else {
738                assert_eq!(
739                    independent_primary_facts, independent_c_facts,
740                    "a false witness must imply identical complete readings for {name}"
741                );
742                assert!(
743                    !independent_differs,
744                    "a false witness must make the existing publication comparator agree for {name}"
745                );
746            }
747        }
748    }
749    #[test]
750    fn the_nested_tag_fixture_really_has_two_readings() {
751        let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
752        let file = ProjectFile::new(
753            std::env::current_dir().expect("test working directory must be available"),
754            "src/widget.h",
755        );
756        let tree = cpp_tree(source);
757        let root = tree.root_node();
758        let ancestry = ParentIndex::new(root);
759        let (primary, c_reading) = parse_cpp_file_with_readings(&file, source, root, &ancestry);
760        let c_reading = c_reading.expect("nested tag requires a C reading");
761        assert!(
762            cpp_projections_differ(&primary, &c_reading),
763            "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
764            primary.declarations(),
765            c_reading.declarations()
766        );
767    }
768    #[test]
769    fn projection_difference_includes_resolution_facts() {
770        let source = "struct Widget {};\n";
771        let file = ProjectFile::new(
772            std::env::current_dir().expect("test working directory must be available"),
773            "src/widget.h",
774        );
775        let tree = cpp_tree(source);
776        let primary = parse_cpp_file(&file, source, &tree);
777        let mut fact_only_difference = primary.clone();
778        assert!(!cpp_projections_differ(&primary, &fact_only_difference));
779
780        fact_only_difference
781            .resolution_facts
782            .names
783            .push(ResolutionNameFact {
784                id: ResolutionNameId::new(0),
785                spelling: "fact-only-difference".to_owned(),
786            });
787        assert!(cpp_projections_differ(&primary, &fact_only_difference));
788    }
789    #[test]
790    fn includes_are_recorded_at_every_depth() {
791        let source = r#"
792#include <vector>
793
794class Widget {
795public:
796    int value() const;
797};
798
799int run() {
800    switch (0) {
801#include <sycl/info/aspects.def>
802    default:
803        return 0;
804    }
805}
806"#;
807        let file = ProjectFile::new(
808            std::env::current_dir().expect("test working directory must be available"),
809            "src/widget.cpp",
810        );
811        let mut parser = Parser::new();
812        parser
813            .set_language(&tree_sitter_cpp::LANGUAGE.into())
814            .expect("C++ grammar");
815        let tree = parser.parse(source, None).expect("C++ tree");
816
817        let parsed = parse_cpp_file(&file, source, &tree);
818        let includes = parsed
819            .imports
820            .iter()
821            .map(|import| import.raw_snippet.clone())
822            .collect::<Vec<_>>();
823
824        assert_eq!(
825            includes,
826            vec![
827                "#include <vector>".to_string(),
828                "#include <sycl/info/aspects.def>".to_string(),
829            ]
830        );
831        assert!(!parsed.declarations().is_empty());
832    }
833}