use crate::graph::RubyGraphSource;
use crate::graph::extractor::{
ruby_enclosing_receiver, ruby_receiver_type, ruby_seed_assignment, ruby_seed_parameter_shadows,
ruby_type_owner,
};
use crate::graph::resolver::{ReceiverMode, ReceiverType, RubySemanticIndex};
use crate::graph::syntax::{
dynamic_dispatch_target_argument, is_call_method_identifier, is_declaration_constant,
is_declaration_identifier, method_receiver_mode, node_text,
};
use crate::graph_support::RubySource;
use crate::syntax::ruby_semantic_identifier_range;
use brokk_bifrost_core::analyzer::tree_walk::{TreeWalkAction, walk_tree_iterative};
use brokk_bifrost_core::analyzer::usages::inverted_edges::{
ClassRangeIndex, FileEdgeScanInput, PerFileEdges, classify_reference_node,
};
use brokk_bifrost_core::analyzer::usages::local_inference::{
LocalInferenceConfig, LocalInferenceEngine,
};
use brokk_bifrost_core::analyzer::{BoundedDefinitionLookup, CodeUnit, ProjectFile};
use brokk_bifrost_core::hash::HashSet;
use tree_sitter::Node;
pub fn scan_file(
graph: RubyGraphSource<'_>,
ruby: &dyn RubySource,
support: &dyn BoundedDefinitionLookup,
file: &ProjectFile,
input: &FileEdgeScanInput<'_>,
) -> PerFileEdges {
let semantic = RubySemanticIndex::build_for_lookup(graph, ruby);
let visible_files = semantic.visible_files_from(file);
let mut scan = RubyEdgeScan {
semantic: &semantic,
support,
file,
source: input.source,
visible_files,
class_ranges: ClassRangeIndex::build(graph.index, file),
input,
edges: PerFileEdges::default(),
};
scan.scan(input.root());
scan.edges
}
struct RubyEdgeScan<'a> {
semantic: &'a RubySemanticIndex<'a>,
support: &'a dyn BoundedDefinitionLookup,
file: &'a ProjectFile,
source: &'a str,
visible_files: HashSet<ProjectFile>,
class_ranges: ClassRangeIndex,
input: &'a FileEdgeScanInput<'a>,
edges: PerFileEdges,
}
impl RubyEdgeScan<'_> {
fn scan(&mut self, root: Node<'_>) {
let mut state = RubyEdgeWalkState {
scan: self,
locals: LocalInferenceEngine::new(LocalInferenceConfig::default()),
lexical_stack: Vec::new(),
method_stack: Vec::new(),
exits: Vec::new(),
};
walk_tree_iterative(
root,
&mut state,
|node, state| state.enter(node),
|state| state.exit(),
);
}
fn record(&mut self, callee: String, node: Node<'_>) {
let range = ruby_semantic_identifier_range(node, self.source);
self.edges.record_kind(
self.input,
callee,
classify_reference_node(node),
range.start_byte,
range.end_byte,
);
}
fn record_unproven_name(&mut self, name: &str, node: Node<'_>) {
let range = ruby_semantic_identifier_range(node, self.source);
self.edges
.record_unproven_name(self.input, name, range.start_byte, range.end_byte);
}
}
enum RubyEdgeExit {
Lexical,
Method,
LocalScope,
}
struct RubyEdgeWalkState<'scan, 'ctx> {
scan: &'scan mut RubyEdgeScan<'ctx>,
locals: LocalInferenceEngine<String>,
lexical_stack: Vec<String>,
method_stack: Vec<ReceiverMode>,
exits: Vec<RubyEdgeExit>,
}
impl RubyEdgeWalkState<'_, '_> {
fn enter(&mut self, node: Node<'_>) -> TreeWalkAction {
match node.kind() {
"class" | "module" => {
self.record_superclass_reference(node);
if let Some(owner) = self.type_owner(node) {
self.lexical_stack.push(owner);
self.exits.push(RubyEdgeExit::Lexical);
self.record_reference(node);
return TreeWalkAction::DescendWithExit;
}
}
"method" | "singleton_method" => {
self.locals.enter_scope();
self.seed_parameter_shadows(node);
self.method_stack.push(method_receiver_mode(node));
self.exits.push(RubyEdgeExit::Method);
return TreeWalkAction::DescendWithExit;
}
"singleton_class" => {
self.locals.enter_scope();
self.method_stack.push(ReceiverMode::Class);
self.exits.push(RubyEdgeExit::Method);
return TreeWalkAction::DescendWithExit;
}
"block" | "do_block" => {
self.locals.enter_scope();
self.exits.push(RubyEdgeExit::LocalScope);
return TreeWalkAction::DescendWithExit;
}
"assignment" => self.seed_assignment(node),
_ => {}
}
self.record_reference(node);
TreeWalkAction::Descend
}
fn exit(&mut self) {
match self.exits.pop() {
Some(RubyEdgeExit::Lexical) => {
self.lexical_stack.pop();
}
Some(RubyEdgeExit::Method) => {
self.method_stack.pop();
self.locals.exit_scope();
}
Some(RubyEdgeExit::LocalScope) => {
self.locals.exit_scope();
}
None => {}
}
}
fn type_owner(&self, node: Node<'_>) -> Option<String> {
ruby_type_owner(
self.scan.semantic,
self.scan.file,
&self.scan.visible_files,
&self.lexical_stack,
node,
self.scan.source,
)
}
fn record_reference(&mut self, node: Node<'_>) {
self.record_constant_reference(node);
self.record_method_reference(node);
}
fn record_superclass_reference(&mut self, node: Node<'_>) {
let Some(superclass) = node.child_by_field_name("superclass") else {
return;
};
let mut stack = vec![superclass];
while let Some(current) = stack.pop() {
self.record_constant_reference(current);
for index in (0..current.named_child_count()).rev() {
if let Some(child) = current.named_child(index) {
stack.push(child);
}
}
}
}
fn record_constant_reference(&mut self, node: Node<'_>) {
if crate::imports::is_ruby_autoload_symbol_argument(node, self.scan.source) {
self.record_autoload_symbol_constant_reference(node);
return;
}
if !matches!(node.kind(), "constant" | "scope_resolution") || is_declaration_constant(node)
{
return;
}
if let Some(unit) = self.scan.semantic.resolve_constant(
self.scan.file,
&self.scan.visible_files,
&self.lexical_stack,
node,
self.scan.source,
) && (unit.is_class() || unit.is_module())
{
self.scan.record(unit.fq_name(), node);
}
}
fn record_autoload_symbol_constant_reference(&mut self, node: Node<'_>) {
let Some(name) = crate::imports::ruby_symbol_name(node, self.scan.source) else {
return;
};
if let Some(unit) = self.scan.semantic.resolve_constant_name(
self.scan.file,
&self.scan.visible_files,
&self.lexical_stack,
&name,
) && (unit.is_class() || unit.is_module())
{
self.scan.record(unit.fq_name(), node);
}
}
fn record_method_reference(&mut self, node: Node<'_>) {
if node.kind() == "identifier" {
self.record_bare_identifier_method_reference(node);
return;
}
if node.kind() != "call" {
return;
}
let Some(method) = node.child_by_field_name("method") else {
return;
};
let member = node_text(method, self.scan.source);
if member.is_empty() {
return;
}
if let Some((dispatched_member, dispatched_node)) =
dynamic_dispatch_target_argument(node, self.scan.source)
{
self.record_call_method_reference(
node,
&dispatched_member,
dispatched_node,
MethodLookup::Explicit,
);
return;
}
self.record_call_method_reference(
node,
member,
method,
if node.child_by_field_name("receiver").is_some() {
MethodLookup::Explicit
} else {
MethodLookup::Bare
},
);
}
fn record_call_method_reference(
&mut self,
node: Node<'_>,
member: &str,
hit_node: Node<'_>,
lookup: MethodLookup,
) {
let receiver_node = node.child_by_field_name("receiver");
let same_owner = match receiver_node {
None => true,
Some(receiver) => receiver.kind() == "self",
};
let receiver = match receiver_node {
Some(receiver) => self.receiver_type(receiver),
None => self.enclosing_receiver(node.start_byte()),
};
let Some(receiver) = receiver else {
self.scan.record_unproven_name(member, hit_node);
return;
};
if member == "new" && receiver.mode == ReceiverMode::Class {
self.record_unique_method_candidate(
self.initialize_receiver(&receiver),
"initialize",
hit_node,
MethodLookup::Explicit,
same_owner,
);
return;
}
self.record_unique_method_candidate(receiver, member, hit_node, lookup, same_owner);
}
fn record_bare_identifier_method_reference(&mut self, node: Node<'_>) {
let name = node_text(node, self.scan.source);
if name.is_empty()
|| self.locals.is_shadowed(name)
|| is_declaration_identifier(node)
|| is_call_method_identifier(node)
{
return;
}
let Some(receiver) = self.enclosing_receiver(node.start_byte()) else {
return;
};
self.record_unique_method_candidate(receiver, name, node, MethodLookup::Bare, true);
}
fn record_unique_method_candidate(
&mut self,
receiver: ReceiverType,
member: &str,
node: Node<'_>,
lookup: MethodLookup,
same_owner: bool,
) {
if same_owner {
self.scan.record_unproven_name(member, node);
return;
}
let candidates = match lookup {
MethodLookup::Bare => self.scan.semantic.resolve_bare_method_candidates(
self.scan.support,
&self.scan.visible_files,
&receiver,
member,
),
MethodLookup::Explicit => self.scan.semantic.resolve_method_candidates(
self.scan.support,
&self.scan.visible_files,
&receiver,
member,
),
};
if let Some(fqn) = unique_candidate_fqn(candidates) {
self.scan.record(fqn, node);
} else {
self.scan.record_unproven_name(member, node);
}
}
fn receiver_type(&self, node: Node<'_>) -> Option<ReceiverType> {
ruby_receiver_type(
self.scan.semantic,
self.scan.file,
&self.scan.visible_files,
&self.lexical_stack,
&self.locals,
&self.method_stack,
node,
self.scan.source,
)
}
fn enclosing_receiver(&self, byte: usize) -> Option<ReceiverType> {
ruby_enclosing_receiver(&self.lexical_stack, &self.method_stack).or_else(|| {
self.scan
.class_ranges
.enclosing(byte)
.map(|owner_fq_name| ReceiverType {
owner_fq_name: owner_fq_name.to_string(),
mode: ReceiverMode::Instance,
})
})
}
fn initialize_receiver(&self, receiver: &ReceiverType) -> ReceiverType {
ReceiverType {
owner_fq_name: receiver.owner_fq_name.clone(),
mode: ReceiverMode::Instance,
}
}
fn seed_assignment(&mut self, node: Node<'_>) {
ruby_seed_assignment(
self.scan.semantic,
self.scan.file,
&self.scan.visible_files,
&self.lexical_stack,
&self.method_stack,
&mut self.locals,
node,
self.scan.source,
);
}
fn seed_parameter_shadows(&mut self, node: Node<'_>) {
ruby_seed_parameter_shadows(&mut self.locals, node, self.scan.source);
}
}
#[derive(Clone, Copy)]
enum MethodLookup {
Bare,
Explicit,
}
fn unique_candidate_fqn(candidates: Vec<CodeUnit>) -> Option<String> {
if candidates.len() == 1 {
candidates
.into_iter()
.next()
.map(|candidate| candidate.fq_name())
} else {
None
}
}