Skip to main content

brokk_bifrost_cpp/
adapter.rs

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