Skip to main content

brokk_bifrost_cpp/
structural.rs

1//! C++ structural spec for `query_code`.
2
3use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
4    attach_positional_argument_roles, attach_role_with_derived_name, attach_terminal_callee,
5    field_name_in_parent, first_named_child,
6};
7use brokk_bifrost_core::analyzer::structural::callable::{
8    CallKind, CallShapeCoverage, CallSiteContext, CallSiteFacts,
9};
10use brokk_bifrost_core::analyzer::structural::edges::{
11    INVERSE_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
12};
13use brokk_bifrost_core::analyzer::structural::facts::Span;
14use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
15use brokk_bifrost_core::analyzer::structural::materialization::{
16    CPP_MATERIALIZATION_SUPPORT, DeclarationMaterializationSupport,
17};
18use brokk_bifrost_core::analyzer::structural::occurrences::{
19    OccurrenceRole, OccurrenceRoleSupport,
20};
21use brokk_bifrost_core::analyzer::structural::resolution::{
22    BindingActivation, BindingKind, EnvironmentAxis, HoistingClass, LexicalEnvironmentSupport,
23    ScopeFormation, default_scope_formation,
24};
25use brokk_bifrost_core::analyzer::structural::routes::{
26    IdentityAxis, IdentityRouteSupport, RouteHopKind,
27};
28use brokk_bifrost_core::analyzer::structural::spec::{RoleSink, StructuralSpec};
29use brokk_bifrost_core::analyzer::tree_walk::{ParentIndex, named_children_iter, node_range};
30use brokk_bifrost_core::analyzer::{Language, Range};
31use brokk_bifrost_core::hash::HashSet;
32use tree_sitter::Node;
33
34#[derive(Debug, Default)]
35pub struct CppStructuralSpec;
36
37pub static CPP_STRUCTURAL_SPEC: CppStructuralSpec = CppStructuralSpec;
38
39pub const CPP_KIND_TABLE: &[(&str, NormalizedKind)] = &[
40    ("call_expression", NormalizedKind::Call),
41    ("new_expression", NormalizedKind::Call),
42    ("field_expression", NormalizedKind::FieldAccess),
43    ("function_definition", NormalizedKind::Function),
44    ("lambda_expression", NormalizedKind::Lambda),
45    ("class_specifier", NormalizedKind::Class),
46    ("struct_specifier", NormalizedKind::Class),
47    ("union_specifier", NormalizedKind::Class),
48    ("alias_declaration", NormalizedKind::Declaration),
49    ("namespace_definition", NormalizedKind::Module),
50    ("assignment_expression", NormalizedKind::Assignment),
51    ("init_declarator", NormalizedKind::Assignment),
52    ("preproc_include", NormalizedKind::Import),
53    ("identifier", NormalizedKind::Identifier),
54    ("field_identifier", NormalizedKind::Identifier),
55    ("namespace_identifier", NormalizedKind::Identifier),
56    ("qualified_identifier", NormalizedKind::Identifier),
57    ("type_identifier", NormalizedKind::Identifier),
58    ("template_function", NormalizedKind::Identifier),
59    ("template_method", NormalizedKind::Identifier),
60    ("template_type", NormalizedKind::Identifier),
61    ("dependent_name", NormalizedKind::Identifier),
62    ("destructor_name", NormalizedKind::Identifier),
63    ("operator_name", NormalizedKind::Identifier),
64    ("primitive_type", NormalizedKind::Identifier),
65    ("char_literal", NormalizedKind::StringLiteral),
66    ("string_literal", NormalizedKind::StringLiteral),
67    ("raw_string_literal", NormalizedKind::StringLiteral),
68    ("number_literal", NormalizedKind::NumericLiteral),
69    ("true", NormalizedKind::BooleanLiteral),
70    ("false", NormalizedKind::BooleanLiteral),
71    ("null", NormalizedKind::NullLiteral),
72    ("return_statement", NormalizedKind::Return),
73    ("throw_statement", NormalizedKind::Throw),
74    ("catch_clause", NormalizedKind::Catch),
75    ("if_statement", NormalizedKind::If),
76    ("for_statement", NormalizedKind::Loop),
77    ("for_range_loop", NormalizedKind::Loop),
78    // A braced block is a scope, and so is a `switch`, whose condition may
79    // declare a variable.
80    ("compound_statement", NormalizedKind::Block),
81    ("switch_statement", NormalizedKind::Block),
82    ("while_statement", NormalizedKind::WhileLoop),
83    ("do_statement", NormalizedKind::WhileLoop),
84];
85
86/// Whether tree-sitter recovered `.field = value` as a declaration child pair:
87/// an `ERROR` containing only the anonymous dot immediately followed by an
88/// `init_declarator` whose declarator is a real identifier.
89///
90/// This is intentionally narrower than rejecting declarators below `ERROR` in
91/// general: macro-decorated declarations and ordinary comma-separated initialized
92/// declarations also use recovery nodes and remain real declarations.
93pub fn is_recovered_designator_init_declarator(node: Node<'_>) -> bool {
94    if node.kind() != "init_declarator" {
95        return false;
96    }
97    let Some(identifier) = node.child_by_field_name("declarator") else {
98        return false;
99    };
100    if identifier.kind() != "identifier" || identifier.is_missing() {
101        return false;
102    }
103    let Some(previous) = node.prev_named_sibling() else {
104        return false;
105    };
106    if previous.kind() != "ERROR"
107        || previous.named_child_count() != 0
108        || previous.child_count() != 1
109        || previous.end_byte() != node.start_byte()
110    {
111        return false;
112    }
113    previous.child(0).is_some_and(|child| {
114        child.kind() == "."
115            && !child.is_named()
116            && child.start_byte() == previous.start_byte()
117            && child.end_byte() == previous.end_byte()
118    })
119}
120
121fn last_named_field_child<'tree>(node: Node<'tree>, field: &str) -> Option<Node<'tree>> {
122    let mut cursor = node.walk();
123    node.children_by_field_name(field, &mut cursor)
124        .filter(|child| child.is_named())
125        .last()
126}
127
128/// The declared name of a `namespace_definition`.
129///
130/// `namespace a::b { }` spells a `nested_namespace_specifier` that nests to
131/// the right, so the innermost specifier's last child is the terminal segment.
132/// An anonymous namespace has no `name` field at all and stays unnamed.
133fn namespace_name_node<'tree>(namespace: Node<'tree>) -> Option<Node<'tree>> {
134    let mut current = namespace.child_by_field_name("name")?;
135    while current.kind() == "nested_namespace_specifier" {
136        current = current.named_child(current.named_child_count().checked_sub(1)?)?;
137    }
138    Some(current)
139}
140
141pub(crate) fn declarator_name_node<'tree>(declarator: Node<'tree>) -> Option<Node<'tree>> {
142    let mut current = declarator;
143    loop {
144        match current.kind() {
145            "identifier"
146            | "field_identifier"
147            | "namespace_identifier"
148            | "type_identifier"
149            | "destructor_name"
150            | "operator_name"
151            | "primitive_type" => return Some(current),
152            "qualified_identifier" => current = last_named_field_child(current, "name")?,
153            "dependent_name" | "template_function" | "template_method" | "template_type" => {
154                current = current.child_by_field_name("name")?;
155            }
156            "function_declarator"
157            | "pointer_declarator"
158            | "array_declarator"
159            | "init_declarator" => current = current.child_by_field_name("declarator")?,
160            "reference_declarator" | "parenthesized_declarator" => {
161                current = first_named_child(current)?;
162            }
163            _ => return None,
164        }
165    }
166}
167
168fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
169    let mut current = expression;
170    loop {
171        match current.kind() {
172            "identifier"
173            | "field_identifier"
174            | "namespace_identifier"
175            | "type_identifier"
176            | "destructor_name"
177            | "operator_name"
178            | "primitive_type"
179            | "this" => return Some(current),
180            "qualified_identifier" => current = last_named_field_child(current, "name")?,
181            "dependent_name" | "template_function" | "template_method" | "template_type" => {
182                current = current.child_by_field_name("name")?;
183            }
184            "field_expression" => current = current.child_by_field_name("field")?,
185            "call_expression" => current = current.child_by_field_name("function")?,
186            "new_expression" => current = current.child_by_field_name("type")?,
187            "parenthesized_expression" => current = first_named_child(current)?,
188            _ => return declarator_name_node(current),
189        }
190    }
191}
192
193fn attach_qualified_scope_receiver(sink: &mut RoleSink<'_>, function: Node<'_>) {
194    if function.kind() != "qualified_identifier" {
195        return;
196    }
197    if let Some(scope) = function.child_by_field_name("scope") {
198        attach_role_with_derived_name(sink, Role::Receiver, scope, expression_name_node);
199    }
200}
201
202fn qualified_declarator_node(mut node: Node<'_>) -> Option<Node<'_>> {
203    loop {
204        if node.kind() == "qualified_identifier" {
205            return Some(node);
206        }
207        node = node
208            .child_by_field_name("declarator")
209            .or_else(|| node.child_by_field_name("name"))
210            .or_else(|| first_named_child(node))?;
211    }
212}
213
214fn node_text<'source>(node: Node<'_>, source: &'source str) -> Option<&'source str> {
215    node.utf8_text(source.as_bytes()).ok()
216}
217
218fn scoped_function_definition(node: Node<'_>) -> Option<Node<'_>> {
219    node.child_by_field_name("declarator")
220        .and_then(qualified_declarator_node)
221        .and_then(|qualified| qualified.child_by_field_name("scope"))
222}
223
224fn is_constructor_definition(node: Node<'_>, source: &str) -> bool {
225    node.child_by_field_name("declarator")
226        .and_then(qualified_declarator_node)
227        .and_then(|qualified| {
228            Some((
229                expression_name_node(qualified.child_by_field_name("scope")?)?,
230                expression_name_node(last_named_field_child(qualified, "name")?)?,
231            ))
232        })
233        .is_some_and(|(scope, name)| node_text(scope, source) == node_text(name, source))
234}
235
236fn unquoted_include_span(node: Node<'_>) -> Option<Span> {
237    if !matches!(node.kind(), "string_literal" | "system_lib_string") {
238        return None;
239    }
240    let start = node.start_byte().checked_add(1)?;
241    let end = node.end_byte().checked_sub(1)?;
242    (start <= end).then_some(Span {
243        start_byte: start,
244        end_byte: end,
245    })
246}
247
248/// The names of every function-like macro this translation unit defines, read
249/// from `preproc_function_def` name fields.
250///
251/// The walk is iterative and descends only through preprocessor conditional
252/// blocks, which is where a definition can nest. A `#define` written inside a
253/// function body is not collected; that is a known and stated boundary, not an
254/// approximation of one.
255fn function_like_macro_names(root: Node<'_>, source: &str) -> HashSet<String> {
256    let mut names = HashSet::default();
257    let mut stack = vec![root];
258    while let Some(node) = stack.pop() {
259        for child in named_children_iter(node) {
260            match child.kind() {
261                "preproc_function_def" => {
262                    if let Some(name) = child.child_by_field_name("name") {
263                        names.insert(source[name.start_byte()..name.end_byte()].to_owned());
264                    }
265                }
266                "preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif"
267                | "preproc_elifdef" => stack.push(child),
268                _ => {}
269            }
270        }
271    }
272    names
273}
274
275fn cpp_member_terminal_name<'tree>(field: Node<'tree>) -> Option<Node<'tree>> {
276    let field = if field.kind() == "dependent_name" {
277        first_named_child(field)?
278    } else {
279        field
280    };
281    expression_name_node(field)
282}
283
284/// Return the one identifier-bearing node that spells a member in a C++
285/// `field_expression`.
286///
287/// The grammar permits the `field` child to be a qualified name, a dependent
288/// name, a template method, or a destructor name. `expression_name_node`
289/// already follows those wrappers to their terminal name. Walking upward from
290/// the candidate and comparing that terminal node keeps receiver identifiers,
291/// declaration names, field-designator labels, and path prefixes out of the
292/// member role without looking at source text.
293fn cpp_member_position(node: Node<'_>) -> Option<OccurrenceRole> {
294    if !matches!(
295        node.kind(),
296        "identifier" | "field_identifier" | "type_identifier" | "operator_name" | "destructor_name"
297    ) {
298        return None;
299    }
300
301    let mut current = node;
302    while let Some(parent) = current.parent() {
303        if parent.kind() == "field_expression" {
304            let field = parent.child_by_field_name("field")?;
305            return cpp_member_terminal_name(field)
306                .filter(|name| name.id() == node.id())
307                .map(|_| OccurrenceRole::MemberPosition);
308        }
309
310        // These are the only named wrappers that can occur between the
311        // terminal field name and its owning field expression.
312        if matches!(
313            parent.kind(),
314            "qualified_identifier" | "dependent_name" | "template_method" | "destructor_name"
315        ) {
316            current = parent;
317        } else {
318            return None;
319        }
320    }
321    None
322}
323
324/// What declares a C or C++ binder.
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326enum CppBinderOwner<'tree> {
327    /// A parameter of a function definition, a lambda or a `catch` clause.
328    Parameter,
329    /// A local declaration; the node is the declarator the name completes.
330    Local(Node<'tree>),
331    /// A range-based `for` variable; the node is the loop.
332    RangeFor(Node<'tree>),
333    /// A structured binding; the node is the binding declarator.
334    StructuredBinding(Node<'tree>),
335    /// A lambda init-capture (`[x = y]`).
336    InitCapture,
337}
338
339/// Declarator wrappers that carry a name inward through their `declarator`
340/// field or their single named child.
341const CPP_DECLARATOR_WRAPPERS: &[&str] = &[
342    "pointer_declarator",
343    "array_declarator",
344    "reference_declarator",
345    "parenthesized_declarator",
346    "attributed_declarator",
347    "variadic_declarator",
348];
349
350/// What declares `node` as a C or C++ variable, or `None` when it is not a
351/// variable binder.
352///
353/// The name is found by walking up the declarator chain. A name that reaches
354/// a function declarator without passing a pointer or reference declarator
355/// names a function; one that does names a function pointer variable. A
356/// parameter binds only in a function definition, a lambda or a `catch`
357/// clause; a prototype's parameter names bind nothing. A file-scope or
358/// member declaration names a member or a global, not a lexical binding.
359fn cpp_binder_owner<'tree>(
360    node: Node<'tree>,
361    parent: impl Fn(Node<'tree>) -> Option<Node<'tree>>,
362) -> Option<CppBinderOwner<'tree>> {
363    if node.kind() != "identifier" {
364        return None;
365    }
366    let mut child = node;
367    let mut current = parent(node)?;
368    let mut indirect = false;
369    loop {
370        let field = field_name_in_parent(current, child);
371        match current.kind() {
372            kind if CPP_DECLARATOR_WRAPPERS.contains(&kind) => {
373                if field.is_some() && field != Some("declarator") {
374                    return None;
375                }
376                indirect |= matches!(kind, "pointer_declarator" | "reference_declarator");
377            }
378            "function_declarator" => {
379                if field != Some("declarator") || !indirect {
380                    return None;
381                }
382            }
383            "init_declarator" => {
384                if field != Some("declarator") {
385                    return None;
386                }
387            }
388            "declaration" => {
389                if field != Some("declarator") {
390                    return None;
391                }
392                let local = std::iter::successors(parent(current), |node| parent(*node))
393                    .take_while(|node| {
394                        !matches!(
395                            node.kind(),
396                            "translation_unit"
397                                | "field_declaration_list"
398                                | "declaration_list"
399                                | "namespace_definition"
400                        )
401                    })
402                    .any(|node| {
403                        matches!(
404                            node.kind(),
405                            "compound_statement" | "for_statement" | "condition_clause"
406                        )
407                    });
408                // The name's scope starts at the end of its declarator, before
409                // any initializer.
410                let declarator = if child.kind() == "init_declarator" {
411                    child.child_by_field_name("declarator")?
412                } else {
413                    child
414                };
415                return local.then_some(CppBinderOwner::Local(declarator));
416            }
417            "parameter_declaration"
418            | "optional_parameter_declaration"
419            | "variadic_parameter_declaration" => {
420                if field != Some("declarator") {
421                    return None;
422                }
423                let list = parent(current)?;
424                let owner = parent(list)?;
425                let binds = match owner.kind() {
426                    "catch_clause" => true,
427                    "abstract_function_declarator" => {
428                        parent(owner).is_some_and(|lambda| lambda.kind() == "lambda_expression")
429                    }
430                    "function_declarator" => {
431                        let mut declarator = owner;
432                        loop {
433                            let Some(up) = parent(declarator) else {
434                                break false;
435                            };
436                            if up.kind() == "function_definition" {
437                                break field_name_in_parent(up, declarator) == Some("declarator");
438                            }
439                            if !CPP_DECLARATOR_WRAPPERS.contains(&up.kind()) {
440                                break false;
441                            }
442                            declarator = up;
443                        }
444                    }
445                    _ => false,
446                };
447                return binds.then_some(CppBinderOwner::Parameter);
448            }
449            "for_range_loop" => {
450                return (field == Some("declarator")).then_some(CppBinderOwner::RangeFor(current));
451            }
452            "structured_binding_declarator" => {
453                return Some(CppBinderOwner::StructuredBinding(current));
454            }
455            "lambda_capture_initializer" => {
456                return (field == Some("left")).then_some(CppBinderOwner::InitCapture);
457            }
458            _ => return None,
459        }
460        child = current;
461        current = parent(current)?;
462    }
463}
464
465/// Whether `node` declares a C or C++ variable, climbing through the walk's
466/// parent index: asking `Node::parent` re-descends from the root, which is
467/// quadratic over a large file.
468fn cpp_is_binder<'tree>(node: Node<'tree>, parents: &ParentIndex<'tree>) -> bool {
469    cpp_binder_owner(node, |node| parents.parent(node)).is_some()
470}
471
472/// The binding one C or C++ binder token introduces, and the interval it is
473/// in effect over.
474///
475/// A parameter or init-capture is in effect over its whole function, lambda
476/// or catch clause. A local is in effect from the end of its declarator: C
477/// and C++ start a name's scope there, before its initializer, so in
478/// `int x = x;` the right side reads the new `x`. A condition declaration is
479/// therefore in effect over the rest of its `if`, including `else`. A
480/// range-`for` variable is in effect over the loop body, and a structured
481/// binding from the end of its binding declarator.
482fn cpp_binding_activation(binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
483    let binding = |kind, hoisting, activation| {
484        Some(BindingActivation {
485            kind,
486            hoisting,
487            activation,
488        })
489    };
490    let after = |node: Node<'_>| Range {
491        start_byte: node.end_byte(),
492        end_byte: scope.end_byte,
493        start_line: node.end_position().row + 1,
494        end_line: scope.end_line,
495    };
496    match cpp_binder_owner(binder, |node| node.parent())? {
497        CppBinderOwner::Parameter | CppBinderOwner::InitCapture => {
498            binding(BindingKind::Parameter, HoistingClass::ScopeWide, scope)
499        }
500        CppBinderOwner::Local(declarator) => binding(
501            BindingKind::Local,
502            HoistingClass::SourceOrder,
503            after(declarator),
504        ),
505        CppBinderOwner::RangeFor(loop_node) => binding(
506            BindingKind::LoopVariable,
507            HoistingClass::DeclaredHead,
508            node_range(loop_node.child_by_field_name("body")?),
509        ),
510        CppBinderOwner::StructuredBinding(declarator) => binding(
511            BindingKind::Local,
512            HoistingClass::SourceOrder,
513            after(declarator),
514        ),
515    }
516}
517
518impl StructuralSpec for CppStructuralSpec {
519    fn language(&self) -> Language {
520        Language::Cpp
521    }
522
523    fn supports_boolean_literal_value(&self) -> bool {
524        true
525    }
526
527    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
528        CPP_KIND_TABLE
529    }
530
531    fn refine_kind(
532        &self,
533        node: Node<'_>,
534        kind: NormalizedKind,
535        enclosing: Option<NormalizedKind>,
536        source: &str,
537        _context: &CallSiteContext,
538    ) -> NormalizedKind {
539        if kind == NormalizedKind::Function
540            && (enclosing == Some(NormalizedKind::Class)
541                || scoped_function_definition(node).is_some())
542        {
543            if is_constructor_definition(node, source) {
544                NormalizedKind::Constructor
545            } else {
546                NormalizedKind::Method
547            }
548        } else {
549            kind
550        }
551    }
552
553    fn supports_kind(&self, kind: NormalizedKind) -> bool {
554        matches!(kind, NormalizedKind::Method | NormalizedKind::Constructor)
555            || self
556                .kind_table()
557                .iter()
558                .any(|(_, fact_kind)| fact_kind.satisfies(kind))
559    }
560
561    fn supports_role(&self, role: Role) -> bool {
562        !matches!(
563            role,
564            Role::Kwarg | Role::Decorator | Role::Iterable | Role::Element
565        )
566    }
567
568    fn call_site_context(&self, root: Node<'_>, source: &str) -> CallSiteContext {
569        CallSiteContext::with_macro_derived_callees(function_like_macro_names(root, source))
570    }
571
572    /// A `new_expression` is a constructor call, and a call whose callee names
573    /// a function-like macro of this translation unit has an argument list
574    /// that belongs to the macro, not to the callable that finally runs
575    /// (#1478). `CALL_TWICE(2)` parses as an ordinary call of two source
576    /// arguments even when the expansion calls a three-parameter callable, so
577    /// the honest answer is unknown coverage and no argument rows at all —
578    /// never a fabricated list an exact-arity assertion could read as clean.
579    ///
580    /// The boundary is deliberate: only macros defined in this file are
581    /// known here, because that is what this file's parse tree contains. A
582    /// macro defined in an included header leaves the site exact, which is
583    /// the same answer the analyzer gives today.
584    fn call_site_facts(
585        &self,
586        node: Node<'_>,
587        source: &str,
588        context: &CallSiteContext,
589    ) -> Option<CallSiteFacts> {
590        if node.kind() == "new_expression" {
591            return Some(CallSiteFacts::of_kind(CallKind::Constructor));
592        }
593        let callee = node.child_by_field_name("function")?;
594        (callee.kind() == "identifier"
595            && context.is_macro_derived_callee(&source[callee.start_byte()..callee.end_byte()]))
596        .then(|| CallSiteFacts::of_coverage(CallShapeCoverage::UnknownMacroDerived))
597    }
598
599    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
600        static SUPPORT: OccurrenceRoleSupport = OccurrenceRoleSupport::NONE
601            .supported(OccurrenceRole::MemberPosition)
602            .supported(OccurrenceRole::ValueReference)
603            .supported(OccurrenceRole::Binder);
604        &SUPPORT
605    }
606
607    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
608        // C and C++ derive scopes and binding intervals; import binders and
609        // the package clause are not derived. The call seams report
610        // per-candidate callable applicability (#1478 M3).
611        static SUPPORT: LexicalEnvironmentSupport = LexicalEnvironmentSupport::NONE
612            .supported(EnvironmentAxis::Scopes)
613            .supported(EnvironmentAxis::BindingIntervals)
614            .supported(EnvironmentAxis::CallableApplicability);
615        &SUPPORT
616    }
617
618    /// An `if` statement is a scope: a C++ condition or init-statement
619    /// declaration is in effect in both branches and nowhere after.
620    fn scope_formation(&self, kind: NormalizedKind) -> ScopeFormation {
621        if kind == NormalizedKind::If {
622            ScopeFormation::BindingScope
623        } else {
624            default_scope_formation(kind)
625        }
626    }
627
628    fn binding_activation(&self, binder: Node<'_>, scope: Range) -> Option<BindingActivation> {
629        cpp_binding_activation(binder, scope)
630    }
631
632    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
633        &CPP_MATERIALIZATION_SUPPORT
634    }
635
636    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
637        &INVERSE_REFERENCE_EDGE_SUPPORT
638    }
639
640    fn identity_route_support(&self) -> &IdentityRouteSupport {
641        // C++'s occurrence adapter is shallow, so it claims no path axes. Its
642        // declaration layer keeps prototype/body occurrences distinct, which
643        // the physical-grouping axis carries, and the analyzer now labels each
644        // of those occurrences a head or a body and pairs a prototype with the
645        // out-of-line callable body that defines it, so the
646        // declaration-definition peer relation is supplied as typed rows with
647        // the body occurrence's own range as provenance (#1650). Classes stay
648        // outside that relation: a same-file forward declaration is not a
649        // distinct occurrence, and across files a class carries no linkage
650        // evidence to pair on.
651        static SUPPORT: IdentityRouteSupport = IdentityRouteSupport::NONE
652            .supported_axis(IdentityAxis::CanonicalIdentity)
653            .supported_axis(IdentityAxis::PhysicalGrouping)
654            .supported_relation(RouteHopKind::NestedOwner)
655            .supported_relation(RouteHopKind::DeclarationDefinitionPeer);
656        &SUPPORT
657    }
658
659    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
660        if let Some(role) = cpp_member_position(node) {
661            sink.occurrence_role(node, role);
662        } else if cpp_is_binder(node, sink.parents()) {
663            sink.occurrence_role(node, OccurrenceRole::Binder);
664        }
665
666        match kind {
667            NormalizedKind::Call => {
668                let function_field = if node.kind() == "new_expression" {
669                    "type"
670                } else {
671                    "function"
672                };
673                if let Some(function) = node.child_by_field_name(function_field) {
674                    attach_terminal_callee(sink, function, expression_name_node(function));
675                    // The callee token of a free-function call is a plain value
676                    // read, and it is the only occurrence the call site leaves
677                    // behind when the function is not indexed (#3466). Member
678                    // calls are excluded: their terminal field is already the
679                    // member-position occurrence, and classifying it twice would
680                    // report one call site as two. A composite callee
681                    // expression names its own callee at an inner node
682                    // (`f()()`), so it is excluded too.
683                    if node.kind() == "call_expression"
684                        && !matches!(function.kind(), "field_expression" | "call_expression")
685                        && let Some(callee) = expression_name_node(function)
686                    {
687                        sink.occurrence_role(callee, OccurrenceRole::ValueReference);
688                    }
689                    if function.kind() == "field_expression"
690                        && let Some(argument) = function.child_by_field_name("argument")
691                    {
692                        attach_role_with_derived_name(
693                            sink,
694                            Role::Receiver,
695                            argument,
696                            expression_name_node,
697                        );
698                    }
699                    attach_qualified_scope_receiver(sink, function);
700                }
701                if let Some(arguments) = node.child_by_field_name("arguments") {
702                    attach_positional_argument_roles(sink, arguments, expression_name_node);
703                }
704            }
705            NormalizedKind::FieldAccess => {
706                if let Some(field) = node.child_by_field_name("field") {
707                    attach_role_with_derived_name(sink, Role::Field, field, expression_name_node);
708                    if let Some(name) = expression_name_node(field) {
709                        sink.set_name(name);
710                    }
711                }
712                if let Some(argument) = node.child_by_field_name("argument") {
713                    attach_role_with_derived_name(
714                        sink,
715                        Role::Object,
716                        argument,
717                        expression_name_node,
718                    );
719                }
720            }
721            NormalizedKind::Function | NormalizedKind::Method | NormalizedKind::Constructor => {
722                if let Some(name) = node
723                    .child_by_field_name("declarator")
724                    .and_then(declarator_name_node)
725                {
726                    sink.set_name(name);
727                }
728            }
729            NormalizedKind::Module => {
730                if let Some(name) = namespace_name_node(node) {
731                    sink.set_name(name);
732                }
733            }
734            NormalizedKind::Class | NormalizedKind::Declaration => {
735                if let Some(name) = node
736                    .child_by_field_name("name")
737                    .and_then(declarator_name_node)
738                    .or_else(|| node.child_by_field_name("name"))
739                {
740                    sink.set_name(name);
741                }
742            }
743            NormalizedKind::Assignment => match node.kind() {
744                "init_declarator" => {
745                    if let Some(declarator) = node.child_by_field_name("declarator") {
746                        attach_role_with_derived_name(
747                            sink,
748                            Role::Left,
749                            declarator,
750                            declarator_name_node,
751                        );
752                        if let Some(name) = declarator_name_node(declarator) {
753                            sink.set_name(name);
754                        }
755                    }
756                    if let Some(value) = node.child_by_field_name("value") {
757                        attach_role_with_derived_name(
758                            sink,
759                            Role::Right,
760                            value,
761                            expression_name_node,
762                        );
763                    }
764                }
765                "assignment_expression" => {
766                    if let Some(left) = node.child_by_field_name("left") {
767                        attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
768                    }
769                    if let Some(right) = node.child_by_field_name("right") {
770                        attach_role_with_derived_name(
771                            sink,
772                            Role::Right,
773                            right,
774                            expression_name_node,
775                        );
776                    }
777                }
778                _ => {}
779            },
780            NormalizedKind::Import => {
781                if let Some(path) = node.child_by_field_name("path") {
782                    if let Some(name) = unquoted_include_span(path) {
783                        sink.role_named_span(Role::Module, path, name);
784                    } else {
785                        attach_role_with_derived_name(
786                            sink,
787                            Role::Module,
788                            path,
789                            expression_name_node,
790                        );
791                    }
792                }
793            }
794            NormalizedKind::Identifier => match expression_name_node(node) {
795                Some(name) => sink.set_name(name),
796                None => sink.set_name(node),
797            },
798            _ => {}
799        }
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::CPP_STRUCTURAL_SPEC;
806    use super::cpp_member_position;
807    use brokk_bifrost_core::analyzer::structural::occurrences::OccurrenceRole;
808    use brokk_bifrost_core::analyzer::structural::spec::{CompiledKinds, RoleSink, StructuralSpec};
809    use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
810    use brokk_bifrost_core::hash::HashMap;
811    use tree_sitter::Parser;
812
813    fn member_occurrences(source: &str) -> Vec<(usize, &str, OccurrenceRole)> {
814        let mut parser = Parser::new();
815        parser
816            .set_language(&tree_sitter_cpp::LANGUAGE.into())
817            .expect("C++ grammar");
818        let tree = parser.parse(source, None).expect("C++ parse");
819        assert!(
820            !tree.root_node().has_error(),
821            "{}",
822            tree.root_node().to_sexp()
823        );
824        let mut found = Vec::new();
825        let mut pending = vec![tree.root_node()];
826        while let Some(node) = pending.pop() {
827            if let Some(role) = cpp_member_position(node) {
828                found.push((
829                    node.start_byte(),
830                    &source[node.start_byte()..node.end_byte()],
831                    role,
832                ));
833            }
834            for index in (0..node.named_child_count()).rev() {
835                if let Some(child) = node.named_child(index) {
836                    pending.push(child);
837                }
838            }
839        }
840        found
841    }
842
843    #[test]
844    fn cpp_member_position_is_limited_to_member_access_and_calls() {
845        let source = concat!(
846            "struct Widget {\n",
847            "    int value;\n",
848            "    int method(int label) {\n",
849            "        return this->value + label.value + label.method()\n",
850            "            + label.template convert<int>() + label.ns::member;\n",
851            "    }\n",
852            "};\n",
853            "int build(Widget widget) {\n",
854            "    Widget result{.value = widget.value};\n",
855            "    return widget.method(result.value);\n",
856            "}\n",
857        );
858        let found = member_occurrences(source);
859        let member_texts = found
860            .iter()
861            .map(|(_, text, role)| (*text, *role))
862            .collect::<Vec<_>>();
863
864        assert_eq!(
865            member_texts,
866            vec![
867                ("value", OccurrenceRole::MemberPosition),
868                ("value", OccurrenceRole::MemberPosition),
869                ("method", OccurrenceRole::MemberPosition),
870                ("convert", OccurrenceRole::MemberPosition),
871                ("member", OccurrenceRole::MemberPosition),
872                ("value", OccurrenceRole::MemberPosition),
873                ("method", OccurrenceRole::MemberPosition),
874                ("value", OccurrenceRole::MemberPosition),
875            ]
876        );
877
878        let at = |needle: &str| source.find(needle).expect("fixture token");
879        for receiver in ["this->", "label.value", "widget.value", "result.value"] {
880            let receiver_start = at(receiver);
881            assert!(
882                found.iter().all(|(offset, _, _)| *offset != receiver_start),
883                "receiver was classified: {receiver:?}"
884            );
885        }
886        let non_members = [
887            at("Widget {"),
888            at("int value") + "int ".len(),
889            at("int method") + "int ".len(),
890            at(".value =") + 1,
891        ];
892        for unrelated_start in non_members {
893            assert!(
894                found
895                    .iter()
896                    .all(|(offset, _, _)| *offset != unrelated_start),
897                "non-member identifier at byte {unrelated_start} was classified"
898            );
899        }
900    }
901
902    #[test]
903    fn cpp_occurrence_support_declares_member_position_and_free_callees() {
904        let support = super::CPP_STRUCTURAL_SPEC.occurrence_role_support();
905        assert!(support.is_supported(OccurrenceRole::MemberPosition));
906        assert!(support.is_supported(OccurrenceRole::ValueReference));
907        for role in [
908            OccurrenceRole::ReceiverPosition,
909            OccurrenceRole::LabelOrKey,
910            OccurrenceRole::DeclarationName,
911        ] {
912            assert!(
913                !support.is_supported(role),
914                "unexpected C++ support for {role:?}"
915            );
916        }
917    }
918
919    /// The callee token of a free-function call is the only occurrence a call
920    /// site leaves behind when the function is not indexed (#3466). Member
921    /// calls stay member-position-only, and a composite callee expression
922    /// names its own callee at an inner node.
923    #[test]
924    fn cpp_free_function_callees_are_value_references() {
925        let source = concat!(
926            "int bound(void) { return 1; }\n",
927            "struct Widget { int method(void) { return 0; } };\n",
928            "int go(struct Widget *widget) {\n",
929            "    return getenv(\"PATH\") != 0\n",
930            "        + ns::getenv(\"USER\") != 0\n",
931            "        + bound()\n",
932            "        + widget->method()\n",
933            "        + bound()();\n",
934            "}\n",
935        );
936        let mut parser = Parser::new();
937        parser
938            .set_language(&tree_sitter_cpp::LANGUAGE.into())
939            .expect("C++ grammar");
940        let tree = parser.parse(source, None).expect("C++ parse");
941        assert!(
942            !tree.root_node().has_error(),
943            "{}",
944            tree.root_node().to_sexp()
945        );
946
947        // Drive the same extraction entry the file walk drives: every fact
948        // node admitted by the compiled kind table, through a real RoleSink.
949        // The sink addresses occurrences by fact id, so the test keeps the
950        // fact id to start byte mapping to name the tokens.
951        let language: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
952        let compiled = CompiledKinds::compile(&language, CPP_STRUCTURAL_SPEC.kind_table());
953        let mut start_by_fact = HashMap::default();
954        let mut extractions = Vec::new();
955        let mut pending = vec![tree.root_node()];
956        while let Some(node) = pending.pop() {
957            if node.is_named()
958                && let Some(kind) = compiled.kind_of(&node)
959                && CPP_STRUCTURAL_SPEC.should_extract(node, kind)
960            {
961                start_by_fact.insert(node.id(), node.start_byte());
962                extractions.push((node.start_byte(), node, kind));
963            }
964            for index in (0..node.named_child_count()).rev() {
965                if let Some(child) = node.named_child(index) {
966                    pending.push(child);
967                }
968            }
969        }
970        extractions.sort_by_key(|(start, _, _)| *start);
971        let mut source_facts =
972            brokk_bifrost_core::analyzer::source_facts::PrimarySourceFactCollector::new(source);
973        let parents = ParentIndex::new(tree.root_node());
974        let mut sink = RoleSink::new(&mut source_facts, 64, None, &parents);
975        for (_, node, kind) in extractions {
976            CPP_STRUCTURAL_SPEC.extract(node, kind, &mut sink);
977        }
978        let (_, _, occurrences, stop) = sink.into_parts();
979        assert_eq!(stop, None);
980
981        let roles_at = |needle: &str| {
982            let start = source.find(needle).expect("fixture token");
983            occurrences
984                .iter()
985                .filter(|occurrence| start_by_fact[&occurrence.target_node] == start)
986                .map(|occurrence| occurrence.role)
987                .collect::<Vec<_>>()
988        };
989        // Free-function callees are value reads, including a qualified callee
990        // whose terminal name is the identifier and the callee of the inner
991        // call in `bound()()`.
992        assert_eq!(
993            roles_at("getenv(\"PATH\")"),
994            vec![OccurrenceRole::ValueReference]
995        );
996        assert_eq!(
997            roles_at("getenv(\"USER\")"),
998            vec![OccurrenceRole::ValueReference],
999            "the terminal name of a qualified callee is the value read"
1000        );
1001        assert_eq!(roles_at("bound()\n"), vec![OccurrenceRole::ValueReference]);
1002        // A member call's terminal field stays one member-position occurrence;
1003        // the call itself adds no second row for the same token.
1004        assert_eq!(roles_at("method()"), vec![OccurrenceRole::MemberPosition]);
1005        // The receiver of a member call is neither a value read of the callee
1006        // nor a member position.
1007        assert!(roles_at("widget->").is_empty());
1008    }
1009}