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_self_receiver_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
24    push_hit_with_options(node, ctx, false, UsageHitKind::SelfReceiver, false);
25}
26
27/// Record a recursive free-function reference for the editor surface.
28///
29/// Usage-graph consumers exclude `SelfReceiver` hits, so allowing the
30/// enclosing definition here does not create a self edge in external usage
31/// results. The structured same-symbol check below prevents unrelated
32/// enclosing units from being classified as recursive references.
33pub fn push_recursive_reference_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
34    if *ctx.limit_exceeded {
35        return;
36    }
37    let start = node.start_byte();
38    if is_member_field_own_declarator(node, ctx) {
39        return;
40    }
41    let line_idx = find_line_index_for_offset(ctx.line_starts, start);
42    let Some(enclosing) = enclosing_context(node, ctx).enclosing.clone() else {
43        return;
44    };
45    if !same_logical_symbol(&enclosing, &ctx.spec.target) || is_target_declaration_name(node, ctx) {
46        return;
47    }
48    insert_hit(node, ctx, enclosing, line_idx, UsageHitKind::SelfReceiver);
49}
50
51pub fn push_definition_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
52    push_hit_with_options(node, ctx, true, UsageHitKind::Definition, false);
53}
54
55pub fn push_unproven_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
56    push_unproven_hit_with_kind(node, ctx, UsageHitKind::Reference);
57}
58
59pub fn push_unproven_definition_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
60    push_unproven_hit_with_kind(node, ctx, UsageHitKind::Definition);
61}
62
63fn push_unproven_hit_with_kind(node: Node<'_>, ctx: &mut ScanCtx<'_>, kind: UsageHitKind) {
64    if is_inside_target_declaration(node, ctx) || is_member_field_own_declarator(node, ctx) {
65        return;
66    }
67    let start = node.start_byte();
68    let end = node.end_byte();
69    let line_idx = find_line_index_for_offset(ctx.line_starts, start);
70    let Some(enclosing) = enclosing_context(node, ctx).enclosing.clone() else {
71        return;
72    };
73    if ctx.target_group.contains(&enclosing) {
74        return;
75    }
76    if enclosing == ctx.spec.target || same_logical_symbol(&enclosing, &ctx.spec.target) {
77        return;
78    }
79    let hit = usage_hit(
80        ctx.file,
81        line_idx,
82        start,
83        end,
84        enclosing,
85        snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
86    );
87    let hit = match kind {
88        UsageHitKind::Reference => hit,
89        UsageHitKind::Definition => hit.into_definition(),
90        UsageHitKind::Import
91        | UsageHitKind::Reexport
92        | UsageHitKind::SelfReceiver
93        | UsageHitKind::OverrideDeclaration => {
94            unreachable!("unsupported unproven C++ hit emission kind: {kind:?}")
95        }
96    };
97    ctx.unproven_hits.insert(hit.into_unproven());
98}
99
100fn push_hit_with_options(
101    node: Node<'_>,
102    ctx: &mut ScanCtx<'_>,
103    allow_logical_target_enclosing: bool,
104    kind: UsageHitKind,
105    allow_inside_target_declaration: bool,
106) {
107    if *ctx.limit_exceeded {
108        return;
109    }
110    let start = node.start_byte();
111    if is_member_field_own_declarator(node, ctx) {
112        return;
113    }
114    let inside_target_declaration =
115        !allow_inside_target_declaration && is_inside_target_declaration(node, ctx);
116    let line_idx = find_line_index_for_offset(ctx.line_starts, start);
117    let Some(enclosing) = enclosing_context(node, ctx).enclosing.clone() else {
118        return;
119    };
120    // A reference whose enclosing declaration is the target itself is a
121    // recursive call (#1638). When the target is declared and defined in one
122    // place the site sits inside the target's own declaration range, which is
123    // why it has to be decided before that range is consulted. The declared
124    // name itself is excluded structurally, through the declarator chain, so
125    // the declaration does not become a usage of itself. `SelfReceiver` gives
126    // the same contract as [`push_recursive_reference_hit`]: editor-visible,
127    // absent from the external usage surface.
128    if matches!(kind, UsageHitKind::Reference | UsageHitKind::SelfReceiver)
129        && ctx.spec.target.is_function()
130        && enclosing == ctx.spec.target
131        && !is_target_declaration_name(node, ctx)
132    {
133        insert_hit(node, ctx, enclosing, line_idx, UsageHitKind::SelfReceiver);
134        return;
135    }
136    if inside_target_declaration {
137        return;
138    }
139    if ctx.target_group.contains(&enclosing) {
140        return;
141    }
142    if enclosing == ctx.spec.target
143        || (!allow_logical_target_enclosing && same_logical_symbol(&enclosing, &ctx.spec.target))
144    {
145        return;
146    }
147    insert_hit(node, ctx, enclosing, line_idx, kind);
148}
149
150fn insert_hit(
151    node: Node<'_>,
152    ctx: &mut ScanCtx<'_>,
153    enclosing: CodeUnit,
154    line_idx: usize,
155    kind: UsageHitKind,
156) {
157    let hit = usage_hit(
158        ctx.file,
159        line_idx,
160        node.start_byte(),
161        node.end_byte(),
162        enclosing,
163        snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
164    );
165    let hit = match kind {
166        UsageHitKind::Reference => hit,
167        UsageHitKind::SelfReceiver => hit.into_self_receiver(),
168        UsageHitKind::Definition => hit.into_definition(),
169        UsageHitKind::Import | UsageHitKind::Reexport | UsageHitKind::OverrideDeclaration => {
170            unreachable!("unsupported C++ hit emission kind: {kind:?}")
171        }
172    };
173    ctx.hits.insert(hit);
174    if kind.included_in(UsageHitSurface::ExternalUsages)
175        && ctx
176            .hits
177            .iter()
178            .filter(|hit| hit.kind.included_in(UsageHitSurface::ExternalUsages))
179            .count()
180            > ctx.max_usages
181    {
182        *ctx.limit_exceeded = true;
183    }
184}
185
186pub fn enclosing_context(node: Node<'_>, ctx: &ScanCtx<'_>) -> EnclosingContext {
187    let key = (node.start_byte(), node.end_byte());
188    if let Some(cached) = ctx.enclosing_cache.borrow().get(&key).cloned() {
189        return cached;
190    }
191    let range = Range {
192        start_byte: node.start_byte(),
193        end_byte: node.end_byte(),
194        start_line: find_line_index_for_offset(ctx.line_starts, node.start_byte()),
195        end_line: find_line_index_for_offset(ctx.line_starts, node.end_byte()),
196    };
197    let enclosing = ctx.analyzer.enclosing_code_unit(ctx.file, &range);
198    let owner = enclosing.as_ref().and_then(|enclosing| {
199        let cached = ctx.enclosing_owner_cache.borrow().get(enclosing).cloned();
200        if let Some(cached) = cached {
201            return cached;
202        }
203        let resolved = precise_parent_of(&ctx.analyzer, ctx.visibility, enclosing)
204            .or_else(|| visible_owner_from_member_name(ctx, enclosing));
205        ctx.enclosing_owner_cache
206            .borrow_mut()
207            .insert(enclosing.clone(), resolved.clone());
208        resolved
209    });
210    let context = EnclosingContext { enclosing, owner };
211    ctx.enclosing_cache
212        .borrow_mut()
213        .insert(key, context.clone());
214    context
215}
216
217/// Returns whether `node` is the target declaration's own declared name.
218///
219/// The declarator chain of a C++ declaration bottoms out at the declared name
220/// (`function_definition.declarator -> function_declarator.declarator ->
221/// identifier`), while parameters, default arguments, and the body hang off
222/// sibling fields. Containment in that terminal therefore covers a qualified
223/// out-of-line name (`void Foo::target()`) without also covering a call written
224/// in a default argument.
225fn is_target_declaration_name(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
226    let mut current = Some(node);
227    while let Some(candidate) = current {
228        if ctx.target_declaration_ranges.iter().any(|range| {
229            candidate.start_byte() == range.start_byte && candidate.end_byte() == range.end_byte
230        }) {
231            let mut declarator = candidate;
232            while let Some(inner) = declarator.child_by_field_name("declarator") {
233                declarator = inner;
234            }
235            return node.start_byte() >= declarator.start_byte()
236                && node.end_byte() <= declarator.end_byte();
237        }
238        current = candidate.parent();
239    }
240    false
241}
242
243fn is_inside_target_declaration(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
244    ctx.target_declaration_ranges
245        .iter()
246        .any(|range| node.start_byte() >= range.start_byte && node.end_byte() <= range.end_byte)
247}
248
249/// Returns whether `node` is on the declared-name path of a class field.
250///
251/// A `field_declaration` also owns default member initializers and, for method
252/// declarations, parameter default values. Those subtrees contain genuine
253/// references and must not be discarded with the declaration's own name.
254pub fn is_member_field_own_declarator(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
255    if !matches!(ctx.spec.kind, TargetKind::MemberField) {
256        return false;
257    }
258    let mut current = node.parent();
259    while let Some(parent) = current {
260        if parent.kind() == "field_declaration" {
261            let mut cursor = parent.walk();
262            return parent
263                .children_by_field_name("declarator", &mut cursor)
264                .any(|mut declarator| {
265                    while let Some(inner) = declarator.child_by_field_name("declarator") {
266                        declarator = inner;
267                    }
268                    node.start_byte() >= declarator.start_byte()
269                        && node.end_byte() <= declarator.end_byte()
270                });
271        }
272        if matches!(parent.kind(), "compound_statement" | "function_definition") {
273            return false;
274        }
275        current = parent.parent();
276    }
277    false
278}