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 receiver_context: Vec::new(),
83 exits: Vec::new(),
84 };
85 walk_tree_iterative(
86 root,
87 &mut state,
88 |node, state| state.enter(node),
89 |state| state.exit(),
90 );
91 }
92
93 fn record(&mut self, callee: String, node: Node<'_>) {
94 let range = ruby_semantic_identifier_range(node, self.source);
95 self.edges.record_kind(
96 self.input,
97 callee,
98 classify_reference_node(node),
99 range.start_byte,
100 range.end_byte,
101 );
102 }
103
104 fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
105 let range = ruby_semantic_identifier_range(node, self.source);
106 self.edges
107 .record_unproven_name(self.input, name, range.start_byte, range.end_byte);
108 }
109}
110
111enum RubyEdgeExit {
112 Type { has_lexical_owner: bool },
113 Method,
114 SingletonClass,
115 LocalScope,
116}
117
118struct RubyEdgeWalkState<'scan, 'ctx> {
119 scan: &'scan mut RubyEdgeScan<'ctx>,
120 locals: LocalInferenceEngine<String>,
121 lexical_stack: Vec<String>,
122 method_stack: Vec<ReceiverMode>,
123 receiver_context: Vec<ReceiverMode>,
124 exits: Vec<RubyEdgeExit>,
125}
126
127impl RubyEdgeWalkState<'_, '_> {
128 fn enter(&mut self, node: Node<'_>) -> TreeWalkAction {
129 match node.kind() {
130 "class" | "module" => {
131 self.record_superclass_reference(node);
132 let owner = self.type_owner(node);
133 if let Some(owner) = owner.as_ref() {
134 self.lexical_stack.push(owner.clone());
135 }
136 self.receiver_context.push(ReceiverMode::Instance);
137 self.exits.push(RubyEdgeExit::Type {
138 has_lexical_owner: owner.is_some(),
139 });
140 self.record_reference(node);
141 return TreeWalkAction::DescendWithExit;
142 }
143 "method" | "singleton_method" => {
144 self.locals.enter_scope();
145 self.seed_parameter_shadows(node);
146 self.method_stack.push(method_receiver_mode(
147 node,
148 self.receiver_context.last().copied(),
149 ));
150 self.exits.push(RubyEdgeExit::Method);
151 return TreeWalkAction::DescendWithExit;
152 }
153 "singleton_class" => {
154 self.locals.enter_scope();
155 self.method_stack.push(ReceiverMode::Class);
156 self.receiver_context.push(ReceiverMode::Class);
157 self.exits.push(RubyEdgeExit::SingletonClass);
158 return TreeWalkAction::DescendWithExit;
159 }
160 "block" | "do_block" => {
161 self.locals.enter_scope();
162 self.exits.push(RubyEdgeExit::LocalScope);
163 return TreeWalkAction::DescendWithExit;
164 }
165 "assignment" => self.seed_assignment(node),
166 _ => {}
167 }
168 self.record_reference(node);
169 TreeWalkAction::Descend
170 }
171
172 fn exit(&mut self) {
173 match self.exits.pop() {
174 Some(RubyEdgeExit::Type { has_lexical_owner }) => {
175 assert!(
176 matches!(self.receiver_context.pop(), Some(ReceiverMode::Instance)),
177 "type receiver context must match its exit"
178 );
179 if has_lexical_owner {
180 self.lexical_stack.pop();
181 }
182 }
183 Some(RubyEdgeExit::Method) => {
184 self.method_stack.pop();
185 self.locals.exit_scope();
186 }
187 Some(RubyEdgeExit::SingletonClass) => {
188 assert!(
189 matches!(self.receiver_context.pop(), Some(ReceiverMode::Class)),
190 "singleton-class receiver context must match its exit"
191 );
192 self.method_stack.pop();
193 self.locals.exit_scope();
194 }
195 Some(RubyEdgeExit::LocalScope) => {
196 self.locals.exit_scope();
197 }
198 None => {}
199 }
200 }
201
202 fn type_owner(&self, node: Node<'_>) -> Option<String> {
203 ruby_type_owner(
204 self.scan.semantic,
205 self.scan.file,
206 &self.scan.visible_files,
207 &self.lexical_stack,
208 node,
209 self.scan.source,
210 )
211 }
212
213 fn record_reference(&mut self, node: Node<'_>) {
214 self.record_constant_reference(node);
215 self.record_method_reference(node);
216 }
217
218 fn record_superclass_reference(&mut self, node: Node<'_>) {
219 let Some(superclass) = node.child_by_field_name("superclass") else {
220 return;
221 };
222 let mut stack = vec![superclass];
223 while let Some(current) = stack.pop() {
224 self.record_constant_reference(current);
225 for index in (0..current.named_child_count()).rev() {
226 if let Some(child) = current.named_child(index) {
227 stack.push(child);
228 }
229 }
230 }
231 }
232
233 fn record_constant_reference(&mut self, node: Node<'_>) {
234 if crate::imports::is_ruby_autoload_symbol_argument(node, self.scan.source) {
235 self.record_autoload_symbol_constant_reference(node);
236 return;
237 }
238 if !matches!(node.kind(), "constant" | "scope_resolution") || is_declaration_constant(node)
239 {
240 return;
241 }
242 if let Some(unit) = self.scan.semantic.resolve_constant(
243 self.scan.file,
244 &self.scan.visible_files,
245 &self.lexical_stack,
246 node,
247 self.scan.source,
248 ) && (unit.is_class() || unit.is_module())
249 {
250 self.scan.record(unit.fq_name(), node);
251 }
252 }
253
254 fn record_autoload_symbol_constant_reference(&mut self, node: Node<'_>) {
255 let Some(name) = crate::imports::ruby_symbol_name(node, self.scan.source) else {
256 return;
257 };
258 if let Some(unit) = self.scan.semantic.resolve_constant_name(
259 self.scan.file,
260 &self.scan.visible_files,
261 &self.lexical_stack,
262 &name,
263 ) && (unit.is_class() || unit.is_module())
264 {
265 self.scan.record(unit.fq_name(), node);
266 }
267 }
268
269 fn record_method_reference(&mut self, node: Node<'_>) {
270 if node.kind() == "identifier" {
271 self.record_bare_identifier_method_reference(node);
272 return;
273 }
274 if node.kind() != "call" {
275 return;
276 }
277 let Some(method) = node.child_by_field_name("method") else {
278 return;
279 };
280 let member = node_text(method, self.scan.source);
281 if member.is_empty() {
282 return;
283 }
284 if let Some((dispatched_member, dispatched_node)) =
285 dynamic_dispatch_target_argument(node, self.scan.source)
286 {
287 self.record_call_method_reference(
288 node,
289 &dispatched_member,
290 dispatched_node,
291 MethodLookup::Explicit,
292 );
293 return;
294 }
295 self.record_call_method_reference(
296 node,
297 member,
298 method,
299 if node.child_by_field_name("receiver").is_some() {
300 MethodLookup::Explicit
301 } else {
302 MethodLookup::Bare
303 },
304 );
305 }
306
307 fn record_call_method_reference(
308 &mut self,
309 node: Node<'_>,
310 member: &str,
311 hit_node: Node<'_>,
312 lookup: MethodLookup,
313 ) {
314 let receiver_node = node.child_by_field_name("receiver");
315 let same_owner = match receiver_node {
319 None => true,
320 Some(receiver) => receiver.kind() == "self",
321 };
322 let receiver = match receiver_node {
323 Some(receiver) => self.receiver_type(receiver),
324 None => self.enclosing_receiver(node.start_byte()),
325 };
326 let Some(receiver) = receiver else {
327 self.scan.record_unproven_name(member, hit_node);
328 return;
329 };
330 if member == "new" && receiver.mode == ReceiverMode::Class {
331 self.record_unique_method_candidate(
332 self.initialize_receiver(&receiver),
333 "initialize",
334 hit_node,
335 MethodLookup::Explicit,
336 same_owner,
337 );
338 return;
339 }
340 self.record_unique_method_candidate(receiver, member, hit_node, lookup, same_owner);
341 }
342
343 fn record_bare_identifier_method_reference(&mut self, node: Node<'_>) {
344 let name = node_text(node, self.scan.source);
345 if name.is_empty()
346 || self.locals.is_shadowed(name)
347 || is_declaration_identifier(node)
348 || is_call_method_identifier(node)
349 {
350 return;
351 }
352 let Some(receiver) = self.enclosing_receiver(node.start_byte()) else {
353 return;
354 };
355 self.record_unique_method_candidate(receiver, name, node, MethodLookup::Bare, true);
358 }
359
360 fn record_unique_method_candidate(
361 &mut self,
362 receiver: ReceiverType,
363 member: &str,
364 node: Node<'_>,
365 lookup: MethodLookup,
366 same_owner: bool,
367 ) {
368 if same_owner {
374 self.scan.record_unproven_name(member, node);
375 return;
376 }
377 let candidates = match lookup {
378 MethodLookup::Bare => self.scan.semantic.resolve_bare_method_candidates(
379 self.scan.support,
380 &self.scan.visible_files,
381 &receiver,
382 member,
383 ),
384 MethodLookup::Explicit => self.scan.semantic.resolve_method_candidates(
385 self.scan.support,
386 &self.scan.visible_files,
387 &receiver,
388 member,
389 ),
390 };
391 if let Some(fqn) = unique_candidate_fqn(candidates) {
392 self.scan.record(fqn, node);
393 } else {
394 self.scan.record_unproven_name(member, node);
395 }
396 }
397
398 fn receiver_type(&self, node: Node<'_>) -> Option<ReceiverType> {
399 ruby_receiver_type(
400 self.scan.semantic,
401 self.scan.file,
402 &self.scan.visible_files,
403 &self.lexical_stack,
404 &self.locals,
405 &self.method_stack,
406 node,
407 self.scan.source,
408 )
409 }
410
411 fn enclosing_receiver(&self, byte: usize) -> Option<ReceiverType> {
412 ruby_enclosing_receiver(&self.lexical_stack, &self.method_stack).or_else(|| {
413 self.scan
414 .class_ranges
415 .enclosing(byte)
416 .map(|owner_fq_name| ReceiverType {
417 owner_fq_name: owner_fq_name.to_string(),
418 mode: ReceiverMode::Instance,
419 })
420 })
421 }
422
423 fn initialize_receiver(&self, receiver: &ReceiverType) -> ReceiverType {
424 ReceiverType {
425 owner_fq_name: receiver.owner_fq_name.clone(),
426 mode: ReceiverMode::Instance,
427 }
428 }
429
430 fn seed_assignment(&mut self, node: Node<'_>) {
431 ruby_seed_assignment(
432 self.scan.semantic,
433 self.scan.file,
434 &self.scan.visible_files,
435 &self.lexical_stack,
436 &self.method_stack,
437 &mut self.locals,
438 node,
439 self.scan.source,
440 );
441 }
442
443 fn seed_parameter_shadows(&mut self, node: Node<'_>) {
444 ruby_seed_parameter_shadows(&mut self.locals, node, self.scan.source);
445 }
446}
447
448#[derive(Clone, Copy)]
449enum MethodLookup {
450 Bare,
451 Explicit,
452}
453
454fn unique_candidate_fqn(candidates: Vec<CodeUnit>) -> Option<String> {
455 if candidates.len() == 1 {
456 candidates
457 .into_iter()
458 .next()
459 .map(|candidate| candidate.fq_name())
460 } else {
461 None
462 }
463}