use tree_sitter::Node;
use super::builder::Builder;
use super::{Read, ScopeId};
impl Builder<'_> {
pub(super) fn walk_read(
&mut self,
n: Node,
kind: &str,
scope: ScopeId,
under_defined: bool,
) -> bool {
match kind {
"unary" => {
self.walk_unary(n, scope, under_defined);
true
}
"call" => {
self.walk_call(n, scope, under_defined);
true
}
"identifier" => {
self.walk_identifier(n, scope, under_defined);
true
}
"pair" if n.child_by_field_name("value").is_none() => {
self.walk_shorthand_pair(n, scope, under_defined);
true
}
_ => false,
}
}
fn walk_shorthand_pair(&mut self, n: Node, scope: ScopeId, under_defined: bool) {
let Some(key) = n.child_by_field_name("key") else {
return;
};
if key.kind() != "hash_key_symbol" {
return;
}
let name = key.utf8_text(self.src).unwrap_or("").to_string();
let bytes = [key.start_byte(), key.end_byte()];
let bound = self.lookup(scope, bytes[0], &name).is_some();
if !bound {
self.vcall_sites.push(bytes[0]);
return;
}
if name.starts_with('_') {
return;
}
for byte in bytes {
self.record_read(
scope,
&name,
Read {
byte,
under_defined,
},
);
}
}
fn walk_unary(&mut self, n: Node, scope: ScopeId, under_defined: bool) {
let op_node = n.child_by_field_name("operator");
let op = op_node.map(|o| self.text(o)).unwrap_or("");
let ud = under_defined || op == "defined?";
let mut cursor = n.walk();
for child in n.children(&mut cursor) {
if op_node.map(|o| o.id()) == Some(child.id()) {
continue;
}
self.walk(child, scope, ud);
}
}
fn walk_call(&mut self, n: Node, scope: ScopeId, under_defined: bool) {
let method_slot = n.child_by_field_name("method");
self.note_csend_site(n, scope);
if is_binding_call(n, method_slot, self.src) {
mark_all_reads(self, scope, n.start_byte(), under_defined);
}
let mut cursor = n.walk();
for child in n.children(&mut cursor) {
if method_slot.map(|m| m.id()) == Some(child.id()) {
continue;
}
self.walk(child, scope, under_defined);
}
}
fn note_csend_site(&mut self, n: Node, scope: ScopeId) {
let op = n
.child_by_field_name("operator")
.map(|o| self.text(o))
.unwrap_or("")
.to_string();
if op == "&."
&& let Some(recv) = n.child_by_field_name("receiver")
&& recv.kind() == "identifier"
{
let name = self.text(recv);
if self.lookup(scope, recv.start_byte(), name).is_some() {
self.csend_sites
.push((recv.start_byte(), name.into(), scope));
}
}
}
fn walk_identifier(&mut self, n: Node, scope: ScopeId, under_defined: bool) {
let name = self.text(n).to_string();
if is_magic_file_ident(&name) {
return;
}
let r = Read {
byte: n.start_byte(),
under_defined,
};
if self.lookup(scope, r.byte, &name).is_some() {
record_named_read(self, scope, &name, r);
} else {
record_unresolved_vcall(self, scope, &name, n.start_byte(), under_defined);
}
}
}
fn is_magic_file_ident(name: &str) -> bool {
matches!(name, "__FILE__" | "__LINE__" | "__ENCODING__")
}
fn record_named_read(b: &mut Builder<'_>, scope: ScopeId, name: &str, r: Read) {
if !name.starts_with('_') {
b.record_read(scope, name, r);
}
}
fn record_unresolved_vcall(
b: &mut Builder<'_>,
scope: ScopeId,
name: &str,
byte: usize,
under_defined: bool,
) {
if name == "binding" {
mark_all_reads(b, scope, byte, under_defined);
}
b.vcall_sites.push(byte);
}
fn is_binding_call(n: Node<'_>, method_slot: Option<Node<'_>>, src: &[u8]) -> bool {
let Some(m) = method_slot else {
return false;
};
if n.child_by_field_name("receiver").is_some() {
return false;
}
m.utf8_text(src).unwrap_or("") == "binding"
}
fn mark_all_reads(b: &mut Builder<'_>, scope: ScopeId, byte: usize, under_defined: bool) {
let names: Vec<Box<str>> = b.scopes[scope].entries.keys().cloned().collect();
for name in names {
if name.starts_with('_') {
continue;
}
b.record_read(
scope,
&name,
Read {
byte,
under_defined,
},
);
}
}