1use crate::graph::RubyGraphSource;
13use crate::graph::extractor::{
14 ruby_enclosing_receiver, ruby_receiver_type, ruby_seed_assignment, ruby_seed_parameter_shadows,
15 ruby_type_owner,
16};
17use crate::graph::resolver::{ReceiverMode, ReceiverType, RubySemanticIndex};
18use crate::graph::syntax::{
19 dynamic_dispatch_target_argument, is_call_method_identifier, is_declaration_constant,
20 is_declaration_identifier, method_receiver_mode, node_text,
21};
22use crate::graph_support::RubySource;
23use crate::syntax::ruby_semantic_identifier_range;
24use brokk_bifrost_core::analyzer::tree_walk::{TreeWalkAction, walk_tree_iterative};
25use brokk_bifrost_core::analyzer::usages::inverted_edges::{
26 ClassRangeIndex, FileEdgeScanInput, PerFileEdges, classify_reference_node,
27};
28use brokk_bifrost_core::analyzer::usages::local_inference::{
29 LocalInferenceConfig, LocalInferenceEngine,
30};
31use brokk_bifrost_core::analyzer::{BoundedDefinitionLookup, CodeUnit, ProjectFile};
32use brokk_bifrost_core::hash::HashSet;
33use tree_sitter::Node;
34
35pub fn scan_file(
42 graph: RubyGraphSource<'_>,
43 ruby: &dyn RubySource,
44 support: &dyn BoundedDefinitionLookup,
45 file: &ProjectFile,
46 input: &FileEdgeScanInput<'_>,
47) -> PerFileEdges {
48 let semantic = RubySemanticIndex::build_for_lookup(graph, ruby);
49 let visible_files = semantic.visible_files_from(file);
50 let mut scan = RubyEdgeScan {
51 semantic: &semantic,
52 support,
53 file,
54 source: input.source,
55 visible_files,
56 class_ranges: ClassRangeIndex::build(graph.index, file),
57 input,
58 edges: PerFileEdges::default(),
59 };
60 scan.scan(input.root());
61 scan.edges
62}
63
64struct RubyEdgeScan<'a> {
65 semantic: &'a RubySemanticIndex<'a>,
66 support: &'a dyn BoundedDefinitionLookup,
67 file: &'a ProjectFile,
68 source: &'a str,
69 visible_files: HashSet<ProjectFile>,
70 class_ranges: ClassRangeIndex,
71 input: &'a FileEdgeScanInput<'a>,
72 edges: PerFileEdges,
73}
74
75impl RubyEdgeScan<'_> {
76 fn scan(&mut self, root: Node<'_>) {
77 let mut state = RubyEdgeWalkState {
78 scan: self,
79 locals: LocalInferenceEngine::new(LocalInferenceConfig::default()),
80 lexical_stack: Vec::new(),
81 method_stack: Vec::new(),
82 exits: Vec::new(),
83 };
84 walk_tree_iterative(
85 root,
86 &mut state,
87 |node, state| state.enter(node),
88 |state| state.exit(),
89 );
90 }
91
92 fn record(&mut self, callee: String, node: Node<'_>) {
93 let range = ruby_semantic_identifier_range(node, self.source);
94 self.edges.record_kind(
95 self.input,
96 callee,
97 classify_reference_node(node),
98 range.start_byte,
99 range.end_byte,
100 );
101 }
102
103 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
104 let range = ruby_semantic_identifier_range(node, self.source);
105 self.edges
106 .record_unproven_name(self.input, name, range.start_byte, range.end_byte);
107 }
108}
109
110enum RubyEdgeExit {
111 Lexical,
112 Method,
113 LocalScope,
114}
115
116struct RubyEdgeWalkState<'scan, 'ctx> {
117 scan: &'scan mut RubyEdgeScan<'ctx>,
118 locals: LocalInferenceEngine<String>,
119 lexical_stack: Vec<String>,
120 method_stack: Vec<ReceiverMode>,
121 exits: Vec<RubyEdgeExit>,
122}
123
124impl RubyEdgeWalkState<'_, '_> {
125 fn enter(&mut self, node: Node<'_>) -> TreeWalkAction {
126 match node.kind() {
127 "class" | "module" => {
128 self.record_superclass_reference(node);
129 if let Some(owner) = self.type_owner(node) {
130 self.lexical_stack.push(owner);
131 self.exits.push(RubyEdgeExit::Lexical);
132 self.record_reference(node);
133 return TreeWalkAction::DescendWithExit;
134 }
135 }
136 "method" | "singleton_method" => {
137 self.locals.enter_scope();
138 self.seed_parameter_shadows(node);
139 self.method_stack.push(method_receiver_mode(node));
140 self.exits.push(RubyEdgeExit::Method);
141 return TreeWalkAction::DescendWithExit;
142 }
143 "singleton_class" => {
144 self.locals.enter_scope();
145 self.method_stack.push(ReceiverMode::Class);
146 self.exits.push(RubyEdgeExit::Method);
147 return TreeWalkAction::DescendWithExit;
148 }
149 "block" | "do_block" => {
150 self.locals.enter_scope();
151 self.exits.push(RubyEdgeExit::LocalScope);
152 return TreeWalkAction::DescendWithExit;
153 }
154 "assignment" => self.seed_assignment(node),
155 _ => {}
156 }
157 self.record_reference(node);
158 TreeWalkAction::Descend
159 }
160
161 fn exit(&mut self) {
162 match self.exits.pop() {
163 Some(RubyEdgeExit::Lexical) => {
164 self.lexical_stack.pop();
165 }
166 Some(RubyEdgeExit::Method) => {
167 self.method_stack.pop();
168 self.locals.exit_scope();
169 }
170 Some(RubyEdgeExit::LocalScope) => {
171 self.locals.exit_scope();
172 }
173 None => {}
174 }
175 }
176
177 fn type_owner(&self, node: Node<'_>) -> Option<String> {
178 ruby_type_owner(
179 self.scan.semantic,
180 self.scan.file,
181 &self.scan.visible_files,
182 &self.lexical_stack,
183 node,
184 self.scan.source,
185 )
186 }
187
188 fn record_reference(&mut self, node: Node<'_>) {
189 self.record_constant_reference(node);
190 self.record_method_reference(node);
191 }
192
193 fn record_superclass_reference(&mut self, node: Node<'_>) {
194 let Some(superclass) = node.child_by_field_name("superclass") else {
195 return;
196 };
197 let mut stack = vec![superclass];
198 while let Some(current) = stack.pop() {
199 self.record_constant_reference(current);
200 for index in (0..current.named_child_count()).rev() {
201 if let Some(child) = current.named_child(index) {
202 stack.push(child);
203 }
204 }
205 }
206 }
207
208 fn record_constant_reference(&mut self, node: Node<'_>) {
209 if crate::imports::is_ruby_autoload_symbol_argument(node, self.scan.source) {
210 self.record_autoload_symbol_constant_reference(node);
211 return;
212 }
213 if !matches!(node.kind(), "constant" | "scope_resolution") || is_declaration_constant(node)
214 {
215 return;
216 }
217 if let Some(unit) = self.scan.semantic.resolve_constant(
218 self.scan.file,
219 &self.scan.visible_files,
220 &self.lexical_stack,
221 node,
222 self.scan.source,
223 ) && (unit.is_class() || unit.is_module())
224 {
225 self.scan.record(unit.fq_name(), node);
226 }
227 }
228
229 fn record_autoload_symbol_constant_reference(&mut self, node: Node<'_>) {
230 let Some(name) = crate::imports::ruby_symbol_name(node, self.scan.source) else {
231 return;
232 };
233 if let Some(unit) = self.scan.semantic.resolve_constant_name(
234 self.scan.file,
235 &self.scan.visible_files,
236 &self.lexical_stack,
237 &name,
238 ) && (unit.is_class() || unit.is_module())
239 {
240 self.scan.record(unit.fq_name(), node);
241 }
242 }
243
244 fn record_method_reference(&mut self, node: Node<'_>) {
245 if node.kind() == "identifier" {
246 self.record_bare_identifier_method_reference(node);
247 return;
248 }
249 if node.kind() != "call" {
250 return;
251 }
252 let Some(method) = node.child_by_field_name("method") else {
253 return;
254 };
255 let member = node_text(method, self.scan.source);
256 if member.is_empty() {
257 return;
258 }
259 if let Some((dispatched_member, dispatched_node)) =
260 dynamic_dispatch_target_argument(node, self.scan.source)
261 {
262 self.record_call_method_reference(
263 node,
264 &dispatched_member,
265 dispatched_node,
266 MethodLookup::Explicit,
267 );
268 return;
269 }
270 self.record_call_method_reference(
271 node,
272 member,
273 method,
274 if node.child_by_field_name("receiver").is_some() {
275 MethodLookup::Explicit
276 } else {
277 MethodLookup::Bare
278 },
279 );
280 }
281
282 fn record_call_method_reference(
283 &mut self,
284 node: Node<'_>,
285 member: &str,
286 hit_node: Node<'_>,
287 lookup: MethodLookup,
288 ) {
289 let receiver_node = node.child_by_field_name("receiver");
290 let same_owner = match receiver_node {
294 None => true,
295 Some(receiver) => receiver.kind() == "self",
296 };
297 let receiver = match receiver_node {
298 Some(receiver) => self.receiver_type(receiver),
299 None => self.enclosing_receiver(node.start_byte()),
300 };
301 let Some(receiver) = receiver else {
302 self.scan.record_unproven_name(member, hit_node);
303 return;
304 };
305 if member == "new" && receiver.mode == ReceiverMode::Class {
306 self.record_unique_method_candidate(
307 self.initialize_receiver(&receiver),
308 "initialize",
309 hit_node,
310 MethodLookup::Explicit,
311 same_owner,
312 );
313 return;
314 }
315 self.record_unique_method_candidate(receiver, member, hit_node, lookup, same_owner);
316 }
317
318 fn record_bare_identifier_method_reference(&mut self, node: Node<'_>) {
319 let name = node_text(node, self.scan.source);
320 if name.is_empty()
321 || self.locals.is_shadowed(name)
322 || is_declaration_identifier(node)
323 || is_call_method_identifier(node)
324 {
325 return;
326 }
327 let Some(receiver) = self.enclosing_receiver(node.start_byte()) else {
328 return;
329 };
330 self.record_unique_method_candidate(receiver, name, node, MethodLookup::Bare, true);
333 }
334
335 fn record_unique_method_candidate(
336 &mut self,
337 receiver: ReceiverType,
338 member: &str,
339 node: Node<'_>,
340 lookup: MethodLookup,
341 same_owner: bool,
342 ) {
343 if same_owner {
349 self.scan.record_unproven_name(member, node);
350 return;
351 }
352 let candidates = match lookup {
353 MethodLookup::Bare => self.scan.semantic.resolve_bare_method_candidates(
354 self.scan.support,
355 &self.scan.visible_files,
356 &receiver,
357 member,
358 ),
359 MethodLookup::Explicit => self.scan.semantic.resolve_method_candidates(
360 self.scan.support,
361 &self.scan.visible_files,
362 &receiver,
363 member,
364 ),
365 };
366 if let Some(fqn) = unique_candidate_fqn(candidates) {
367 self.scan.record(fqn, node);
368 } else {
369 self.scan.record_unproven_name(member, node);
370 }
371 }
372
373 fn receiver_type(&self, node: Node<'_>) -> Option<ReceiverType> {
374 ruby_receiver_type(
375 self.scan.semantic,
376 self.scan.file,
377 &self.scan.visible_files,
378 &self.lexical_stack,
379 &self.locals,
380 &self.method_stack,
381 node,
382 self.scan.source,
383 )
384 }
385
386 fn enclosing_receiver(&self, byte: usize) -> Option<ReceiverType> {
387 ruby_enclosing_receiver(&self.lexical_stack, &self.method_stack).or_else(|| {
388 self.scan
389 .class_ranges
390 .enclosing(byte)
391 .map(|owner_fq_name| ReceiverType {
392 owner_fq_name: owner_fq_name.to_string(),
393 mode: ReceiverMode::Instance,
394 })
395 })
396 }
397
398 fn initialize_receiver(&self, receiver: &ReceiverType) -> ReceiverType {
399 ReceiverType {
400 owner_fq_name: receiver.owner_fq_name.clone(),
401 mode: ReceiverMode::Instance,
402 }
403 }
404
405 fn seed_assignment(&mut self, node: Node<'_>) {
406 ruby_seed_assignment(
407 self.scan.semantic,
408 self.scan.file,
409 &self.scan.visible_files,
410 &self.lexical_stack,
411 &self.method_stack,
412 &mut self.locals,
413 node,
414 self.scan.source,
415 );
416 }
417
418 fn seed_parameter_shadows(&mut self, node: Node<'_>) {
419 ruby_seed_parameter_shadows(&mut self.locals, node, self.scan.source);
420 }
421}
422
423#[derive(Clone, Copy)]
424enum MethodLookup {
425 Bare,
426 Explicit,
427}
428
429fn unique_candidate_fqn(candidates: Vec<CodeUnit>) -> Option<String> {
430 if candidates.len() == 1 {
431 candidates
432 .into_iter()
433 .next()
434 .map(|candidate| candidate.fq_name())
435 } else {
436 None
437 }
438}