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
43pub 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
71pub 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 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
304fn 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
336pub 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}