Skip to main content

brokk_bifrost_cpp/graph/
hits.rs

1use crate::graph::extractor::{EnclosingContext, ScanCtx};
2use crate::graph::resolver::{
3    TargetKind, declarator_name_node, precise_parent_of, same_logical_symbol,
4    visible_owner_from_member_name,
5};
6use brokk_bifrost_core::analyzer::usages::common::{SNIPPET_CONTEXT_LINES, usage_hit};
7use brokk_bifrost_core::analyzer::usages::model::{UsageHitKind, UsageHitSurface};
8use brokk_bifrost_core::analyzer::{CodeUnit, Range};
9use brokk_bifrost_core::text_utils::{find_line_index_for_offset, snippet_around_line};
10use tree_sitter::Node;
11
12pub fn push_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
13    push_hit_with_options(node, ctx, false, UsageHitKind::Reference, false);
14}
15
16pub fn push_type_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
17    if ctx.has_physically_visible_type_target {
18        push_hit_with_options(node, ctx, false, UsageHitKind::Reference, true);
19    } else {
20        push_unproven_hit(node, ctx);
21    }
22}
23
24pub fn push_type_hit_range(anchor: Node<'_>, start: usize, end: usize, ctx: &mut ScanCtx<'_>) {
25    if ctx.has_physically_visible_type_target {
26        push_hit_range_with_options(
27            anchor,
28            start,
29            end,
30            ctx,
31            false,
32            UsageHitKind::Reference,
33            true,
34        );
35    } else {
36        push_unproven_hit_range(anchor, start, end, ctx, UsageHitKind::Reference);
37    }
38}
39
40/// Record a reference occupying `start..end` while judging enclosure from
41/// `anchor`.
42///
43/// A reference recovered from a macro replacement has no node of its own in
44/// the scanned file: the whole replacement is one `preproc_arg`. That token is
45/// the anchor for enclosure and declaration-range questions, while the range
46/// names the exact bytes the member spells.
47pub fn push_reference_hit_range(anchor: Node<'_>, start: usize, end: usize, ctx: &mut ScanCtx<'_>) {
48    push_hit_range_with_options(
49        anchor,
50        start,
51        end,
52        ctx,
53        false,
54        UsageHitKind::Reference,
55        false,
56    );
57}
58
59pub fn push_unproven_reference_hit_range(
60    anchor: Node<'_>,
61    start: usize,
62    end: usize,
63    ctx: &mut ScanCtx<'_>,
64) {
65    push_unproven_hit_range(anchor, start, end, ctx, UsageHitKind::Reference);
66}
67
68pub fn push_self_receiver_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
69    push_hit_with_options(node, ctx, false, UsageHitKind::SelfReceiver, false);
70}
71
72/// Record a recursive free-function reference for the editor surface.
73///
74/// Usage-graph consumers exclude `SelfReceiver` hits, so allowing the
75/// enclosing definition here does not create a self edge in external usage
76/// results. The structured same-symbol check below prevents unrelated
77/// enclosing units from being classified as recursive references.
78pub fn push_recursive_reference_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
79    if *ctx.limit_exceeded {
80        return;
81    }
82    let start = node.start_byte();
83    if is_member_field_own_declarator(node, ctx) {
84        return;
85    }
86    let line_idx = find_line_index_for_offset(ctx.line_starts, start);
87    let Some(enclosing) = enclosing_context(node, ctx).enclosing.clone() else {
88        return;
89    };
90    if !same_logical_symbol(&enclosing, &ctx.spec.target) || is_target_declaration_name(node, ctx) {
91        return;
92    }
93    insert_hit(node, ctx, enclosing, line_idx, UsageHitKind::SelfReceiver);
94}
95
96pub fn push_definition_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
97    push_hit_with_options(node, ctx, true, UsageHitKind::Definition, false);
98}
99
100/// Record a declaration-only spelling that is linked to the target's physical
101/// definition. It is an external reference to that definition even when the
102/// analyzer reconciles both occurrences into one logical CodeUnit, so the
103/// ordinary own-declaration suppression does not apply.
104pub fn push_declaration_reference_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
105    if *ctx.limit_exceeded {
106        return;
107    }
108    let line_idx = find_line_index_for_offset(ctx.line_starts, node.start_byte());
109    insert_hit(
110        node,
111        ctx,
112        ctx.spec.target.clone(),
113        line_idx,
114        UsageHitKind::Reference,
115    );
116}
117
118pub fn push_unproven_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
119    push_unproven_hit_with_kind(node, ctx, UsageHitKind::Reference);
120}
121
122pub fn push_unproven_definition_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
123    push_unproven_hit_with_kind(node, ctx, UsageHitKind::Definition);
124}
125
126fn push_unproven_hit_with_kind(node: Node<'_>, ctx: &mut ScanCtx<'_>, kind: UsageHitKind) {
127    push_unproven_hit_range(node, node.start_byte(), node.end_byte(), ctx, kind);
128}
129
130fn push_unproven_hit_range(
131    anchor: Node<'_>,
132    start: usize,
133    end: usize,
134    ctx: &mut ScanCtx<'_>,
135    kind: UsageHitKind,
136) {
137    if is_inside_target_declaration(anchor, ctx) || is_member_field_own_declarator(anchor, ctx) {
138        return;
139    }
140    let line_idx = find_line_index_for_offset(ctx.line_starts, start);
141    let Some(enclosing) = enclosing_context(anchor, ctx).enclosing.clone() else {
142        return;
143    };
144    if ctx.target_group.contains(&enclosing) {
145        return;
146    }
147    if enclosing == ctx.spec.target || same_logical_symbol(&enclosing, &ctx.spec.target) {
148        return;
149    }
150    let hit = usage_hit(
151        ctx.file,
152        line_idx,
153        start,
154        end,
155        enclosing,
156        snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
157    );
158    let hit = match kind {
159        UsageHitKind::Reference => hit,
160        UsageHitKind::Definition => hit.into_definition(),
161        UsageHitKind::Import
162        | UsageHitKind::Reexport
163        | UsageHitKind::SelfReceiver
164        | UsageHitKind::DeclaredReference
165        | UsageHitKind::OverrideDeclaration => {
166            unreachable!("unsupported unproven C++ hit emission kind: {kind:?}")
167        }
168    };
169    ctx.unproven_hits.insert(hit.into_unproven());
170}
171
172fn push_hit_with_options(
173    node: Node<'_>,
174    ctx: &mut ScanCtx<'_>,
175    allow_logical_target_enclosing: bool,
176    kind: UsageHitKind,
177    allow_inside_target_declaration: bool,
178) {
179    push_hit_range_with_options(
180        node,
181        node.start_byte(),
182        node.end_byte(),
183        ctx,
184        allow_logical_target_enclosing,
185        kind,
186        allow_inside_target_declaration,
187    );
188}
189
190#[allow(clippy::too_many_arguments)]
191fn push_hit_range_with_options(
192    anchor: Node<'_>,
193    start: usize,
194    end: usize,
195    ctx: &mut ScanCtx<'_>,
196    allow_logical_target_enclosing: bool,
197    kind: UsageHitKind,
198    allow_inside_target_declaration: bool,
199) {
200    if *ctx.limit_exceeded {
201        return;
202    }
203    if is_member_field_own_declarator(anchor, ctx) {
204        return;
205    }
206    let inside_target_declaration =
207        !allow_inside_target_declaration && is_inside_target_declaration(anchor, ctx);
208    let line_idx = find_line_index_for_offset(ctx.line_starts, start);
209    let Some(enclosing) = enclosing_context(anchor, ctx).enclosing.clone() else {
210        return;
211    };
212    // A reference whose enclosing declaration is the target itself is a
213    // recursive call (#1638). When the target is declared and defined in one
214    // place the site sits inside the target's own declaration range, which is
215    // why it has to be decided before that range is consulted. The declared
216    // name itself is excluded structurally, through the declarator chain, so
217    // the declaration does not become a usage of itself. `SelfReceiver` gives
218    // the same contract as [`push_recursive_reference_hit`]: editor-visible,
219    // absent from the external usage surface.
220    if matches!(kind, UsageHitKind::Reference | UsageHitKind::SelfReceiver)
221        && ctx.spec.target.is_function()
222        && enclosing == ctx.spec.target
223        && !is_target_declaration_name(anchor, ctx)
224    {
225        insert_hit_range(
226            start,
227            end,
228            ctx,
229            enclosing,
230            line_idx,
231            UsageHitKind::SelfReceiver,
232        );
233        return;
234    }
235    if inside_target_declaration {
236        return;
237    }
238    if ctx.target_group.contains(&enclosing) {
239        return;
240    }
241    if enclosing == ctx.spec.target
242        || (!allow_logical_target_enclosing && same_logical_symbol(&enclosing, &ctx.spec.target))
243    {
244        return;
245    }
246    insert_hit_range(start, end, ctx, enclosing, line_idx, kind);
247}
248
249fn insert_hit(
250    node: Node<'_>,
251    ctx: &mut ScanCtx<'_>,
252    enclosing: CodeUnit,
253    line_idx: usize,
254    kind: UsageHitKind,
255) {
256    insert_hit_range(
257        node.start_byte(),
258        node.end_byte(),
259        ctx,
260        enclosing,
261        line_idx,
262        kind,
263    );
264}
265
266fn insert_hit_range(
267    start: usize,
268    end: usize,
269    ctx: &mut ScanCtx<'_>,
270    enclosing: CodeUnit,
271    line_idx: usize,
272    kind: UsageHitKind,
273) {
274    let hit = usage_hit(
275        ctx.file,
276        line_idx,
277        start,
278        end,
279        enclosing,
280        snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
281    );
282    let hit = match kind {
283        UsageHitKind::Reference => hit,
284        UsageHitKind::SelfReceiver => hit.into_self_receiver(),
285        UsageHitKind::Definition => hit.into_definition(),
286        UsageHitKind::Import
287        | UsageHitKind::Reexport
288        | UsageHitKind::DeclaredReference
289        | UsageHitKind::OverrideDeclaration => {
290            unreachable!("unsupported C++ hit emission kind: {kind:?}")
291        }
292    };
293    ctx.hits.insert(hit);
294    if kind.included_in(UsageHitSurface::ExternalUsages)
295        && ctx
296            .hits
297            .iter()
298            .filter(|hit| hit.kind.included_in(UsageHitSurface::ExternalUsages))
299            .count()
300            > ctx.max_usages
301    {
302        *ctx.limit_exceeded = true;
303    }
304}
305
306pub fn enclosing_context(node: Node<'_>, ctx: &ScanCtx<'_>) -> EnclosingContext {
307    let key = (node.start_byte(), node.end_byte());
308    if let Some(cached) = ctx.enclosing_cache.borrow().get(&key).cloned() {
309        return cached;
310    }
311    let range = Range {
312        start_byte: node.start_byte(),
313        end_byte: node.end_byte(),
314        start_line: find_line_index_for_offset(ctx.line_starts, node.start_byte()),
315        end_line: find_line_index_for_offset(ctx.line_starts, node.end_byte()),
316    };
317    let enclosing = ctx.analyzer.enclosing_code_unit(ctx.file, &range);
318    let owner = enclosing.as_ref().and_then(|enclosing| {
319        let cached = ctx.enclosing_owner_cache.borrow().get(enclosing).cloned();
320        if let Some(cached) = cached {
321            return cached;
322        }
323        let resolved = precise_parent_of(&ctx.analyzer, ctx.visibility, enclosing)
324            .or_else(|| visible_owner_from_member_name(ctx, enclosing));
325        ctx.enclosing_owner_cache
326            .borrow_mut()
327            .insert(enclosing.clone(), resolved.clone());
328        resolved
329    });
330    let context = EnclosingContext { enclosing, owner };
331    ctx.enclosing_cache
332        .borrow_mut()
333        .insert(key, context.clone());
334    context
335}
336
337/// Returns whether `node` is the target declaration's own declared name.
338///
339/// The declarator chain of a C++ declaration bottoms out at the declared name
340/// (`function_definition.declarator -> function_declarator.declarator ->
341/// identifier`), while parameters, default arguments, and the body hang off
342/// sibling fields. Containment in that terminal therefore covers a qualified
343/// out-of-line name (`void Foo::target()`) without also covering a call written
344/// in a default argument. `declarator_name_node` walks that chain through the
345/// wrappers the grammar leaves unlabelled, such as `reference_declarator`.
346fn is_target_declaration_name(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
347    let mut current = Some(node);
348    while let Some(candidate) = current {
349        if ctx.target_declaration_ranges.iter().any(|range| {
350            candidate.start_byte() == range.start_byte && candidate.end_byte() == range.end_byte
351        }) {
352            return declarator_name_node(candidate).is_some_and(|declarator| {
353                node.start_byte() >= declarator.start_byte()
354                    && node.end_byte() <= declarator.end_byte()
355            });
356        }
357        current = candidate.parent();
358    }
359    false
360}
361
362fn is_inside_target_declaration(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
363    ctx.target_declaration_ranges
364        .iter()
365        .any(|range| node.start_byte() >= range.start_byte && node.end_byte() <= range.end_byte)
366}
367
368/// Returns whether `node` is on the declared-name path of a class field.
369///
370/// A `field_declaration` also owns default member initializers and, for method
371/// declarations, parameter default values. Those subtrees contain genuine
372/// references and must not be discarded with the declaration's own name.
373///
374/// The declared name is the leaf of the declarator chain, and the grammar
375/// leaves some links in that chain unlabelled: `reference_declarator` holds its
376/// inner declarator as a plain named child, so `XmlWriter& write(Fmt f = A::B)`
377/// folds the whole parameter list into the outermost declarator's range.
378/// `declarator_name_node` follows the unlabelled links as well (#2196).
379pub fn is_member_field_own_declarator(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
380    if !matches!(ctx.spec.kind, TargetKind::MemberField) {
381        return false;
382    }
383    let mut current = node.parent();
384    while let Some(parent) = current {
385        if parent.kind() == "field_declaration" {
386            let mut cursor = parent.walk();
387            return parent
388                .children_by_field_name("declarator", &mut cursor)
389                .filter_map(declarator_name_node)
390                .any(|declarator| {
391                    node.start_byte() >= declarator.start_byte()
392                        && node.end_byte() <= declarator.end_byte()
393                });
394        }
395        if matches!(parent.kind(), "compound_statement" | "function_definition") {
396            return false;
397        }
398        current = parent.parent();
399    }
400    false
401}