Skip to main content

brokk_bifrost_cpp/graph/
hits.rs

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