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. Milestone 3 of
85/// `.agents/plans/c-compilation-language-tag-scope.md` stores both readings of
86/// a header when they differ, so extraction has to be reachable under a
87/// dialect the path itself does not name.
88///
89/// This entry point owns the whole tree's work, including the parent index the
90/// walk asks its ancestor questions of.
91pub fn parse_cpp_file_in_dialect(
92    file: &ProjectFile,
93    source: &str,
94    tree: &Tree,
95    dialect: LanguageDialect,
96) -> ParsedFile {
97    let root = tree.root_node();
98    let ancestry = ParentIndex::new(root);
99    parse_cpp_readings(file, source, root, &ancestry, &[dialect])
100        .pop()
101        .expect("one dialect reading")
102}
103
104/// Extract the primary dialect from a caller-owned tree and parent index.
105pub fn parse_cpp_file_with_ancestry<'tree>(
106    file: &ProjectFile,
107    source: &str,
108    root: Node<'tree>,
109    ancestry: &ParentIndex<'tree>,
110) -> ParsedFile {
111    let dialect = LanguageDialect::for_path(Language::Cpp, file.rel_path());
112    parse_cpp_readings(file, source, root, ancestry, &[dialect])
113        .pop()
114        .expect("one dialect reading")
115}
116
117/// Extract a header's C++ and C readings together. Both declaration policies
118/// consume one primary event stream. Exact construction-ID finalization gives
119/// each dialect stable content-owned source and structural projections. The C
120/// reading is absent when the primary declaration walk proves identical tag scope.
121pub fn parse_cpp_file_with_readings<'tree>(
122    file: &ProjectFile,
123    source: &str,
124    root: Node<'tree>,
125    ancestry: &ParentIndex<'tree>,
126) -> (ParsedFile, Option<ParsedFile>) {
127    let mut readings = parse_cpp_readings(
128        file,
129        source,
130        root,
131        ancestry,
132        &[
133            LanguageDialect::for_path(Language::Cpp, file.rel_path()),
134            LanguageDialect::CppC,
135        ],
136    );
137    let c = (readings.len() == 2).then(|| readings.pop().expect("C reading"));
138    let primary = readings.pop().expect("primary reading");
139    (primary, c)
140}
141
142/// Whether two readings of one blob disagree about any identity-bearing
143/// output: which declarations exist, what they are named, which are top level
144/// or definition-lookup entries, where they start and end, what they are
145/// nested in, and how they are signed.
146///
147/// This is the "differs" test behind storing a header's C projection only when
148/// it says something the C++ projection does not (issue #1970): absence of the
149/// second row-set must unambiguously mean "identical", so anything a
150/// resolution surface can observe has to be compared here.
151pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
152    left.declarations() != right.declarations()
153        || left.top_level_declarations != right.top_level_declarations
154        || left.definition_lookup_units != right.definition_lookup_units
155        || left.children != right.children
156        || left.ranges != right.ranges
157        || left.signatures != right.signatures
158        || left.type_aliases != right.type_aliases
159        || left.resolution_facts != right.resolution_facts
160}
161
162pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
163    let trimmed = reference.trim();
164    let before_args = trimmed
165        .split_once('(')
166        .map(|(head, _)| head)
167        .unwrap_or(trimmed);
168    before_args
169        .rsplit_once("::")
170        .or_else(|| before_args.rsplit_once('.'))
171        .map(|(receiver, _)| receiver.to_string())
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use brokk_bifrost_core::analyzer::resolution_facts::{ResolutionNameFact, ResolutionNameId};
178    use tree_sitter::Parser;
179
180    fn cpp_tree(source: &str) -> tree_sitter::Tree {
181        let mut parser = Parser::new();
182        parser
183            .set_language(&tree_sitter_cpp::LANGUAGE.into())
184            .expect("C++ grammar");
185        parser.parse(source, None).expect("C++ tree")
186    }
187
188    /// Everything one reading publishes, rendered so that two readings can be
189    /// compared without depending on hash iteration order.
190    fn published_facts(parsed: &ParsedFile) -> Vec<String> {
191        let mut facts = vec![
192            format!("package={}", parsed.package_name),
193            format!("content_qualifier={}", parsed.content_qualifier),
194            format!("top_level={:?}", parsed.top_level_declarations),
195            format!("imports={:?}", parsed.imports),
196            format!("materializations={:?}", parsed.materialization_records),
197            format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
198        ];
199        let mut unordered = |label: &str, mut entries: Vec<String>| {
200            entries.sort();
201            facts.push(format!("{label}={entries:?}"));
202        };
203        unordered(
204            "declarations",
205            parsed.declarations().iter().map(debug_of).collect(),
206        );
207        unordered(
208            "definition_lookup",
209            parsed
210                .definition_lookup_units
211                .iter()
212                .map(debug_of)
213                .collect(),
214        );
215        unordered(
216            "type_identifiers",
217            parsed.type_identifiers.iter().map(debug_of).collect(),
218        );
219        unordered(
220            "type_aliases",
221            parsed.type_aliases.iter().map(debug_of).collect(),
222        );
223        unordered(
224            "scala_traits",
225            parsed.scala_traits.iter().map(debug_of).collect(),
226        );
227        unordered(
228            "test_region_units",
229            parsed.test_region_units.iter().map(debug_of).collect(),
230        );
231        unordered(
232            "navigation_truncated",
233            parsed
234                .navigation_ranges_truncated
235                .iter()
236                .map(debug_of)
237                .collect(),
238        );
239        unordered("children", pairs(&parsed.children));
240        unordered("ranges", pairs(&parsed.ranges));
241        unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
242        unordered("signatures", pairs(&parsed.signatures));
243        unordered("signature_metadata", pairs(&parsed.signature_metadata));
244        unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
245        unordered(
246            "supertype_lookup_paths",
247            pairs(&parsed.supertype_lookup_paths),
248        );
249        unordered("scala_exports", pairs(&parsed.scala_exports));
250        unordered(
251            "cpp_template_metadata",
252            pairs(&parsed.cpp_template_metadata),
253        );
254        unordered(
255            "ruby_method_dispatch_modes",
256            pairs(&parsed.ruby_method_dispatch_modes),
257        );
258        facts
259    }
260
261    fn debug_of<T: std::fmt::Debug>(value: T) -> String {
262        format!("{value:?}")
263    }
264
265    fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
266        map: &brokk_bifrost_core::hash::HashMap<K, V>,
267    ) -> Vec<String> {
268        map.iter().map(|entry| format!("{entry:?}")).collect()
269    }
270
271    /// The two readings of one header, taken the way production takes them,
272    /// publish exactly what two independent extractions publish.
273    ///
274    /// Milestone 3b of `.agents/plans/immutable-revision-persisted-fact-reuse.md`
275    /// stopped the C reading from rebuilding the parent index, re-sweeping
276    /// includes and identifiers, and re-running the quoted-include line scan
277    /// that the C++ reading of the same tree had already produced. That is a
278    /// deduplication and nothing else: if any published fact moved, something
279    /// believed dialect-insensitive is not.
280    ///
281    /// The error-recovery shapes are the interesting half. Their walks reparse
282    /// byte regions into trees of their own and re-own sibling nodes under
283    /// recovered class scopes, so they are where a shared index would show up
284    /// if sharing were unsound.
285    #[test]
286    fn a_shared_reading_publishes_what_an_independent_one_publishes() {
287        let fixtures: &[(&str, &str, bool, bool)] = &[
288            (
289                "nested tag inside an aggregate, plus a nested include",
290                r#"
291#include <vector>
292struct outer {
293#include "member_list.def"
294    struct inner { int v; } i;
295};
296struct inner *p;
297                "#,
298                true,
299                true,
300            ),
301            (
302                "a quoted include only the line scan can recover",
303                r#"
304#include "visible.h"
305class Broken {
306    void method(
307#include "hidden.h"
308                "#,
309                false,
310                false,
311            ),
312            (
313                "forward declarations replaced by their definitions",
314                r#"
315typedef unsigned long long u64;
316namespace generated {
317struct tag0;
318struct tag1;
319struct tag1 {
320    struct nested { int v; } n;
321    u64 first;
322};
323struct tag0 { int second; };
324}
325                "#,
326                true,
327                true,
328            ),
329            (
330                "a fragmented export-macro class body",
331                r#"
332#define SIMPLECPP_LIB
333namespace simplecpp {
334using TokenString = std::string;
335struct Location { int line{}; };
336class SIMPLECPP_LIB Token {
337  TokenString prefix;
338  void prefix_method() {}
339 public:
340  Token(const TokenString &s, const Location &loc, bool wsahead = false) :
341      whitespaceahead(wsahead), location(loc), string(s)
342      {
343      flags();
344  }
345  struct Nested { int v; } nested;
346  TokenString string;
347  bool whitespaceahead;
348  Location location;
349 private:
350  void flags() {
351      whitespaceahead = true;
352  }
353};
354}
355                "#,
356                true,
357                true,
358            ),
359            (
360                "nested struct, union, and enum tags",
361                r#"
362struct StructOwner { struct StructTag { int value; }; };
363struct UnionOwner { union UnionTag { int value; }; };
364struct EnumOwner { enum EnumTag { value }; };
365"#,
366                true,
367                true,
368            ),
369            (
370                "C++ class and anonymous tag near misses",
371                r#"
372struct Owner {
373    class NestedClass { int value; };
374    struct { int anonymous_value; } anonymous_member;
375};
376struct TopLevel { int value; };
377"#,
378                false,
379                false,
380            ),
381            (
382                "tag reparsed under a recovered export class",
383                r#"
384namespace api {
385
386/**
387* Doc comment
388*/
389class PROJECT_PUBLIC_API(2, 0) RecoveryOwner : public virtual BaseKey {
390   public:
391      /** Construct from a point. */
392      RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
393
394#if defined(PROJECT_HAS_LEGACY_POINT)
395      /** Construct from a legacy point. */
396      RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
397#endif
398
399      struct RecoveredTag { int value; };
400      std::string algo_name() const override;
401      AlgorithmIdentifier algorithm_identifier() const override;
402};
403}
404"#,
405                true,
406                true,
407            ),
408        ];
409
410        for (name, source, expected_witness, expected_differs) in fixtures {
411            let file = ProjectFile::new(
412                std::env::current_dir().expect("test working directory must be available"),
413                "src/widget.h",
414            );
415            let tree = cpp_tree(source);
416            let root = tree.root_node();
417
418            let independent_primary = parse_cpp_file(&file, source, &tree);
419            let independent_c =
420                parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
421            let independent_primary_facts = published_facts(&independent_primary);
422            let independent_c_facts = published_facts(&independent_c);
423            let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
424            assert_eq!(
425                independent_differs,
426                *expected_differs,
427                "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:#?}",
428                root.has_error(),
429                root.to_sexp(),
430                independent_primary.declarations(),
431                independent_c.declarations(),
432            );
433
434            let ancestry = ParentIndex::new(root);
435            let (shared_primary, shared_c) =
436                parse_cpp_file_with_readings(&file, source, root, &ancestry);
437            let c_tag_scope_witness = shared_c.is_some();
438            if *name == "tag reparsed under a recovered export class" {
439                assert!(
440                    root.has_error(),
441                    "the recovery fixture must parse with errors"
442                );
443                let recovered_owner = independent_primary
444                    .declarations()
445                    .iter()
446                    .find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
447                    .expect("the malformed export-class head must recover its owner");
448                let owner_range = independent_primary
449                    .ranges
450                    .get(recovered_owner)
451                    .and_then(|ranges| ranges.first())
452                    .expect("the recovered owner must retain its class range");
453                let mut pending = vec![root];
454                let mut recognized_recovery = false;
455                while let Some(node) = pending.pop() {
456                    if crate::declarations::recovered_fragmented_class_has_body(
457                        node,
458                        source,
459                        "RecoveryOwner",
460                        owner_range,
461                    ) {
462                        recognized_recovery = true;
463                        break;
464                    }
465                    let mut cursor = node.walk();
466                    pending.extend(node.named_children(&mut cursor));
467                }
468                assert!(
469                    recognized_recovery,
470                    "the fixture must enter the fragmented export-class recovery path"
471                );
472                let mut pending = vec![root];
473                let mut tag = None;
474                while let Some(node) = pending.pop() {
475                    if matches!(
476                        node.kind(),
477                        "struct_specifier" | "union_specifier" | "enum_specifier"
478                    ) && node.child_by_field_name("name").is_some_and(|name| {
479                        name.utf8_text(source.as_bytes())
480                            .is_ok_and(|text| text == "RecoveredTag")
481                    }) {
482                        tag = Some(node);
483                        break;
484                    }
485                    let mut cursor = node.walk();
486                    pending.extend(node.named_children(&mut cursor));
487                }
488                let tag = tag.expect("the original recovered fixture exposes its tag node");
489                let mut ancestor = ancestry.parent(tag);
490                while let Some(node) = ancestor {
491                    assert!(
492                        !matches!(
493                            node.kind(),
494                            "class_specifier" | "struct_specifier" | "union_specifier"
495                        ),
496                        "the original CST must not already give RecoveredTag an aggregate owner"
497                    );
498                    ancestor = ancestry.parent(node);
499                }
500            }
501
502            assert_eq!(
503                c_tag_scope_witness,
504                *expected_witness,
505                "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:#?}",
506                root.has_error(),
507                root.to_sexp(),
508                independent_primary.declarations(),
509                independent_c.declarations(),
510            );
511            assert_eq!(
512                independent_primary_facts,
513                published_facts(&shared_primary),
514                "C++ reading of {name}"
515            );
516            if *expected_witness {
517                let shared_c = shared_c.expect("a witnessed C reading is emitted");
518                assert_eq!(
519                    independent_c_facts,
520                    published_facts(&shared_c),
521                    "C reading of {name}"
522                );
523            } else {
524                assert_eq!(
525                    independent_primary_facts, independent_c_facts,
526                    "a false witness must imply identical complete readings for {name}"
527                );
528                assert!(
529                    !independent_differs,
530                    "a false witness must make the existing publication comparator agree for {name}"
531                );
532            }
533        }
534    }
535
536    /// A tag nested in an aggregate is the whole reason the second reading
537    /// exists, so the fixture above must actually produce two different
538    /// readings; otherwise the comparison would pass on two empty answers.
539    #[test]
540    fn the_nested_tag_fixture_really_has_two_readings() {
541        let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
542        let file = ProjectFile::new(
543            std::env::current_dir().expect("test working directory must be available"),
544            "src/widget.h",
545        );
546        let tree = cpp_tree(source);
547        let root = tree.root_node();
548        let ancestry = ParentIndex::new(root);
549        let (primary, c_reading) = parse_cpp_file_with_readings(&file, source, root, &ancestry);
550        let c_reading = c_reading.expect("nested tag requires a C reading");
551        assert!(
552            cpp_projections_differ(&primary, &c_reading),
553            "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
554            primary.declarations(),
555            c_reading.declarations()
556        );
557    }
558
559    #[test]
560    fn projection_difference_includes_resolution_facts() {
561        let source = "struct Widget {};\n";
562        let file = ProjectFile::new(
563            std::env::current_dir().expect("test working directory must be available"),
564            "src/widget.h",
565        );
566        let tree = cpp_tree(source);
567        let primary = parse_cpp_file(&file, source, &tree);
568        let mut fact_only_difference = primary.clone();
569        assert!(!cpp_projections_differ(&primary, &fact_only_difference));
570
571        fact_only_difference
572            .resolution_facts
573            .names
574            .push(ResolutionNameFact {
575                id: ResolutionNameId::new(0),
576                spelling: "fact-only-difference".to_owned(),
577            });
578        assert!(cpp_projections_differ(&primary, &fact_only_difference));
579    }
580
581    /// Every `#include` is an include claim, wherever it is written. The
582    /// declaration walk descends only through declaration scopes, so a
583    /// directive inside a function body (llama.cpp's `sycl/info/aspects.def`
584    /// inside a `switch`) or inside a class body (Eigen's
585    /// `EIGEN_DENSEBASE_PLUGIN` in `DenseBase.h`) used to be invisible.
586    ///
587    /// The nested directive here is not a quoted include, so the established
588    /// `recover_quoted_includes` line scan cannot supply it: only the preorder
589    /// sweep over the tree can.
590    #[test]
591    fn includes_are_recorded_at_every_depth() {
592        let source = r#"
593#include <vector>
594
595class Widget {
596public:
597    int value() const;
598};
599
600int run() {
601    switch (0) {
602#include <sycl/info/aspects.def>
603    default:
604        return 0;
605    }
606}
607"#;
608        let file = ProjectFile::new(
609            std::env::current_dir().expect("test working directory must be available"),
610            "src/widget.cpp",
611        );
612        let mut parser = Parser::new();
613        parser
614            .set_language(&tree_sitter_cpp::LANGUAGE.into())
615            .expect("C++ grammar");
616        let tree = parser.parse(source, None).expect("C++ tree");
617
618        let parsed = parse_cpp_file(&file, source, &tree);
619        let includes = parsed
620            .imports
621            .iter()
622            .map(|import| import.raw_snippet.clone())
623            .collect::<Vec<_>>();
624
625        assert_eq!(
626            includes,
627            vec![
628                "#include <vector>".to_string(),
629                "#include <sycl/info/aspects.def>".to_string(),
630            ]
631        );
632        assert!(!parsed.declarations().is_empty());
633    }
634}