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
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_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 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
337fn 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
368pub 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}