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_proven_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_proven_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
40pub 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
72pub 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
100pub 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_declared_reference_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
123 if *ctx.limit_exceeded {
124 return;
125 }
126 let line_idx = find_line_index_for_offset(ctx.line_starts, node.start_byte());
127 let hit = usage_hit(
128 ctx.file,
129 line_idx,
130 node.start_byte(),
131 node.end_byte(),
132 ctx.spec.target.clone(),
133 snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
134 )
135 .into_declared_reference();
136 ctx.hits.insert(hit);
137}
138
139pub fn push_recovered_definition_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
144 if *ctx.limit_exceeded {
145 return;
146 }
147 let line_idx = find_line_index_for_offset(ctx.line_starts, node.start_byte());
148 let hit = usage_hit(
149 ctx.file,
150 line_idx,
151 node.start_byte(),
152 node.end_byte(),
153 ctx.spec.target.clone(),
154 snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
155 )
156 .into_definition();
157 ctx.hits.insert(hit);
158}
159
160pub fn push_unproven_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
161 push_unproven_hit_with_kind(node, ctx, UsageHitKind::Reference);
162}
163
164pub fn push_unproven_definition_hit(node: Node<'_>, ctx: &mut ScanCtx<'_>) {
165 push_unproven_hit_with_kind(node, ctx, UsageHitKind::Definition);
166}
167
168fn push_unproven_hit_with_kind(node: Node<'_>, ctx: &mut ScanCtx<'_>, kind: UsageHitKind) {
169 push_unproven_hit_range(node, node.start_byte(), node.end_byte(), ctx, kind);
170}
171
172fn push_unproven_hit_range(
173 anchor: Node<'_>,
174 start: usize,
175 end: usize,
176 ctx: &mut ScanCtx<'_>,
177 kind: UsageHitKind,
178) {
179 if is_inside_target_declaration(anchor, ctx) || is_member_field_own_declarator(anchor, ctx) {
180 return;
181 }
182 let line_idx = find_line_index_for_offset(ctx.line_starts, start);
183 let Some(enclosing) = enclosing_context(anchor, ctx).enclosing.clone() else {
184 return;
185 };
186 if ctx.target_group.contains(&enclosing) {
187 return;
188 }
189 if enclosing == ctx.spec.target || same_logical_symbol(&enclosing, &ctx.spec.target) {
190 return;
191 }
192 let hit = usage_hit(
193 ctx.file,
194 line_idx,
195 start,
196 end,
197 enclosing,
198 snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
199 );
200 let hit = match kind {
201 UsageHitKind::Reference => hit,
202 UsageHitKind::Definition => hit.into_definition(),
203 UsageHitKind::Import
204 | UsageHitKind::Reexport
205 | UsageHitKind::SelfReceiver
206 | UsageHitKind::DeclaredReference
207 | UsageHitKind::OverrideDeclaration => {
208 unreachable!("unsupported unproven C++ hit emission kind: {kind:?}")
209 }
210 };
211 ctx.unproven_hits.insert(hit.into_unproven());
212}
213
214fn push_hit_with_options(
215 node: Node<'_>,
216 ctx: &mut ScanCtx<'_>,
217 allow_logical_target_enclosing: bool,
218 kind: UsageHitKind,
219 allow_inside_target_declaration: bool,
220) {
221 push_hit_range_with_options(
222 node,
223 node.start_byte(),
224 node.end_byte(),
225 ctx,
226 allow_logical_target_enclosing,
227 kind,
228 allow_inside_target_declaration,
229 );
230}
231
232#[allow(clippy::too_many_arguments)]
233fn push_hit_range_with_options(
234 anchor: Node<'_>,
235 start: usize,
236 end: usize,
237 ctx: &mut ScanCtx<'_>,
238 allow_logical_target_enclosing: bool,
239 kind: UsageHitKind,
240 allow_inside_target_declaration: bool,
241) {
242 if *ctx.limit_exceeded {
243 return;
244 }
245 if is_member_field_own_declarator(anchor, ctx) {
246 return;
247 }
248 let inside_target_declaration =
249 !allow_inside_target_declaration && is_inside_target_declaration(anchor, ctx);
250 let line_idx = find_line_index_for_offset(ctx.line_starts, start);
251 let Some(enclosing) = enclosing_context(anchor, ctx).enclosing.clone() else {
252 return;
253 };
254 if matches!(kind, UsageHitKind::Reference | UsageHitKind::SelfReceiver)
263 && ctx.spec.target.is_function()
264 && enclosing == ctx.spec.target
265 && !is_target_declaration_name(anchor, ctx)
266 {
267 insert_hit_range(
268 start,
269 end,
270 ctx,
271 enclosing,
272 line_idx,
273 UsageHitKind::SelfReceiver,
274 );
275 return;
276 }
277 if inside_target_declaration {
278 return;
279 }
280 if ctx.target_group.contains(&enclosing) {
281 return;
282 }
283 if enclosing == ctx.spec.target
284 || (!allow_logical_target_enclosing && same_logical_symbol(&enclosing, &ctx.spec.target))
285 {
286 return;
287 }
288 insert_hit_range(start, end, ctx, enclosing, line_idx, kind);
289}
290
291fn insert_hit(
292 node: Node<'_>,
293 ctx: &mut ScanCtx<'_>,
294 enclosing: CodeUnit,
295 line_idx: usize,
296 kind: UsageHitKind,
297) {
298 insert_hit_range(
299 node.start_byte(),
300 node.end_byte(),
301 ctx,
302 enclosing,
303 line_idx,
304 kind,
305 );
306}
307
308fn insert_hit_range(
309 start: usize,
310 end: usize,
311 ctx: &mut ScanCtx<'_>,
312 enclosing: CodeUnit,
313 line_idx: usize,
314 kind: UsageHitKind,
315) {
316 let hit = usage_hit(
317 ctx.file,
318 line_idx,
319 start,
320 end,
321 enclosing,
322 snippet_around_line(ctx.source, ctx.line_starts, line_idx, SNIPPET_CONTEXT_LINES),
323 );
324 let hit = match kind {
325 UsageHitKind::Reference => hit,
326 UsageHitKind::SelfReceiver => hit.into_self_receiver(),
327 UsageHitKind::Definition => hit.into_definition(),
328 UsageHitKind::Import | UsageHitKind::Reexport | UsageHitKind::OverrideDeclaration => {
329 unreachable!("unsupported C++ hit emission kind: {kind:?}")
330 }
331 UsageHitKind::DeclaredReference => hit.into_declared_reference(),
332 };
333 if ctx.hits.insert(hit) && kind.included_in(UsageHitSurface::ExternalUsages) {
334 ctx.external_hit_count += 1;
335 if ctx.external_hit_count > ctx.max_usages {
336 *ctx.limit_exceeded = true;
337 }
338 }
339}
340
341pub fn enclosing_context(node: Node<'_>, ctx: &ScanCtx<'_>) -> EnclosingContext {
342 let key = (node.start_byte(), node.end_byte());
343 if let Some(cached) = ctx.enclosing_cache.borrow().get(&key).cloned() {
344 return cached;
345 }
346 let range = Range {
347 start_byte: node.start_byte(),
348 end_byte: node.end_byte(),
349 start_line: find_line_index_for_offset(ctx.line_starts, node.start_byte()),
350 end_line: find_line_index_for_offset(ctx.line_starts, node.end_byte()),
351 };
352 let enclosing = ctx.analyzer.enclosing_code_unit(ctx.file, &range);
353 let owner = enclosing.as_ref().and_then(|enclosing| {
354 let cached = ctx.enclosing_owner_cache.borrow().get(enclosing).cloned();
355 if let Some(cached) = cached {
356 return cached;
357 }
358 let resolved = precise_parent_of(&ctx.analyzer, ctx.visibility, enclosing)
359 .or_else(|| visible_owner_from_member_name(ctx, enclosing));
360 ctx.enclosing_owner_cache
361 .borrow_mut()
362 .insert(enclosing.clone(), resolved.clone());
363 resolved
364 });
365 let context = EnclosingContext { enclosing, owner };
366 ctx.enclosing_cache
367 .borrow_mut()
368 .insert(key, context.clone());
369 context
370}
371
372fn is_target_declaration_name(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
382 let mut current = Some(node);
383 while let Some(candidate) = current {
384 if ctx.target_declaration_ranges.iter().any(|range| {
385 candidate.start_byte() == range.start_byte && candidate.end_byte() == range.end_byte
386 }) {
387 return declarator_name_node(candidate).is_some_and(|declarator| {
388 node.start_byte() >= declarator.start_byte()
389 && node.end_byte() <= declarator.end_byte()
390 });
391 }
392 current = candidate.parent();
393 }
394 false
395}
396
397fn is_inside_target_declaration(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
398 ctx.target_declaration_ranges
399 .iter()
400 .any(|range| node.start_byte() >= range.start_byte && node.end_byte() <= range.end_byte)
401}
402
403pub fn is_member_field_own_declarator(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
415 if !matches!(ctx.spec.kind, TargetKind::MemberField) {
416 return false;
417 }
418 let mut current = node.parent();
419 while let Some(parent) = current {
420 if parent.kind() == "field_declaration" {
421 let mut cursor = parent.walk();
422 return parent
423 .children_by_field_name("declarator", &mut cursor)
424 .filter_map(declarator_name_node)
425 .any(|declarator| {
426 node.start_byte() >= declarator.start_byte()
427 && node.end_byte() <= declarator.end_byte()
428 });
429 }
430 if matches!(parent.kind(), "compound_statement" | "function_definition") {
431 return false;
432 }
433 current = parent.parent();
434 }
435 false
436}