1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Read-recording constructs: unary operators (`defined?` taints), calls
//! (safe-nav site collection), and bare identifiers (reads or vcalls).
use tree_sitter::Node;
use super::builder::Builder;
use super::{Read, ScopeId};
impl Builder<'_> {
/// Returns true when `kind` is a read-shaped construct.
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
}
// Ruby 3 shorthand hash (`foo(user:)`): a pair with no value
// is a variable reference wearing a label.
"pair" if n.child_by_field_name("value").is_none() => {
self.walk_shorthand_pair(n, scope, under_defined);
true
}
_ => false,
}
}
/// Record the shorthand key as a read of the identically named
/// local (or a vcall when no such local exists -- Ruby would call
/// the method).
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();
// Two read positions across the key: UsedOnce demands exactly one
// read, and a shorthand read can never be inlined away (`42:` is
// not valid Ruby), so it must never qualify as the single use.
let bytes = [key.start_byte(), key.end_byte()];
if !self.lookup(scope, bytes[0], &name).is_some() {
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.map(|o| self.text(o)).unwrap_or("") == "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) {
// never treat the @method slot as a variable read
let method_slot = n.child_by_field_name("method");
self.note_csend_site(n, scope);
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);
}
}
/// Safe-navigation on a local receiver: recorded for the ABC
/// repeated-csend discount.
fn note_csend_site(&mut self, n: Node, scope: ScopeId) {
if n.child_by_field_name("operator")
.map(|o| self.text(o))
.unwrap_or("")
.to_string()
== "&."
&& 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();
// Magic constants (parser/RuboCop); tree-sitter emits them as identifier.
if matches!(name.as_str(), "__FILE__" | "__LINE__" | "__ENCODING__") {
return;
}
let r = Read {
byte: n.start_byte(),
under_defined,
};
if self.lookup(scope, r.byte, &name).is_some() {
if !name.starts_with('_') {
self.record_read(scope, &name, r);
}
} else {
// unresolved bare identifier == zero-arity method call
self.vcall_sites.push(n.start_byte());
}
}
}