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::MacroReplacementField;
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, Vec<MacroReplacementField>>,
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, Vec<MacroReplacementField>>,
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, Vec<MacroReplacementField>>,
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        namespace_forward_scans: HashMap::default(),
248        field_owners: None,
249        recovery_captures: Vec::new(),
250        object_macro_fields,
251        ambiguous_object_macro_fields: HashSet::default(),
252    };
253    visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
254}
255
256/// Whether two readings of one blob disagree about any identity-bearing
257/// output: which declarations exist, what they are named, which are top level
258/// or definition-lookup entries, where they start and end, what they are
259/// nested in, and how they are signed.
260///
261/// This is the "differs" test behind storing a header's C projection only when
262/// it says something the C++ projection does not (issue #1970): absence of the
263/// second row-set must unambiguously mean "identical", so anything a
264/// resolution surface can observe has to be compared here.
265pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
266    left.declarations() != right.declarations()
267        || left.top_level_declarations != right.top_level_declarations
268        || left.definition_lookup_units != right.definition_lookup_units
269        || left.children != right.children
270        || left.ranges != right.ranges
271        || left.signatures != right.signatures
272        || left.type_aliases != right.type_aliases
273}
274
275pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
276    let trimmed = reference.trim();
277    let before_args = trimmed
278        .split_once('(')
279        .map(|(head, _)| head)
280        .unwrap_or(trimmed);
281    before_args
282        .rsplit_once("::")
283        .or_else(|| before_args.rsplit_once('.'))
284        .map(|(receiver, _)| receiver.to_string())
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use tree_sitter::Parser;
291
292    fn cpp_tree(source: &str) -> tree_sitter::Tree {
293        let mut parser = Parser::new();
294        parser
295            .set_language(&tree_sitter_cpp::LANGUAGE.into())
296            .expect("C++ grammar");
297        parser.parse(source, None).expect("C++ tree")
298    }
299
300    /// Everything one reading publishes, rendered so that two readings can be
301    /// compared without depending on hash iteration order.
302    fn published_facts(parsed: &ParsedFile) -> Vec<String> {
303        let mut facts = vec![
304            format!("package={}", parsed.package_name),
305            format!("content_qualifier={}", parsed.content_qualifier),
306            format!("top_level={:?}", parsed.top_level_declarations),
307            format!("imports={:?}", parsed.imports),
308            format!("materializations={:?}", parsed.materialization_records),
309            format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
310        ];
311        let mut unordered = |label: &str, mut entries: Vec<String>| {
312            entries.sort();
313            facts.push(format!("{label}={entries:?}"));
314        };
315        unordered(
316            "declarations",
317            parsed.declarations().iter().map(debug_of).collect(),
318        );
319        unordered(
320            "definition_lookup",
321            parsed
322                .definition_lookup_units
323                .iter()
324                .map(debug_of)
325                .collect(),
326        );
327        unordered(
328            "type_identifiers",
329            parsed.type_identifiers.iter().map(debug_of).collect(),
330        );
331        unordered(
332            "type_aliases",
333            parsed.type_aliases.iter().map(debug_of).collect(),
334        );
335        unordered(
336            "scala_traits",
337            parsed.scala_traits.iter().map(debug_of).collect(),
338        );
339        unordered(
340            "test_region_units",
341            parsed.test_region_units.iter().map(debug_of).collect(),
342        );
343        unordered(
344            "navigation_truncated",
345            parsed
346                .navigation_ranges_truncated
347                .iter()
348                .map(debug_of)
349                .collect(),
350        );
351        unordered("children", pairs(&parsed.children));
352        unordered("ranges", pairs(&parsed.ranges));
353        unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
354        unordered("signatures", pairs(&parsed.signatures));
355        unordered("signature_metadata", pairs(&parsed.signature_metadata));
356        unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
357        unordered(
358            "supertype_lookup_paths",
359            pairs(&parsed.supertype_lookup_paths),
360        );
361        unordered("scala_exports", pairs(&parsed.scala_exports));
362        unordered(
363            "cpp_template_metadata",
364            pairs(&parsed.cpp_template_metadata),
365        );
366        unordered(
367            "ruby_method_dispatch_modes",
368            pairs(&parsed.ruby_method_dispatch_modes),
369        );
370        facts
371    }
372
373    fn debug_of<T: std::fmt::Debug>(value: T) -> String {
374        format!("{value:?}")
375    }
376
377    fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
378        map: &brokk_bifrost_core::hash::HashMap<K, V>,
379    ) -> Vec<String> {
380        map.iter().map(|entry| format!("{entry:?}")).collect()
381    }
382
383    /// The two readings of one header, taken the way production takes them,
384    /// publish exactly what two independent extractions publish.
385    ///
386    /// Milestone 3b of `.agents/plans/immutable-revision-persisted-fact-reuse.md`
387    /// stopped the C reading from rebuilding the parent index, re-sweeping
388    /// includes and identifiers, and re-running the quoted-include line scan
389    /// that the C++ reading of the same tree had already produced. That is a
390    /// deduplication and nothing else: if any published fact moved, something
391    /// believed dialect-insensitive is not.
392    ///
393    /// The error-recovery shapes are the interesting half. Their walks reparse
394    /// byte regions into trees of their own and re-own sibling nodes under
395    /// recovered class scopes, so they are where a shared index would show up
396    /// if sharing were unsound.
397    #[test]
398    fn a_shared_reading_publishes_what_an_independent_one_publishes() {
399        let fixtures: &[(&str, &str)] = &[
400            (
401                "nested tag inside an aggregate, plus a nested include",
402                r#"
403#include <vector>
404struct outer {
405#include "member_list.def"
406    struct inner { int v; } i;
407};
408struct inner *p;
409"#,
410            ),
411            (
412                "a quoted include only the line scan can recover",
413                r#"
414#include "visible.h"
415class Broken {
416    void method(
417#include "hidden.h"
418"#,
419            ),
420            (
421                "forward declarations replaced by their definitions",
422                r#"
423typedef unsigned long long u64;
424namespace generated {
425struct tag0;
426struct tag1;
427struct tag1 {
428    struct nested { int v; } n;
429    u64 first;
430};
431struct tag0 { int second; };
432}
433"#,
434            ),
435            (
436                "a fragmented export-macro class body",
437                r#"
438#define SIMPLECPP_LIB
439namespace simplecpp {
440using TokenString = std::string;
441struct Location { int line{}; };
442class SIMPLECPP_LIB Token {
443  TokenString prefix;
444  void prefix_method() {}
445 public:
446  Token(const TokenString &s, const Location &loc, bool wsahead = false) :
447      whitespaceahead(wsahead), location(loc), string(s)
448      {
449      flags();
450  }
451  struct Nested { int v; } nested;
452  TokenString string;
453  bool whitespaceahead;
454  Location location;
455 private:
456  void flags() {
457      whitespaceahead = true;
458  }
459};
460}
461"#,
462            ),
463        ];
464
465        for (name, source) in fixtures {
466            let file = ProjectFile::new(
467                std::env::current_dir().expect("test working directory must be available"),
468                "src/widget.h",
469            );
470            let tree = cpp_tree(source);
471            let root = tree.root_node();
472
473            let independent_primary = parse_cpp_file(&file, source, &tree);
474            let independent_c =
475                parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
476
477            let ancestry = ParentIndex::new(root);
478            let shared_primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
479            let shared_c = parse_cpp_c_reading(&file, source, root, &ancestry, &shared_primary);
480
481            assert_eq!(
482                published_facts(&independent_primary),
483                published_facts(&shared_primary),
484                "C++ reading of {name}"
485            );
486            assert_eq!(
487                published_facts(&independent_c),
488                published_facts(&shared_c),
489                "C reading of {name}"
490            );
491        }
492    }
493
494    /// A tag nested in an aggregate is the whole reason the second reading
495    /// exists, so the fixture above must actually produce two different
496    /// readings; otherwise the comparison would pass on two empty answers.
497    #[test]
498    fn the_nested_tag_fixture_really_has_two_readings() {
499        let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
500        let file = ProjectFile::new(
501            std::env::current_dir().expect("test working directory must be available"),
502            "src/widget.h",
503        );
504        let tree = cpp_tree(source);
505        let root = tree.root_node();
506        let ancestry = ParentIndex::new(root);
507        let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
508        let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
509        assert!(
510            cpp_projections_differ(&primary, &c_reading),
511            "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
512            primary.declarations(),
513            c_reading.declarations()
514        );
515    }
516
517    /// Every `#include` is an include claim, wherever it is written. The
518    /// declaration walk descends only through declaration scopes, so a
519    /// directive inside a function body (llama.cpp's `sycl/info/aspects.def`
520    /// inside a `switch`) or inside a class body (Eigen's
521    /// `EIGEN_DENSEBASE_PLUGIN` in `DenseBase.h`) used to be invisible.
522    ///
523    /// The nested directive here is not a quoted include, so the established
524    /// `recover_quoted_includes` line scan cannot supply it: only the preorder
525    /// sweep over the tree can.
526    #[test]
527    fn includes_are_recorded_at_every_depth() {
528        let source = r#"
529#include <vector>
530
531class Widget {
532public:
533    int value() const;
534};
535
536int run() {
537    switch (0) {
538#include <sycl/info/aspects.def>
539    default:
540        return 0;
541    }
542}
543"#;
544        let file = ProjectFile::new(
545            std::env::current_dir().expect("test working directory must be available"),
546            "src/widget.cpp",
547        );
548        let mut parser = Parser::new();
549        parser
550            .set_language(&tree_sitter_cpp::LANGUAGE.into())
551            .expect("C++ grammar");
552        let tree = parser.parse(source, None).expect("C++ tree");
553
554        let parsed = parse_cpp_file(&file, source, &tree);
555        let includes = parsed
556            .imports
557            .iter()
558            .map(|import| import.raw_snippet.clone())
559            .collect::<Vec<_>>();
560
561        assert_eq!(
562            includes,
563            vec![
564                "#include <vector>".to_string(),
565                "#include <sycl/info/aspects.def>".to_string(),
566            ]
567        );
568        assert!(!parsed.declarations().is_empty());
569    }
570}