use tree_sitter::Node;
use super::builder::Builder;
use super::{IntroKind, Read, ScopeId, ScopeKind};
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
}
"super" => {
mark_zsuper_reads(self, scope, n.start_byte(), 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()];
if self.lookup(scope, bytes[0], &name).is_none() {
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 ud = under_defined || op_node.is_some_and(|o| self.text(o) == "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) {
if n
.child_by_field_name("operator")
.is_some_and(|o| self.text(o) == "&.")
&& 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_reads(
b: &mut Builder<'_>,
scope: ScopeId,
byte: usize,
under_defined: bool,
keep: impl Fn(&str, &super::Entry) -> bool,
) {
let names: Vec<Box<str>> = b.scopes[scope]
.entries
.iter()
.filter(|(n, e)| keep(n, e))
.map(|(n, _)| n.clone())
.collect();
for name in names {
b.record_read(
scope,
&name,
Read {
byte,
under_defined,
},
);
}
}
fn mark_all_reads(b: &mut Builder<'_>, scope: ScopeId, byte: usize, under_defined: bool) {
mark_reads(b, scope, byte, under_defined, |n, _| !n.starts_with('_'));
}
fn mark_zsuper_reads(b: &mut Builder<'_>, scope: ScopeId, byte: usize, under_defined: bool) {
let Some(method) = method_scope(b, scope) else {
return;
};
mark_reads(b, method, byte, under_defined, |n, e| {
e.intro_kind == IntroKind::Param && !n.starts_with('_')
});
}
fn method_scope(b: &Builder<'_>, mut scope: ScopeId) -> Option<ScopeId> {
loop {
if b.scopes[scope].kind == ScopeKind::Method {
return Some(scope);
}
scope = b.scopes[scope].parent?;
}
}