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/// 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/// Extract a source after seeding its object-like field-list environment from
59/// include-visible declarations. The seed is consumed structurally by the
60/// ordinary declaration walk; local definitions and undef directives retain
61/// their source-order semantics.
62pub fn parse_cpp_file_with_object_macro_fields(
63    file: &ProjectFile,
64    source: &str,
65    tree: &Tree,
66    object_macro_fields: HashMap<String, ObjectMacroReplacement>,
67) -> ParsedFile {
68    let root = tree.root_node();
69    let ancestry = ParentIndex::new(root);
70    parse_cpp_reading_with_object_macro_fields(
71        file,
72        source,
73        root,
74        LanguageDialect::for_path(Language::Cpp, file.rel_path()),
75        &ancestry,
76        object_macro_fields,
77    )
78}
79
80/// Extract `file` under an explicitly named dialect.
81///
82/// A header carries no compilation language of its own, so its blob has two
83/// legitimate readings: under [`LanguageDialect::CppC`] a tag declared inside
84/// an aggregate member list has file scope (C17 6.2.1), under the plain C++
85/// dialect it is a nested class. Milestone 3 of
86/// `.agents/plans/c-compilation-language-tag-scope.md` stores both readings of
87/// a header when they differ, so extraction has to be reachable under a
88/// dialect the path itself does not name.
89///
90/// This entry point owns the whole tree's work, including the parent index the
91/// walk asks its ancestor questions of. A caller that wants both readings of
92/// one tree must use [`parse_cpp_file_with_ancestry`] and
93/// [`parse_cpp_c_reading`] instead, which share that index.
94pub fn parse_cpp_file_in_dialect(
95    file: &ProjectFile,
96    source: &str,
97    tree: &Tree,
98    dialect: LanguageDialect,
99) -> ParsedFile {
100    let root = tree.root_node();
101    let ancestry = ParentIndex::new(root);
102    parse_cpp_reading(file, source, root, dialect, &ancestry)
103}
104
105/// Extract `file` under the dialect its own path selects, asking its ancestor
106/// questions of a caller-owned index over the same tree.
107///
108/// The index costs one hash entry per node and is a property of the tree, not
109/// of the reading, so a header that gets both readings builds it once. See
110/// [`parse_cpp_c_reading`] for the second reading.
111pub fn parse_cpp_file_with_ancestry<'tree>(
112    file: &ProjectFile,
113    source: &str,
114    root: Node<'tree>,
115    ancestry: &ParentIndex<'tree>,
116) -> ParsedFile {
117    parse_cpp_reading(
118        file,
119        source,
120        root,
121        LanguageDialect::for_path(Language::Cpp, file.rel_path()),
122        ancestry,
123    )
124}
125
126/// The C reading of a tree whose other reading is already in hand.
127///
128/// The dialect decides exactly one thing: where a struct/union/enum tag
129/// declared inside another aggregate's member list is declared (the
130/// `c_tag_semantics` readers in [`crate::declarations`]). Everything else this
131/// file's extraction produces is a property of the tree and of the source text
132/// -- the parent index, the `#include` sweep, the identifier sweep, and the
133/// quoted-include line recovery -- so the C reading takes those from `primary`
134/// rather than recomputing them over the same bytes. A debug build re-runs the
135/// sweeps and asserts they agree, which is what holds the claim honest if a
136/// future walk ever starts contributing to one of those families.
137pub fn parse_cpp_c_reading<'tree>(
138    file: &ProjectFile,
139    source: &str,
140    root: Node<'tree>,
141    ancestry: &ParentIndex<'tree>,
142    primary: &ParsedFile,
143) -> ParsedFile {
144    let mut parsed = ParsedFile::new(String::new());
145    parsed.imports = primary.imports.clone();
146    parsed.type_identifiers = primary.type_identifiers.clone();
147    walk_cpp_declarations(
148        file,
149        source,
150        root,
151        LanguageDialect::CppC,
152        ancestry,
153        &mut parsed,
154        HashMap::default(),
155    );
156    parsed.finalize_deferred_replacements();
157
158    #[cfg(debug_assertions)]
159    {
160        let mut recomputed = ParsedFile::new(String::new());
161        collect_cpp_includes(root, source, &mut recomputed);
162        collect_cpp_identifiers(root, source, &mut recomputed.type_identifiers);
163        recover_quoted_includes(source, &mut recomputed);
164        assert_eq!(
165            parsed.imports, recomputed.imports,
166            "the C reading's includes are the C++ reading's includes: {:?}",
167            file
168        );
169        assert_eq!(
170            parsed.type_identifiers, recomputed.type_identifiers,
171            "the C reading's identifiers are the C++ reading's identifiers: {:?}",
172            file
173        );
174    }
175
176    parsed
177}
178
179/// One complete reading of `root`, sweeps included.
180fn parse_cpp_reading<'tree>(
181    file: &ProjectFile,
182    source: &str,
183    root: Node<'tree>,
184    dialect: LanguageDialect,
185    ancestry: &ParentIndex<'tree>,
186) -> ParsedFile {
187    parse_cpp_reading_with_object_macro_fields(
188        file,
189        source,
190        root,
191        dialect,
192        ancestry,
193        HashMap::default(),
194    )
195}
196
197fn parse_cpp_reading_with_object_macro_fields<'tree>(
198    file: &ProjectFile,
199    source: &str,
200    root: Node<'tree>,
201    dialect: LanguageDialect,
202    ancestry: &ParentIndex<'tree>,
203    object_macro_fields: HashMap<String, ObjectMacroReplacement>,
204) -> ParsedFile {
205    let mut parsed = ParsedFile::new(String::new());
206
207    collect_cpp_includes(root, source, &mut parsed);
208    collect_cpp_identifiers(root, source, &mut parsed.type_identifiers);
209
210    walk_cpp_declarations(
211        file,
212        source,
213        root,
214        dialect,
215        ancestry,
216        &mut parsed,
217        object_macro_fields,
218    );
219    // A line scan over the source rather than a tree walk: it recovers the
220    // quoted directives a parse error hid from the tree, skipping any snippet
221    // the sweep above already recorded.
222    recover_quoted_includes(source, &mut parsed);
223    parsed.finalize_deferred_replacements();
224    parsed
225}
226
227/// The declaration walk itself: the only part of an extraction the dialect
228/// changes. The caller finalizes, because the primary reading recovers its
229/// quoted includes between the walk and that compaction.
230fn walk_cpp_declarations<'tree>(
231    file: &ProjectFile,
232    source: &str,
233    root: Node<'tree>,
234    dialect: LanguageDialect,
235    ancestry: &ParentIndex<'tree>,
236    parsed: &mut ParsedFile,
237    object_macro_fields: HashMap<String, ObjectMacroReplacement>,
238) {
239    let mut visitor = CppVisitor {
240        file,
241        source,
242        parsed,
243        c_tag_semantics: dialect == LanguageDialect::CppC,
244        recovered_class_sibling_scopes: HashMap::default(),
245        consumed_fragment_regions: Vec::new(),
246        orphaned_namespaces: OrphanedNamespaceScopeIndex::build(root, source),
247        partitioned_regions: Vec::new(),
248        namespace_forward_scans: HashMap::default(),
249        field_owners: None,
250        recovery_captures: Vec::new(),
251        object_macro_fields,
252        ambiguous_object_macro_fields: HashSet::default(),
253    };
254    visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
255}
256
257/// Whether two readings of one blob disagree about any identity-bearing
258/// output: which declarations exist, what they are named, which are top level
259/// or definition-lookup entries, where they start and end, what they are
260/// nested in, and how they are signed.
261///
262/// This is the "differs" test behind storing a header's C projection only when
263/// it says something the C++ projection does not (issue #1970): absence of the
264/// second row-set must unambiguously mean "identical", so anything a
265/// resolution surface can observe has to be compared here.
266pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
267    left.declarations() != right.declarations()
268        || left.top_level_declarations != right.top_level_declarations
269        || left.definition_lookup_units != right.definition_lookup_units
270        || left.children != right.children
271        || left.ranges != right.ranges
272        || left.signatures != right.signatures
273        || left.type_aliases != right.type_aliases
274}
275
276pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
277    let trimmed = reference.trim();
278    let before_args = trimmed
279        .split_once('(')
280        .map(|(head, _)| head)
281        .unwrap_or(trimmed);
282    before_args
283        .rsplit_once("::")
284        .or_else(|| before_args.rsplit_once('.'))
285        .map(|(receiver, _)| receiver.to_string())
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use tree_sitter::Parser;
292
293    fn cpp_tree(source: &str) -> tree_sitter::Tree {
294        let mut parser = Parser::new();
295        parser
296            .set_language(&tree_sitter_cpp::LANGUAGE.into())
297            .expect("C++ grammar");
298        parser.parse(source, None).expect("C++ tree")
299    }
300
301    /// Everything one reading publishes, rendered so that two readings can be
302    /// compared without depending on hash iteration order.
303    fn published_facts(parsed: &ParsedFile) -> Vec<String> {
304        let mut facts = vec![
305            format!("package={}", parsed.package_name),
306            format!("content_qualifier={}", parsed.content_qualifier),
307            format!("top_level={:?}", parsed.top_level_declarations),
308            format!("imports={:?}", parsed.imports),
309            format!("materializations={:?}", parsed.materialization_records),
310            format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
311        ];
312        let mut unordered = |label: &str, mut entries: Vec<String>| {
313            entries.sort();
314            facts.push(format!("{label}={entries:?}"));
315        };
316        unordered(
317            "declarations",
318            parsed.declarations().iter().map(debug_of).collect(),
319        );
320        unordered(
321            "definition_lookup",
322            parsed
323                .definition_lookup_units
324                .iter()
325                .map(debug_of)
326                .collect(),
327        );
328        unordered(
329            "type_identifiers",
330            parsed.type_identifiers.iter().map(debug_of).collect(),
331        );
332        unordered(
333            "type_aliases",
334            parsed.type_aliases.iter().map(debug_of).collect(),
335        );
336        unordered(
337            "scala_traits",
338            parsed.scala_traits.iter().map(debug_of).collect(),
339        );
340        unordered(
341            "test_region_units",
342            parsed.test_region_units.iter().map(debug_of).collect(),
343        );
344        unordered(
345            "navigation_truncated",
346            parsed
347                .navigation_ranges_truncated
348                .iter()
349                .map(debug_of)
350                .collect(),
351        );
352        unordered("children", pairs(&parsed.children));
353        unordered("ranges", pairs(&parsed.ranges));
354        unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
355        unordered("signatures", pairs(&parsed.signatures));
356        unordered("signature_metadata", pairs(&parsed.signature_metadata));
357        unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
358        unordered(
359            "supertype_lookup_paths",
360            pairs(&parsed.supertype_lookup_paths),
361        );
362        unordered("scala_exports", pairs(&parsed.scala_exports));
363        unordered(
364            "cpp_template_metadata",
365            pairs(&parsed.cpp_template_metadata),
366        );
367        unordered(
368            "ruby_method_dispatch_modes",
369            pairs(&parsed.ruby_method_dispatch_modes),
370        );
371        facts
372    }
373
374    fn debug_of<T: std::fmt::Debug>(value: T) -> String {
375        format!("{value:?}")
376    }
377
378    fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
379        map: &brokk_bifrost_core::hash::HashMap<K, V>,
380    ) -> Vec<String> {
381        map.iter().map(|entry| format!("{entry:?}")).collect()
382    }
383
384    /// The two readings of one header, taken the way production takes them,
385    /// publish exactly what two independent extractions publish.
386    ///
387    /// Milestone 3b of `.agents/plans/immutable-revision-persisted-fact-reuse.md`
388    /// stopped the C reading from rebuilding the parent index, re-sweeping
389    /// includes and identifiers, and re-running the quoted-include line scan
390    /// that the C++ reading of the same tree had already produced. That is a
391    /// deduplication and nothing else: if any published fact moved, something
392    /// believed dialect-insensitive is not.
393    ///
394    /// The error-recovery shapes are the interesting half. Their walks reparse
395    /// byte regions into trees of their own and re-own sibling nodes under
396    /// recovered class scopes, so they are where a shared index would show up
397    /// if sharing were unsound.
398    #[test]
399    fn a_shared_reading_publishes_what_an_independent_one_publishes() {
400        let fixtures: &[(&str, &str)] = &[
401            (
402                "nested tag inside an aggregate, plus a nested include",
403                r#"
404#include <vector>
405struct outer {
406#include "member_list.def"
407    struct inner { int v; } i;
408};
409struct inner *p;
410"#,
411            ),
412            (
413                "a quoted include only the line scan can recover",
414                r#"
415#include "visible.h"
416class Broken {
417    void method(
418#include "hidden.h"
419"#,
420            ),
421            (
422                "forward declarations replaced by their definitions",
423                r#"
424typedef unsigned long long u64;
425namespace generated {
426struct tag0;
427struct tag1;
428struct tag1 {
429    struct nested { int v; } n;
430    u64 first;
431};
432struct tag0 { int second; };
433}
434"#,
435            ),
436            (
437                "a fragmented export-macro class body",
438                r#"
439#define SIMPLECPP_LIB
440namespace simplecpp {
441using TokenString = std::string;
442struct Location { int line{}; };
443class SIMPLECPP_LIB Token {
444  TokenString prefix;
445  void prefix_method() {}
446 public:
447  Token(const TokenString &s, const Location &loc, bool wsahead = false) :
448      whitespaceahead(wsahead), location(loc), string(s)
449      {
450      flags();
451  }
452  struct Nested { int v; } nested;
453  TokenString string;
454  bool whitespaceahead;
455  Location location;
456 private:
457  void flags() {
458      whitespaceahead = true;
459  }
460};
461}
462"#,
463            ),
464        ];
465
466        for (name, source) in fixtures {
467            let file = ProjectFile::new(
468                std::env::current_dir().expect("test working directory must be available"),
469                "src/widget.h",
470            );
471            let tree = cpp_tree(source);
472            let root = tree.root_node();
473
474            let independent_primary = parse_cpp_file(&file, source, &tree);
475            let independent_c =
476                parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
477
478            let ancestry = ParentIndex::new(root);
479            let shared_primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
480            let shared_c = parse_cpp_c_reading(&file, source, root, &ancestry, &shared_primary);
481
482            assert_eq!(
483                published_facts(&independent_primary),
484                published_facts(&shared_primary),
485                "C++ reading of {name}"
486            );
487            assert_eq!(
488                published_facts(&independent_c),
489                published_facts(&shared_c),
490                "C reading of {name}"
491            );
492        }
493    }
494
495    /// A tag nested in an aggregate is the whole reason the second reading
496    /// exists, so the fixture above must actually produce two different
497    /// readings; otherwise the comparison would pass on two empty answers.
498    #[test]
499    fn the_nested_tag_fixture_really_has_two_readings() {
500        let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
501        let file = ProjectFile::new(
502            std::env::current_dir().expect("test working directory must be available"),
503            "src/widget.h",
504        );
505        let tree = cpp_tree(source);
506        let root = tree.root_node();
507        let ancestry = ParentIndex::new(root);
508        let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
509        let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
510        assert!(
511            cpp_projections_differ(&primary, &c_reading),
512            "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
513            primary.declarations(),
514            c_reading.declarations()
515        );
516    }
517
518    /// Every `#include` is an include claim, wherever it is written. The
519    /// declaration walk descends only through declaration scopes, so a
520    /// directive inside a function body (llama.cpp's `sycl/info/aspects.def`
521    /// inside a `switch`) or inside a class body (Eigen's
522    /// `EIGEN_DENSEBASE_PLUGIN` in `DenseBase.h`) used to be invisible.
523    ///
524    /// The nested directive here is not a quoted include, so the established
525    /// `recover_quoted_includes` line scan cannot supply it: only the preorder
526    /// sweep over the tree can.
527    #[test]
528    fn includes_are_recorded_at_every_depth() {
529        let source = r#"
530#include <vector>
531
532class Widget {
533public:
534    int value() const;
535};
536
537int run() {
538    switch (0) {
539#include <sycl/info/aspects.def>
540    default:
541        return 0;
542    }
543}
544"#;
545        let file = ProjectFile::new(
546            std::env::current_dir().expect("test working directory must be available"),
547            "src/widget.cpp",
548        );
549        let mut parser = Parser::new();
550        parser
551            .set_language(&tree_sitter_cpp::LANGUAGE.into())
552            .expect("C++ grammar");
553        let tree = parser.parse(source, None).expect("C++ tree");
554
555        let parsed = parse_cpp_file(&file, source, &tree);
556        let includes = parsed
557            .imports
558            .iter()
559            .map(|import| import.raw_snippet.clone())
560            .collect::<Vec<_>>();
561
562        assert_eq!(
563            includes,
564            vec![
565                "#include <vector>".to_string(),
566                "#include <sycl/info/aspects.def>".to_string(),
567            ]
568        );
569        assert!(!parsed.declarations().is_empty());
570    }
571}