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
27pub 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 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 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
217fn 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
249pub 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}