Skip to main content

brokk_bifrost_cpp/
structural.rs

1//! C++ structural spec for `query_code`.
2
3use brokk_bifrost_core::analyzer::Language;
4use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
5    attach_positional_argument_roles, attach_role_with_derived_name, attach_terminal_callee,
6    first_named_child,
7};
8use brokk_bifrost_core::analyzer::structural::callable::{
9    CallKind, CallShapeCoverage, CallSiteContext, CallSiteFacts,
10};
11use brokk_bifrost_core::analyzer::structural::edges::{
12    INVERSE_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
13};
14use brokk_bifrost_core::analyzer::structural::facts::Span;
15use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
16use brokk_bifrost_core::analyzer::structural::materialization::{
17    CPP_MATERIALIZATION_SUPPORT, DeclarationMaterializationSupport,
18};
19use brokk_bifrost_core::analyzer::structural::occurrences::{
20    OccurrenceRole, OccurrenceRoleSupport,
21};
22use brokk_bifrost_core::analyzer::structural::resolution::{
23    CALLABLE_APPLICABILITY_ONLY_SUPPORT, LexicalEnvironmentSupport,
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::named_children_iter;
30use brokk_bifrost_core::hash::HashSet;
31use tree_sitter::Node;
32
33#[derive(Debug, Default)]
34pub struct CppStructuralSpec;
35
36pub static CPP_STRUCTURAL_SPEC: CppStructuralSpec = CppStructuralSpec;
37
38pub const CPP_KIND_TABLE: &[(&str, NormalizedKind)] = &[
39    ("call_expression", NormalizedKind::Call),
40    ("new_expression", NormalizedKind::Call),
41    ("field_expression", NormalizedKind::FieldAccess),
42    ("function_definition", NormalizedKind::Function),
43    ("lambda_expression", NormalizedKind::Lambda),
44    ("class_specifier", NormalizedKind::Class),
45    ("struct_specifier", NormalizedKind::Class),
46    ("union_specifier", NormalizedKind::Class),
47    ("alias_declaration", NormalizedKind::Declaration),
48    ("namespace_definition", NormalizedKind::Module),
49    ("assignment_expression", NormalizedKind::Assignment),
50    ("init_declarator", NormalizedKind::Assignment),
51    ("preproc_include", NormalizedKind::Import),
52    ("identifier", NormalizedKind::Identifier),
53    ("field_identifier", NormalizedKind::Identifier),
54    ("namespace_identifier", NormalizedKind::Identifier),
55    ("qualified_identifier", NormalizedKind::Identifier),
56    ("type_identifier", NormalizedKind::Identifier),
57    ("template_function", NormalizedKind::Identifier),
58    ("template_method", NormalizedKind::Identifier),
59    ("template_type", NormalizedKind::Identifier),
60    ("dependent_name", NormalizedKind::Identifier),
61    ("destructor_name", NormalizedKind::Identifier),
62    ("operator_name", NormalizedKind::Identifier),
63    ("primitive_type", NormalizedKind::Identifier),
64    ("char_literal", NormalizedKind::StringLiteral),
65    ("string_literal", NormalizedKind::StringLiteral),
66    ("raw_string_literal", NormalizedKind::StringLiteral),
67    ("number_literal", NormalizedKind::NumericLiteral),
68    ("true", NormalizedKind::BooleanLiteral),
69    ("false", NormalizedKind::BooleanLiteral),
70    ("null", NormalizedKind::NullLiteral),
71    ("return_statement", NormalizedKind::Return),
72    ("throw_statement", NormalizedKind::Throw),
73    ("catch_clause", NormalizedKind::Catch),
74    ("if_statement", NormalizedKind::If),
75    ("for_statement", NormalizedKind::Loop),
76    ("while_statement", NormalizedKind::WhileLoop),
77    ("do_statement", NormalizedKind::WhileLoop),
78];
79
80/// Whether tree-sitter recovered `.field = value` as a declaration child pair:
81/// an `ERROR` containing only the anonymous dot immediately followed by an
82/// `init_declarator` whose declarator is a real identifier.
83///
84/// This is intentionally narrower than rejecting declarators below `ERROR` in
85/// general: macro-decorated declarations and ordinary comma-separated initialized
86/// declarations also use recovery nodes and remain real declarations.
87pub fn is_recovered_designator_init_declarator(node: Node<'_>) -> bool {
88    if node.kind() != "init_declarator" {
89        return false;
90    }
91    let Some(identifier) = node.child_by_field_name("declarator") else {
92        return false;
93    };
94    if identifier.kind() != "identifier" || identifier.is_missing() {
95        return false;
96    }
97    let Some(previous) = node.prev_named_sibling() else {
98        return false;
99    };
100    if previous.kind() != "ERROR"
101        || previous.named_child_count() != 0
102        || previous.child_count() != 1
103        || previous.end_byte() != node.start_byte()
104    {
105        return false;
106    }
107    previous.child(0).is_some_and(|child| {
108        child.kind() == "."
109            && !child.is_named()
110            && child.start_byte() == previous.start_byte()
111            && child.end_byte() == previous.end_byte()
112    })
113}
114
115fn last_named_field_child<'tree>(node: Node<'tree>, field: &str) -> Option<Node<'tree>> {
116    let mut cursor = node.walk();
117    node.children_by_field_name(field, &mut cursor)
118        .filter(|child| child.is_named())
119        .last()
120}
121
122/// The declared name of a `namespace_definition`.
123///
124/// `namespace a::b { }` spells a `nested_namespace_specifier` that nests to
125/// the right, so the innermost specifier's last child is the terminal segment.
126/// An anonymous namespace has no `name` field at all and stays unnamed.
127fn namespace_name_node<'tree>(namespace: Node<'tree>) -> Option<Node<'tree>> {
128    let mut current = namespace.child_by_field_name("name")?;
129    while current.kind() == "nested_namespace_specifier" {
130        current = current.named_child(current.named_child_count().checked_sub(1)?)?;
131    }
132    Some(current)
133}
134
135fn declarator_name_node<'tree>(declarator: Node<'tree>) -> Option<Node<'tree>> {
136    let mut current = declarator;
137    loop {
138        match current.kind() {
139            "identifier"
140            | "field_identifier"
141            | "namespace_identifier"
142            | "type_identifier"
143            | "destructor_name"
144            | "operator_name"
145            | "primitive_type" => return Some(current),
146            "qualified_identifier" => current = last_named_field_child(current, "name")?,
147            "dependent_name" | "template_function" | "template_method" | "template_type" => {
148                current = current.child_by_field_name("name")?;
149            }
150            "function_declarator"
151            | "pointer_declarator"
152            | "array_declarator"
153            | "init_declarator" => current = current.child_by_field_name("declarator")?,
154            "reference_declarator" | "parenthesized_declarator" => {
155                current = first_named_child(current)?;
156            }
157            _ => return None,
158        }
159    }
160}
161
162fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
163    let mut current = expression;
164    loop {
165        match current.kind() {
166            "identifier"
167            | "field_identifier"
168            | "namespace_identifier"
169            | "type_identifier"
170            | "destructor_name"
171            | "operator_name"
172            | "primitive_type"
173            | "this" => return Some(current),
174            "qualified_identifier" => current = last_named_field_child(current, "name")?,
175            "dependent_name" | "template_function" | "template_method" | "template_type" => {
176                current = current.child_by_field_name("name")?;
177            }
178            "field_expression" => current = current.child_by_field_name("field")?,
179            "call_expression" => current = current.child_by_field_name("function")?,
180            "new_expression" => current = current.child_by_field_name("type")?,
181            "parenthesized_expression" => current = first_named_child(current)?,
182            _ => return declarator_name_node(current),
183        }
184    }
185}
186
187fn attach_qualified_scope_receiver(sink: &mut RoleSink<'_>, function: Node<'_>) {
188    if function.kind() != "qualified_identifier" {
189        return;
190    }
191    if let Some(scope) = function.child_by_field_name("scope") {
192        attach_role_with_derived_name(sink, Role::Receiver, scope, expression_name_node);
193    }
194}
195
196fn qualified_declarator_node(mut node: Node<'_>) -> Option<Node<'_>> {
197    loop {
198        if node.kind() == "qualified_identifier" {
199            return Some(node);
200        }
201        node = node
202            .child_by_field_name("declarator")
203            .or_else(|| node.child_by_field_name("name"))
204            .or_else(|| first_named_child(node))?;
205    }
206}
207
208fn node_text<'source>(node: Node<'_>, source: &'source str) -> Option<&'source str> {
209    node.utf8_text(source.as_bytes()).ok()
210}
211
212fn scoped_function_definition(node: Node<'_>) -> Option<Node<'_>> {
213    node.child_by_field_name("declarator")
214        .and_then(qualified_declarator_node)
215        .and_then(|qualified| qualified.child_by_field_name("scope"))
216}
217
218fn is_constructor_definition(node: Node<'_>, source: &str) -> bool {
219    node.child_by_field_name("declarator")
220        .and_then(qualified_declarator_node)
221        .and_then(|qualified| {
222            Some((
223                expression_name_node(qualified.child_by_field_name("scope")?)?,
224                expression_name_node(last_named_field_child(qualified, "name")?)?,
225            ))
226        })
227        .is_some_and(|(scope, name)| node_text(scope, source) == node_text(name, source))
228}
229
230fn unquoted_include_span(node: Node<'_>) -> Option<Span> {
231    if !matches!(node.kind(), "string_literal" | "system_lib_string") {
232        return None;
233    }
234    let start = node.start_byte().checked_add(1)?;
235    let end = node.end_byte().checked_sub(1)?;
236    (start <= end).then_some(Span {
237        start_byte: start,
238        end_byte: end,
239    })
240}
241
242/// The names of every function-like macro this translation unit defines, read
243/// from `preproc_function_def` name fields.
244///
245/// The walk is iterative and descends only through preprocessor conditional
246/// blocks, which is where a definition can nest. A `#define` written inside a
247/// function body is not collected; that is a known and stated boundary, not an
248/// approximation of one.
249fn function_like_macro_names(root: Node<'_>, source: &str) -> HashSet<String> {
250    let mut names = HashSet::default();
251    let mut stack = vec![root];
252    while let Some(node) = stack.pop() {
253        for child in named_children_iter(node) {
254            match child.kind() {
255                "preproc_function_def" => {
256                    if let Some(name) = child.child_by_field_name("name") {
257                        names.insert(source[name.start_byte()..name.end_byte()].to_owned());
258                    }
259                }
260                "preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif"
261                | "preproc_elifdef" => stack.push(child),
262                _ => {}
263            }
264        }
265    }
266    names
267}
268
269fn cpp_member_terminal_name<'tree>(field: Node<'tree>) -> Option<Node<'tree>> {
270    let field = if field.kind() == "dependent_name" {
271        first_named_child(field)?
272    } else {
273        field
274    };
275    expression_name_node(field)
276}
277
278/// Return the one identifier-bearing node that spells a member in a C++
279/// `field_expression`.
280///
281/// The grammar permits the `field` child to be a qualified name, a dependent
282/// name, a template method, or a destructor name. `expression_name_node`
283/// already follows those wrappers to their terminal name. Walking upward from
284/// the candidate and comparing that terminal node keeps receiver identifiers,
285/// declaration names, field-designator labels, and path prefixes out of the
286/// member role without looking at source text.
287fn cpp_member_position(node: Node<'_>) -> Option<OccurrenceRole> {
288    if !matches!(
289        node.kind(),
290        "identifier" | "field_identifier" | "type_identifier" | "operator_name" | "destructor_name"
291    ) {
292        return None;
293    }
294
295    let mut current = node;
296    while let Some(parent) = current.parent() {
297        if parent.kind() == "field_expression" {
298            let field = parent.child_by_field_name("field")?;
299            return cpp_member_terminal_name(field)
300                .filter(|name| name.id() == node.id())
301                .map(|_| OccurrenceRole::MemberPosition);
302        }
303
304        // These are the only named wrappers that can occur between the
305        // terminal field name and its owning field expression.
306        if matches!(
307            parent.kind(),
308            "qualified_identifier" | "dependent_name" | "template_method" | "destructor_name"
309        ) {
310            current = parent;
311        } else {
312            return None;
313        }
314    }
315    None
316}
317
318impl StructuralSpec for CppStructuralSpec {
319    fn language(&self) -> Language {
320        Language::Cpp
321    }
322
323    fn supports_boolean_literal_value(&self) -> bool {
324        true
325    }
326
327    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
328        CPP_KIND_TABLE
329    }
330
331    fn refine_kind(
332        &self,
333        node: Node<'_>,
334        kind: NormalizedKind,
335        enclosing: Option<NormalizedKind>,
336        source: &str,
337        _context: &CallSiteContext,
338    ) -> NormalizedKind {
339        if kind == NormalizedKind::Function
340            && (enclosing == Some(NormalizedKind::Class)
341                || scoped_function_definition(node).is_some())
342        {
343            if is_constructor_definition(node, source) {
344                NormalizedKind::Constructor
345            } else {
346                NormalizedKind::Method
347            }
348        } else {
349            kind
350        }
351    }
352
353    fn supports_kind(&self, kind: NormalizedKind) -> bool {
354        matches!(kind, NormalizedKind::Method | NormalizedKind::Constructor)
355            || self
356                .kind_table()
357                .iter()
358                .any(|(_, fact_kind)| fact_kind.satisfies(kind))
359    }
360
361    fn supports_role(&self, role: Role) -> bool {
362        !matches!(
363            role,
364            Role::Kwarg | Role::Decorator | Role::Iterable | Role::Element
365        )
366    }
367
368    fn call_site_context(&self, root: Node<'_>, source: &str) -> CallSiteContext {
369        CallSiteContext::with_macro_derived_callees(function_like_macro_names(root, source))
370    }
371
372    /// A `new_expression` is a constructor call, and a call whose callee names
373    /// a function-like macro of this translation unit has an argument list
374    /// that belongs to the macro, not to the callable that finally runs
375    /// (#1478). `CALL_TWICE(2)` parses as an ordinary call of two source
376    /// arguments even when the expansion calls a three-parameter callable, so
377    /// the honest answer is unknown coverage and no argument rows at all —
378    /// never a fabricated list an exact-arity assertion could read as clean.
379    ///
380    /// The boundary is deliberate: only macros defined in this file are
381    /// known here, because that is what this file's parse tree contains. A
382    /// macro defined in an included header leaves the site exact, which is
383    /// the same answer the analyzer gives today.
384    fn call_site_facts(
385        &self,
386        node: Node<'_>,
387        source: &str,
388        context: &CallSiteContext,
389    ) -> Option<CallSiteFacts> {
390        if node.kind() == "new_expression" {
391            return Some(CallSiteFacts::of_kind(CallKind::Constructor));
392        }
393        let callee = node.child_by_field_name("function")?;
394        (callee.kind() == "identifier"
395            && context.is_macro_derived_callee(&source[callee.start_byte()..callee.end_byte()]))
396        .then(|| CallSiteFacts::of_coverage(CallShapeCoverage::UnknownMacroDerived))
397    }
398
399    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
400        static SUPPORT: OccurrenceRoleSupport = OccurrenceRoleSupport::NONE
401            .supported(OccurrenceRole::MemberPosition)
402            .supported(OccurrenceRole::ValueReference);
403        &SUPPORT
404    }
405
406    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
407        // C and C++ classify no scopes, binding intervals, import binders or
408        // package clause, but the call seams report per-candidate callable
409        // applicability (#1478 M3). The per-axis table states exactly that.
410        &CALLABLE_APPLICABILITY_ONLY_SUPPORT
411    }
412
413    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
414        &CPP_MATERIALIZATION_SUPPORT
415    }
416
417    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
418        &INVERSE_REFERENCE_EDGE_SUPPORT
419    }
420
421    fn identity_route_support(&self) -> &IdentityRouteSupport {
422        // C++'s occurrence adapter is shallow, so it claims no path axes. Its
423        // declaration layer keeps prototype/body occurrences distinct, which
424        // the physical-grouping axis carries, and the analyzer now labels each
425        // of those occurrences a head or a body and pairs a prototype with the
426        // out-of-line callable body that defines it, so the
427        // declaration-definition peer relation is supplied as typed rows with
428        // the body occurrence's own range as provenance (#1650). Classes stay
429        // outside that relation: a same-file forward declaration is not a
430        // distinct occurrence, and across files a class carries no linkage
431        // evidence to pair on.
432        static SUPPORT: IdentityRouteSupport = IdentityRouteSupport::NONE
433            .supported_axis(IdentityAxis::CanonicalIdentity)
434            .supported_axis(IdentityAxis::PhysicalGrouping)
435            .supported_relation(RouteHopKind::NestedOwner)
436            .supported_relation(RouteHopKind::DeclarationDefinitionPeer);
437        &SUPPORT
438    }
439
440    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
441        if let Some(role) = cpp_member_position(node) {
442            sink.occurrence_role(node, role);
443        }
444
445        match kind {
446            NormalizedKind::Call => {
447                let function_field = if node.kind() == "new_expression" {
448                    "type"
449                } else {
450                    "function"
451                };
452                if let Some(function) = node.child_by_field_name(function_field) {
453                    attach_terminal_callee(sink, function, expression_name_node(function));
454                    // The callee token of a free-function call is a plain value
455                    // read, and it is the only occurrence the call site leaves
456                    // behind when the function is not indexed (#3466). Member
457                    // calls are excluded: their terminal field is already the
458                    // member-position occurrence, and classifying it twice would
459                    // report one call site as two. A composite callee
460                    // expression names its own callee at an inner node
461                    // (`f()()`), so it is excluded too.
462                    if node.kind() == "call_expression"
463                        && !matches!(function.kind(), "field_expression" | "call_expression")
464                        && let Some(callee) = expression_name_node(function)
465                    {
466                        sink.occurrence_role(callee, OccurrenceRole::ValueReference);
467                    }
468                    if function.kind() == "field_expression"
469                        && let Some(argument) = function.child_by_field_name("argument")
470                    {
471                        attach_role_with_derived_name(
472                            sink,
473                            Role::Receiver,
474                            argument,
475                            expression_name_node,
476                        );
477                    }
478                    attach_qualified_scope_receiver(sink, function);
479                }
480                if let Some(arguments) = node.child_by_field_name("arguments") {
481                    attach_positional_argument_roles(sink, arguments, expression_name_node);
482                }
483            }
484            NormalizedKind::FieldAccess => {
485                if let Some(field) = node.child_by_field_name("field") {
486                    attach_role_with_derived_name(sink, Role::Field, field, expression_name_node);
487                    if let Some(name) = expression_name_node(field) {
488                        sink.set_name(name);
489                    }
490                }
491                if let Some(argument) = node.child_by_field_name("argument") {
492                    attach_role_with_derived_name(
493                        sink,
494                        Role::Object,
495                        argument,
496                        expression_name_node,
497                    );
498                }
499            }
500            NormalizedKind::Function | NormalizedKind::Method | NormalizedKind::Constructor => {
501                if let Some(name) = node
502                    .child_by_field_name("declarator")
503                    .and_then(declarator_name_node)
504                {
505                    sink.set_name(name);
506                }
507            }
508            NormalizedKind::Module => {
509                if let Some(name) = namespace_name_node(node) {
510                    sink.set_name(name);
511                }
512            }
513            NormalizedKind::Class | NormalizedKind::Declaration => {
514                if let Some(name) = node
515                    .child_by_field_name("name")
516                    .and_then(declarator_name_node)
517                    .or_else(|| node.child_by_field_name("name"))
518                {
519                    sink.set_name(name);
520                }
521            }
522            NormalizedKind::Assignment => match node.kind() {
523                "init_declarator" => {
524                    if let Some(declarator) = node.child_by_field_name("declarator") {
525                        attach_role_with_derived_name(
526                            sink,
527                            Role::Left,
528                            declarator,
529                            declarator_name_node,
530                        );
531                        if let Some(name) = declarator_name_node(declarator) {
532                            sink.set_name(name);
533                        }
534                    }
535                    if let Some(value) = node.child_by_field_name("value") {
536                        attach_role_with_derived_name(
537                            sink,
538                            Role::Right,
539                            value,
540                            expression_name_node,
541                        );
542                    }
543                }
544                "assignment_expression" => {
545                    if let Some(left) = node.child_by_field_name("left") {
546                        attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
547                    }
548                    if let Some(right) = node.child_by_field_name("right") {
549                        attach_role_with_derived_name(
550                            sink,
551                            Role::Right,
552                            right,
553                            expression_name_node,
554                        );
555                    }
556                }
557                _ => {}
558            },
559            NormalizedKind::Import => {
560                if let Some(path) = node.child_by_field_name("path") {
561                    if let Some(name) = unquoted_include_span(path) {
562                        sink.role_named_span(Role::Module, path, name);
563                    } else {
564                        attach_role_with_derived_name(
565                            sink,
566                            Role::Module,
567                            path,
568                            expression_name_node,
569                        );
570                    }
571                }
572            }
573            NormalizedKind::Identifier => match expression_name_node(node) {
574                Some(name) => sink.set_name(name),
575                None => sink.set_name(node),
576            },
577            _ => {}
578        }
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::CPP_STRUCTURAL_SPEC;
585    use super::cpp_member_position;
586    use brokk_bifrost_core::analyzer::structural::occurrences::OccurrenceRole;
587    use brokk_bifrost_core::analyzer::structural::spec::{CompiledKinds, RoleSink, StructuralSpec};
588    use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
589    use brokk_bifrost_core::hash::HashMap;
590    use tree_sitter::Parser;
591
592    fn member_occurrences(source: &str) -> Vec<(usize, &str, OccurrenceRole)> {
593        let mut parser = Parser::new();
594        parser
595            .set_language(&tree_sitter_cpp::LANGUAGE.into())
596            .expect("C++ grammar");
597        let tree = parser.parse(source, None).expect("C++ parse");
598        assert!(
599            !tree.root_node().has_error(),
600            "{}",
601            tree.root_node().to_sexp()
602        );
603        let mut found = Vec::new();
604        let mut pending = vec![tree.root_node()];
605        while let Some(node) = pending.pop() {
606            if let Some(role) = cpp_member_position(node) {
607                found.push((
608                    node.start_byte(),
609                    &source[node.start_byte()..node.end_byte()],
610                    role,
611                ));
612            }
613            for index in (0..node.named_child_count()).rev() {
614                if let Some(child) = node.named_child(index) {
615                    pending.push(child);
616                }
617            }
618        }
619        found
620    }
621
622    #[test]
623    fn cpp_member_position_is_limited_to_member_access_and_calls() {
624        let source = concat!(
625            "struct Widget {\n",
626            "    int value;\n",
627            "    int method(int label) {\n",
628            "        return this->value + label.value + label.method()\n",
629            "            + label.template convert<int>() + label.ns::member;\n",
630            "    }\n",
631            "};\n",
632            "int build(Widget widget) {\n",
633            "    Widget result{.value = widget.value};\n",
634            "    return widget.method(result.value);\n",
635            "}\n",
636        );
637        let found = member_occurrences(source);
638        let member_texts = found
639            .iter()
640            .map(|(_, text, role)| (*text, *role))
641            .collect::<Vec<_>>();
642
643        assert_eq!(
644            member_texts,
645            vec![
646                ("value", OccurrenceRole::MemberPosition),
647                ("value", OccurrenceRole::MemberPosition),
648                ("method", OccurrenceRole::MemberPosition),
649                ("convert", OccurrenceRole::MemberPosition),
650                ("member", OccurrenceRole::MemberPosition),
651                ("value", OccurrenceRole::MemberPosition),
652                ("method", OccurrenceRole::MemberPosition),
653                ("value", OccurrenceRole::MemberPosition),
654            ]
655        );
656
657        let at = |needle: &str| source.find(needle).expect("fixture token");
658        for receiver in ["this->", "label.value", "widget.value", "result.value"] {
659            let receiver_start = at(receiver);
660            assert!(
661                found.iter().all(|(offset, _, _)| *offset != receiver_start),
662                "receiver was classified: {receiver:?}"
663            );
664        }
665        let non_members = [
666            at("Widget {"),
667            at("int value") + "int ".len(),
668            at("int method") + "int ".len(),
669            at(".value =") + 1,
670        ];
671        for unrelated_start in non_members {
672            assert!(
673                found
674                    .iter()
675                    .all(|(offset, _, _)| *offset != unrelated_start),
676                "non-member identifier at byte {unrelated_start} was classified"
677            );
678        }
679    }
680
681    #[test]
682    fn cpp_occurrence_support_declares_member_position_and_free_callees() {
683        let support = super::CPP_STRUCTURAL_SPEC.occurrence_role_support();
684        assert!(support.is_supported(OccurrenceRole::MemberPosition));
685        assert!(support.is_supported(OccurrenceRole::ValueReference));
686        for role in [
687            OccurrenceRole::ReceiverPosition,
688            OccurrenceRole::LabelOrKey,
689            OccurrenceRole::DeclarationName,
690        ] {
691            assert!(
692                !support.is_supported(role),
693                "unexpected C++ support for {role:?}"
694            );
695        }
696    }
697
698    /// The callee token of a free-function call is the only occurrence a call
699    /// site leaves behind when the function is not indexed (#3466). Member
700    /// calls stay member-position-only, and a composite callee expression
701    /// names its own callee at an inner node.
702    #[test]
703    fn cpp_free_function_callees_are_value_references() {
704        let source = concat!(
705            "int bound(void) { return 1; }\n",
706            "struct Widget { int method(void) { return 0; } };\n",
707            "int go(struct Widget *widget) {\n",
708            "    return getenv(\"PATH\") != 0\n",
709            "        + ns::getenv(\"USER\") != 0\n",
710            "        + bound()\n",
711            "        + widget->method()\n",
712            "        + bound()();\n",
713            "}\n",
714        );
715        let mut parser = Parser::new();
716        parser
717            .set_language(&tree_sitter_cpp::LANGUAGE.into())
718            .expect("C++ grammar");
719        let tree = parser.parse(source, None).expect("C++ parse");
720        assert!(
721            !tree.root_node().has_error(),
722            "{}",
723            tree.root_node().to_sexp()
724        );
725
726        // Drive the same extraction entry the file walk drives: every fact
727        // node admitted by the compiled kind table, through a real RoleSink.
728        // The sink addresses occurrences by fact id, so the test keeps the
729        // fact id to start byte mapping to name the tokens.
730        let language: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
731        let compiled = CompiledKinds::compile(&language, CPP_STRUCTURAL_SPEC.kind_table());
732        let mut facts = HashMap::default();
733        let mut start_by_fact = HashMap::default();
734        let mut extractions = Vec::new();
735        let mut pending = vec![tree.root_node()];
736        while let Some(node) = pending.pop() {
737            if node.is_named()
738                && let Some(kind) = compiled.kind_of(&node)
739                && CPP_STRUCTURAL_SPEC.should_extract(node, kind)
740            {
741                let fact = facts.len() as u32;
742                facts.insert(node.id(), fact);
743                start_by_fact.insert(fact, node.start_byte());
744                extractions.push((node.start_byte(), node, kind));
745            }
746            for index in (0..node.named_child_count()).rev() {
747                if let Some(child) = node.named_child(index) {
748                    pending.push(child);
749                }
750            }
751        }
752        extractions.sort_by_key(|(start, _, _)| *start);
753        let mut roles = Vec::new();
754        let mut occurrences = Vec::new();
755        let parents = ParentIndex::new(tree.root_node());
756        let mut sink = RoleSink::new(&facts, &mut roles, &mut occurrences, 64, None, &parents);
757        for (_, node, kind) in extractions {
758            CPP_STRUCTURAL_SPEC.extract(node, kind, &mut sink);
759        }
760        assert_eq!(sink.into_parts().1, None);
761
762        let roles_at = |needle: &str| {
763            let start = source.find(needle).expect("fixture token");
764            occurrences
765                .iter()
766                .filter(|(fact, _)| start_by_fact[fact] == start)
767                .map(|(_, role)| *role)
768                .collect::<Vec<_>>()
769        };
770        // Free-function callees are value reads, including a qualified callee
771        // whose terminal name is the identifier and the callee of the inner
772        // call in `bound()()`.
773        assert_eq!(
774            roles_at("getenv(\"PATH\")"),
775            vec![OccurrenceRole::ValueReference]
776        );
777        assert_eq!(
778            roles_at("getenv(\"USER\")"),
779            vec![OccurrenceRole::ValueReference],
780            "the terminal name of a qualified callee is the value read"
781        );
782        assert_eq!(roles_at("bound()\n"), vec![OccurrenceRole::ValueReference]);
783        // A member call's terminal field stays one member-position occurrence;
784        // the call itself adds no second row for the same token.
785        assert_eq!(roles_at("method()"), vec![OccurrenceRole::MemberPosition]);
786        // The receiver of a member call is neither a value read of the callee
787        // nor a member position.
788        assert!(roles_at("widget->").is_empty());
789    }
790}