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_inside_target_declaration(node, ctx) || 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) {
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 (!allow_inside_target_declaration && is_inside_target_declaration(node, ctx))
112        || is_member_field_own_declarator(node, ctx)
113    {
114        return;
115    }
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    if ctx.target_group.contains(&enclosing) {
121        return;
122    }
123    if enclosing == ctx.spec.target
124        || (!allow_logical_target_enclosing && same_logical_symbol(&enclosing, &ctx.spec.target))
125    {
126        return;
127    }
128    insert_hit(node, ctx, enclosing, line_idx, kind);
129}
130
131fn insert_hit(
132    node: Node<'_>,
133    ctx: &mut ScanCtx<'_>,
134    enclosing: CodeUnit,
135    line_idx: usize,
136    kind: UsageHitKind,
137) {
138    let hit = usage_hit(
139        ctx.file,
140        line_idx,
141        node.start_byte(),
142        node.end_byte(),
143        enclosing,
144        snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
145    );
146    let hit = match kind {
147        UsageHitKind::Reference => hit,
148        UsageHitKind::SelfReceiver => hit.into_self_receiver(),
149        UsageHitKind::Definition => hit.into_definition(),
150        UsageHitKind::Import | UsageHitKind::Reexport | UsageHitKind::OverrideDeclaration => {
151            unreachable!("unsupported C++ hit emission kind: {kind:?}")
152        }
153    };
154    ctx.hits.insert(hit);
155    if kind.included_in(UsageHitSurface::ExternalUsages)
156        && ctx
157            .hits
158            .iter()
159            .filter(|hit| hit.kind.included_in(UsageHitSurface::ExternalUsages))
160            .count()
161            > ctx.max_usages
162    {
163        *ctx.limit_exceeded = true;
164    }
165}
166
167pub fn enclosing_context(node: Node<'_>, ctx: &ScanCtx<'_>) -> EnclosingContext {
168    let key = (node.start_byte(), node.end_byte());
169    if let Some(cached) = ctx.enclosing_cache.borrow().get(&key).cloned() {
170        return cached;
171    }
172    let range = Range {
173        start_byte: node.start_byte(),
174        end_byte: node.end_byte(),
175        start_line: find_line_index_for_offset(ctx.line_starts, node.start_byte()),
176        end_line: find_line_index_for_offset(ctx.line_starts, node.end_byte()),
177    };
178    let enclosing = ctx.analyzer.enclosing_code_unit(ctx.file, &range);
179    let owner = enclosing.as_ref().and_then(|enclosing| {
180        let cached = ctx.enclosing_owner_cache.borrow().get(enclosing).cloned();
181        if let Some(cached) = cached {
182            return cached;
183        }
184        let resolved = precise_parent_of(&ctx.analyzer, ctx.visibility, enclosing)
185            .or_else(|| visible_owner_from_member_name(ctx, enclosing));
186        ctx.enclosing_owner_cache
187            .borrow_mut()
188            .insert(enclosing.clone(), resolved.clone());
189        resolved
190    });
191    let context = EnclosingContext { enclosing, owner };
192    ctx.enclosing_cache
193        .borrow_mut()
194        .insert(key, context.clone());
195    context
196}
197
198fn is_inside_target_declaration(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
199    ctx.target_declaration_ranges
200        .iter()
201        .any(|range| node.start_byte() >= range.start_byte && node.end_byte() <= range.end_byte)
202}
203
204/// Returns whether `node` is on the declared-name path of a class field.
205///
206/// A `field_declaration` also owns default member initializers and, for method
207/// declarations, parameter default values. Those subtrees contain genuine
208/// references and must not be discarded with the declaration's own name.
209pub fn is_member_field_own_declarator(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
210    if !matches!(ctx.spec.kind, TargetKind::MemberField) {
211        return false;
212    }
213    let mut current = node.parent();
214    while let Some(parent) = current {
215        if parent.kind() == "field_declaration" {
216            let mut cursor = parent.walk();
217            return parent
218                .children_by_field_name("declarator", &mut cursor)
219                .any(|mut declarator| {
220                    while let Some(inner) = declarator.child_by_field_name("declarator") {
221                        declarator = inner;
222                    }
223                    node.start_byte() >= declarator.start_byte()
224                        && node.end_byte() <= declarator.end_byte()
225                });
226        }
227        if matches!(parent.kind(), "compound_statement" | "function_definition") {
228            return false;
229        }
230        current = parent.parent();
231    }
232    false
233}