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