method-ray 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/{rust → core}/Cargo.toml +1 -1
- data/core/src/analyzer/assignments.rs +499 -0
- data/{rust → core}/src/analyzer/attributes.rs +2 -1
- data/{rust → core}/src/analyzer/blocks.rs +140 -0
- data/{rust → core}/src/analyzer/calls.rs +7 -3
- data/{rust → core}/src/analyzer/definitions.rs +12 -7
- data/{rust → core}/src/analyzer/dispatch.rs +431 -13
- data/core/src/analyzer/exceptions.rs +622 -0
- data/{rust → core}/src/analyzer/install.rs +37 -1
- data/{rust → core}/src/analyzer/literals.rs +3 -17
- data/core/src/analyzer/loops.rs +301 -0
- data/{rust → core}/src/analyzer/mod.rs +4 -0
- data/{rust → core}/src/analyzer/operators.rs +119 -27
- data/{rust → core}/src/analyzer/parameters.rs +214 -5
- data/core/src/analyzer/super_calls.rs +285 -0
- data/{rust → core}/src/cache/rbs_cache.rs +0 -1
- data/{rust → core}/src/cli/commands.rs +3 -3
- data/{rust → core}/src/diagnostics/diagnostic.rs +0 -3
- data/{rust → core}/src/diagnostics/formatter.rs +0 -1
- data/{rust → core}/src/env/box_manager.rs +2 -4
- data/{rust → core}/src/env/global_env.rs +28 -7
- data/{rust → core}/src/env/local_env.rs +35 -1
- data/{rust → core}/src/env/method_registry.rs +117 -25
- data/{rust → core}/src/env/scope.rs +91 -4
- data/{rust → core}/src/env/vertex_manager.rs +0 -1
- data/{rust → core}/src/graph/box.rs +134 -8
- data/{rust → core}/src/graph/change_set.rs +14 -0
- data/{rust → core}/src/lsp/server.rs +1 -1
- data/{rust → core}/src/rbs/loader.rs +1 -2
- data/{rust → core}/src/source_map.rs +0 -1
- data/{rust → core}/src/types.rs +11 -1
- data/ext/Cargo.toml +2 -2
- data/lib/methodray/binary_locator.rb +2 -2
- data/lib/methodray/commands.rb +1 -1
- data/lib/methodray/version.rb +1 -1
- metadata +54 -50
- /data/{rust → core}/src/analyzer/conditionals.rs +0 -0
- /data/{rust → core}/src/analyzer/parentheses.rs +0 -0
- /data/{rust → core}/src/analyzer/returns.rs +0 -0
- /data/{rust → core}/src/analyzer/variables.rs +0 -0
- /data/{rust → core}/src/cache/mod.rs +0 -0
- /data/{rust → core}/src/checker.rs +0 -0
- /data/{rust → core}/src/cli/args.rs +0 -0
- /data/{rust → core}/src/cli/mod.rs +0 -0
- /data/{rust → core}/src/diagnostics/mod.rs +0 -0
- /data/{rust → core}/src/env/mod.rs +0 -0
- /data/{rust → core}/src/env/type_error.rs +0 -0
- /data/{rust → core}/src/graph/mod.rs +0 -0
- /data/{rust → core}/src/graph/vertex.rs +0 -0
- /data/{rust → core}/src/lib.rs +0 -0
- /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
- /data/{rust → core}/src/lsp/main.rs +0 -0
- /data/{rust → core}/src/lsp/mod.rs +0 -0
- /data/{rust → core}/src/main.rs +0 -0
- /data/{rust → core}/src/parser.rs +0 -0
- /data/{rust → core}/src/rbs/converter.rs +0 -0
- /data/{rust → core}/src/rbs/error.rs +0 -0
- /data/{rust → core}/src/rbs/mod.rs +0 -0
|
@@ -118,13 +118,9 @@ fn install_array_literal_elements(
|
|
|
118
118
|
|
|
119
119
|
let array_type = if element_types.is_empty() {
|
|
120
120
|
Type::array()
|
|
121
|
-
} else if element_types.len() == 1 {
|
|
122
|
-
let elem_type = element_types.into_iter().next().unwrap();
|
|
123
|
-
Type::array_of(elem_type)
|
|
124
121
|
} else {
|
|
125
122
|
let types_vec: Vec<Type> = element_types.into_iter().collect();
|
|
126
|
-
|
|
127
|
-
Type::array_of(union_type)
|
|
123
|
+
Type::array_of(Type::union_of(types_vec))
|
|
128
124
|
};
|
|
129
125
|
|
|
130
126
|
Some(genv.new_source(array_type))
|
|
@@ -176,18 +172,8 @@ fn install_hash_literal_elements(
|
|
|
176
172
|
let hash_type = if key_types.is_empty() || value_types.is_empty() {
|
|
177
173
|
Type::hash()
|
|
178
174
|
} else {
|
|
179
|
-
let key_type =
|
|
180
|
-
|
|
181
|
-
} else {
|
|
182
|
-
let types_vec: Vec<Type> = key_types.into_iter().collect();
|
|
183
|
-
Type::Union(types_vec)
|
|
184
|
-
};
|
|
185
|
-
let value_type = if value_types.len() == 1 {
|
|
186
|
-
value_types.into_iter().next().unwrap()
|
|
187
|
-
} else {
|
|
188
|
-
let types_vec: Vec<Type> = value_types.into_iter().collect();
|
|
189
|
-
Type::Union(types_vec)
|
|
190
|
-
};
|
|
175
|
+
let key_type = Type::union_of(key_types.into_iter().collect());
|
|
176
|
+
let value_type = Type::union_of(value_types.into_iter().collect());
|
|
191
177
|
Type::hash_of(key_type, value_type)
|
|
192
178
|
};
|
|
193
179
|
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
//! Loops - while/until loop type inference
|
|
2
|
+
//!
|
|
3
|
+
//! Ruby loop expressions evaluate to nil (except when break passes a value).
|
|
4
|
+
//! Break value propagation is not yet supported.
|
|
5
|
+
|
|
6
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
7
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
8
|
+
use crate::types::Type;
|
|
9
|
+
use ruby_prism::{ForNode, UntilNode, WhileNode};
|
|
10
|
+
|
|
11
|
+
use super::bytes_to_name;
|
|
12
|
+
use super::install::{install_node, install_statements};
|
|
13
|
+
|
|
14
|
+
/// Process WhileNode: `while predicate; statements; end`
|
|
15
|
+
///
|
|
16
|
+
/// Returns nil. Traverses predicate and body to register method calls
|
|
17
|
+
/// and variable assignments in the type graph.
|
|
18
|
+
pub(crate) fn process_while_node(
|
|
19
|
+
genv: &mut GlobalEnv,
|
|
20
|
+
lenv: &mut LocalEnv,
|
|
21
|
+
changes: &mut ChangeSet,
|
|
22
|
+
source: &str,
|
|
23
|
+
while_node: &WhileNode,
|
|
24
|
+
) -> Option<VertexId> {
|
|
25
|
+
install_node(genv, lenv, changes, source, &while_node.predicate());
|
|
26
|
+
|
|
27
|
+
if let Some(stmts) = while_node.statements() {
|
|
28
|
+
install_statements(genv, lenv, changes, source, &stmts);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Some(genv.new_source(Type::Nil))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Process UntilNode: `until predicate; statements; end`
|
|
35
|
+
///
|
|
36
|
+
/// Returns nil. Traverses predicate and body to register method calls
|
|
37
|
+
/// and variable assignments in the type graph.
|
|
38
|
+
pub(crate) fn process_until_node(
|
|
39
|
+
genv: &mut GlobalEnv,
|
|
40
|
+
lenv: &mut LocalEnv,
|
|
41
|
+
changes: &mut ChangeSet,
|
|
42
|
+
source: &str,
|
|
43
|
+
until_node: &UntilNode,
|
|
44
|
+
) -> Option<VertexId> {
|
|
45
|
+
install_node(genv, lenv, changes, source, &until_node.predicate());
|
|
46
|
+
|
|
47
|
+
if let Some(stmts) = until_node.statements() {
|
|
48
|
+
install_statements(genv, lenv, changes, source, &stmts);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Some(genv.new_source(Type::Nil))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Process ForNode: `for index in collection; statements; end`
|
|
55
|
+
///
|
|
56
|
+
/// Ruby's `for` does NOT create a new scope — the loop variable persists
|
|
57
|
+
/// after the loop. This differs from `collection.each { |x| }` which
|
|
58
|
+
/// creates a block scope.
|
|
59
|
+
///
|
|
60
|
+
/// Returns nil (consistent with while/until; Ruby's for returns the
|
|
61
|
+
/// collection, but the return value is rarely used in practice).
|
|
62
|
+
pub(crate) fn process_for_node(
|
|
63
|
+
genv: &mut GlobalEnv,
|
|
64
|
+
lenv: &mut LocalEnv,
|
|
65
|
+
changes: &mut ChangeSet,
|
|
66
|
+
source: &str,
|
|
67
|
+
for_node: &ForNode,
|
|
68
|
+
) -> Option<VertexId> {
|
|
69
|
+
let collection_vtx = install_node(genv, lenv, changes, source, &for_node.collection());
|
|
70
|
+
|
|
71
|
+
// TODO: MultiTargetNode (e.g., `for a, b in [[1, "x"]]`) is not yet supported
|
|
72
|
+
if let Some(target) = for_node.index().as_local_variable_target_node() {
|
|
73
|
+
let name = bytes_to_name(target.name().as_slice());
|
|
74
|
+
let var_vtx = genv.new_vertex();
|
|
75
|
+
|
|
76
|
+
// Array[T] or Range[T] → loop var gets T
|
|
77
|
+
let elem_type = collection_vtx
|
|
78
|
+
.and_then(|vtx| genv.get_source(vtx))
|
|
79
|
+
.and_then(|src| src.ty.type_args())
|
|
80
|
+
.and_then(|args| args.first().cloned());
|
|
81
|
+
if let Some(ty) = elem_type {
|
|
82
|
+
let elem_src = genv.new_source(ty);
|
|
83
|
+
genv.add_edge(elem_src, var_vtx);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lenv.new_var(name, var_vtx);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if let Some(stmts) = for_node.statements() {
|
|
90
|
+
install_statements(genv, lenv, changes, source, &stmts);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Some(genv.new_source(Type::Nil))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[cfg(test)]
|
|
97
|
+
mod tests {
|
|
98
|
+
use crate::analyzer::install::AstInstaller;
|
|
99
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
100
|
+
use crate::graph::VertexId;
|
|
101
|
+
use crate::parser::ParseSession;
|
|
102
|
+
use crate::types::Type;
|
|
103
|
+
|
|
104
|
+
/// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
|
|
105
|
+
fn analyze(source: &str) -> GlobalEnv {
|
|
106
|
+
let session = ParseSession::new();
|
|
107
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
108
|
+
let root = parse_result.node();
|
|
109
|
+
let program = root.as_program_node().unwrap();
|
|
110
|
+
|
|
111
|
+
let mut genv = GlobalEnv::new();
|
|
112
|
+
let mut lenv = LocalEnv::new();
|
|
113
|
+
|
|
114
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
115
|
+
for stmt in &program.statements().body() {
|
|
116
|
+
installer.install_node(&stmt);
|
|
117
|
+
}
|
|
118
|
+
installer.finish();
|
|
119
|
+
|
|
120
|
+
genv
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Helper: get the type string for a vertex ID (checks both Vertex and Source)
|
|
124
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
125
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
126
|
+
vertex.show()
|
|
127
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
128
|
+
source.ty.show()
|
|
129
|
+
} else {
|
|
130
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_while_returns_nil() {
|
|
136
|
+
let source = r#"
|
|
137
|
+
class Foo
|
|
138
|
+
def bar
|
|
139
|
+
while true
|
|
140
|
+
"hello"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
"#;
|
|
145
|
+
let genv = analyze(source);
|
|
146
|
+
let info = genv
|
|
147
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
148
|
+
.expect("Foo#bar should be registered");
|
|
149
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
150
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn test_until_returns_nil() {
|
|
155
|
+
let source = r#"
|
|
156
|
+
class Foo
|
|
157
|
+
def bar
|
|
158
|
+
until false
|
|
159
|
+
"hello"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
"#;
|
|
164
|
+
let genv = analyze(source);
|
|
165
|
+
let info = genv
|
|
166
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
167
|
+
.expect("Foo#bar should be registered");
|
|
168
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
169
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn test_while_variable_assignment_in_body() {
|
|
174
|
+
// Should not panic — variable assignment inside loop is processed
|
|
175
|
+
let source = r#"
|
|
176
|
+
x = "initial"
|
|
177
|
+
while true
|
|
178
|
+
x = "hello"
|
|
179
|
+
end
|
|
180
|
+
"#;
|
|
181
|
+
analyze(source);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[test]
|
|
185
|
+
fn test_while_modifier_form() {
|
|
186
|
+
let source = r#"
|
|
187
|
+
class Foo
|
|
188
|
+
def bar
|
|
189
|
+
x = "hello" while false
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
"#;
|
|
193
|
+
let genv = analyze(source);
|
|
194
|
+
let info = genv
|
|
195
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
196
|
+
.expect("Foo#bar should be registered");
|
|
197
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
198
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn test_begin_end_while() {
|
|
203
|
+
let source = r#"
|
|
204
|
+
class Foo
|
|
205
|
+
def bar
|
|
206
|
+
begin
|
|
207
|
+
"hello"
|
|
208
|
+
end while false
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
"#;
|
|
212
|
+
let genv = analyze(source);
|
|
213
|
+
let info = genv
|
|
214
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
215
|
+
.expect("Foo#bar should be registered");
|
|
216
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
217
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- for loop tests ---
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn test_for_returns_nil() {
|
|
224
|
+
let source = r#"
|
|
225
|
+
class Foo
|
|
226
|
+
def bar
|
|
227
|
+
for x in [1, 2, 3]
|
|
228
|
+
x
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
"#;
|
|
233
|
+
let genv = analyze(source);
|
|
234
|
+
let info = genv
|
|
235
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
236
|
+
.expect("Foo#bar should be registered");
|
|
237
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
238
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn test_for_variable_type_from_array() {
|
|
243
|
+
let source = r#"
|
|
244
|
+
for item in [1, 2, 3]
|
|
245
|
+
item
|
|
246
|
+
end
|
|
247
|
+
"#;
|
|
248
|
+
// Should not panic; loop variable is registered
|
|
249
|
+
analyze(source);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn test_for_variable_persists_after_loop() {
|
|
254
|
+
// for does NOT create a new scope — variable persists
|
|
255
|
+
let source = r#"
|
|
256
|
+
class Foo
|
|
257
|
+
def bar
|
|
258
|
+
for x in [1, 2, 3]
|
|
259
|
+
end
|
|
260
|
+
x
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
"#;
|
|
264
|
+
// Should not panic — x is accessible after the loop
|
|
265
|
+
analyze(source);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[test]
|
|
269
|
+
fn test_for_empty_body() {
|
|
270
|
+
let source = r#"
|
|
271
|
+
class Foo
|
|
272
|
+
def bar
|
|
273
|
+
for x in [1, 2, 3]
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
"#;
|
|
278
|
+
let genv = analyze(source);
|
|
279
|
+
let info = genv
|
|
280
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
281
|
+
.expect("Foo#bar should be registered");
|
|
282
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
283
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
fn test_for_with_method_call_in_body() {
|
|
288
|
+
// Should not panic — method call on loop variable is processed
|
|
289
|
+
// (type error check requires RBS, covered by Ruby integration test)
|
|
290
|
+
let source = r#"
|
|
291
|
+
class Foo
|
|
292
|
+
def bar
|
|
293
|
+
for item in ["hello", "world"]
|
|
294
|
+
item.upcase
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
"#;
|
|
299
|
+
analyze(source);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
mod assignments;
|
|
1
2
|
mod attributes;
|
|
2
3
|
mod blocks;
|
|
3
4
|
mod calls;
|
|
4
5
|
mod conditionals;
|
|
5
6
|
mod definitions;
|
|
7
|
+
mod exceptions;
|
|
6
8
|
mod dispatch;
|
|
7
9
|
mod install;
|
|
8
10
|
mod literals;
|
|
11
|
+
mod loops;
|
|
9
12
|
mod operators;
|
|
10
13
|
mod parameters;
|
|
11
14
|
mod parentheses;
|
|
12
15
|
mod returns;
|
|
16
|
+
mod super_calls;
|
|
13
17
|
mod variables;
|
|
14
18
|
|
|
15
19
|
pub use install::AstInstaller;
|
|
@@ -1,42 +1,52 @@
|
|
|
1
|
-
//! Operators - logical operator type inference (&&,
|
|
1
|
+
//! Operators - logical operator type inference (&&, ||, !)
|
|
2
2
|
|
|
3
3
|
use crate::env::{GlobalEnv, LocalEnv};
|
|
4
4
|
use crate::graph::{ChangeSet, VertexId};
|
|
5
|
-
use
|
|
5
|
+
use crate::types::Type;
|
|
6
|
+
use ruby_prism::{AndNode, Node, OrNode};
|
|
6
7
|
|
|
7
8
|
use super::install::install_node;
|
|
8
9
|
|
|
9
|
-
///
|
|
10
|
-
|
|
11
|
-
/// Short-circuit semantics: if `a` is falsy, returns `a`; otherwise returns `b`.
|
|
12
|
-
/// Static approximation: we cannot determine truthiness at compile time,
|
|
13
|
-
/// so we conservatively produce Union(type(a), type(b)).
|
|
14
|
-
pub(crate) fn process_and_node(
|
|
10
|
+
/// Merge two branch nodes into a union type vertex.
|
|
11
|
+
fn process_binary_logical_op<'a>(
|
|
15
12
|
genv: &mut GlobalEnv,
|
|
16
13
|
lenv: &mut LocalEnv,
|
|
17
14
|
changes: &mut ChangeSet,
|
|
18
15
|
source: &str,
|
|
19
|
-
|
|
16
|
+
left: Node<'a>,
|
|
17
|
+
right: Node<'a>,
|
|
20
18
|
) -> Option<VertexId> {
|
|
21
19
|
let result_vtx = genv.new_vertex();
|
|
22
20
|
|
|
23
|
-
let
|
|
24
|
-
if let Some(vtx) = left_vtx {
|
|
21
|
+
if let Some(vtx) = install_node(genv, lenv, changes, source, &left) {
|
|
25
22
|
genv.add_edge(vtx, result_vtx);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
let
|
|
29
|
-
if let Some(vtx) = right_vtx {
|
|
25
|
+
if let Some(vtx) = install_node(genv, lenv, changes, source, &right) {
|
|
30
26
|
genv.add_edge(vtx, result_vtx);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
Some(result_vtx)
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
/// Process AndNode (a && b): Union(type(a), type(b))
|
|
33
|
+
///
|
|
34
|
+
/// Runtime: if `a` is falsy, returns `a`; otherwise returns `b`.
|
|
35
|
+
/// Static: conservatively produce Union(type(a), type(b)).
|
|
36
|
+
pub(crate) fn process_and_node(
|
|
37
|
+
genv: &mut GlobalEnv,
|
|
38
|
+
lenv: &mut LocalEnv,
|
|
39
|
+
changes: &mut ChangeSet,
|
|
40
|
+
source: &str,
|
|
41
|
+
and_node: &AndNode,
|
|
42
|
+
) -> Option<VertexId> {
|
|
43
|
+
process_binary_logical_op(genv, lenv, changes, source, and_node.left(), and_node.right())
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/// Process OrNode (a || b): Union(type(a), type(b))
|
|
37
47
|
///
|
|
38
|
-
///
|
|
39
|
-
/// Static
|
|
48
|
+
/// Runtime: if `a` is truthy, returns `a`; otherwise returns `b`.
|
|
49
|
+
/// Static: conservatively produce Union(type(a), type(b)).
|
|
40
50
|
pub(crate) fn process_or_node(
|
|
41
51
|
genv: &mut GlobalEnv,
|
|
42
52
|
lenv: &mut LocalEnv,
|
|
@@ -44,19 +54,28 @@ pub(crate) fn process_or_node(
|
|
|
44
54
|
source: &str,
|
|
45
55
|
or_node: &OrNode,
|
|
46
56
|
) -> Option<VertexId> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let left_vtx = install_node(genv, lenv, changes, source, &or_node.left());
|
|
50
|
-
if let Some(vtx) = left_vtx {
|
|
51
|
-
genv.add_edge(vtx, result_vtx);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let right_vtx = install_node(genv, lenv, changes, source, &or_node.right());
|
|
55
|
-
if let Some(vtx) = right_vtx {
|
|
56
|
-
genv.add_edge(vtx, result_vtx);
|
|
57
|
-
}
|
|
57
|
+
process_binary_logical_op(genv, lenv, changes, source, or_node.left(), or_node.right())
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
/// Process not operator (!expr): TrueClass | FalseClass
|
|
61
|
+
///
|
|
62
|
+
/// In ruby-prism, `!expr` is represented as a CallNode with method name "!".
|
|
63
|
+
/// Static approximation: we cannot determine the receiver's truthiness at
|
|
64
|
+
/// compile time, so conservatively return TrueClass | FalseClass for any `!` call.
|
|
65
|
+
/// In practice, `!nil` and `!false` are always true, but we do not track that here.
|
|
66
|
+
///
|
|
67
|
+
/// Receiver side effects are already analyzed by the caller (process_needs_child).
|
|
68
|
+
///
|
|
69
|
+
/// TODO: Ruby allows overriding `BasicObject#!`. Currently we always return
|
|
70
|
+
/// TrueClass | FalseClass, ignoring user-defined `!` methods. If needed, look up
|
|
71
|
+
/// the receiver's RBS definition and use its return type instead.
|
|
72
|
+
pub(crate) fn process_not_operator(genv: &mut GlobalEnv) -> VertexId {
|
|
73
|
+
let result_vtx = genv.new_vertex();
|
|
74
|
+
let true_vtx = genv.new_source(Type::instance("TrueClass"));
|
|
75
|
+
let false_vtx = genv.new_source(Type::instance("FalseClass"));
|
|
76
|
+
genv.add_edge(true_vtx, result_vtx);
|
|
77
|
+
genv.add_edge(false_vtx, result_vtx);
|
|
78
|
+
result_vtx
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
#[cfg(test)]
|
|
@@ -189,4 +208,77 @@ end
|
|
|
189
208
|
assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
|
|
190
209
|
}
|
|
191
210
|
|
|
211
|
+
// ============================================
|
|
212
|
+
// Not operator (!) tests
|
|
213
|
+
// ============================================
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn test_not_operator_returns_boolean_union() {
|
|
217
|
+
let source = r#"
|
|
218
|
+
class Foo
|
|
219
|
+
def bar
|
|
220
|
+
!true
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
"#;
|
|
224
|
+
let genv = analyze(source);
|
|
225
|
+
let info = genv
|
|
226
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
227
|
+
.expect("bar should be registered");
|
|
228
|
+
let ty = get_type_show(&genv, info.return_vertex.unwrap());
|
|
229
|
+
assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
|
|
230
|
+
assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn test_not_operator_receiver_side_effects_analyzed() {
|
|
235
|
+
let source = r#"
|
|
236
|
+
class Foo
|
|
237
|
+
def bar
|
|
238
|
+
!(1.upcase)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
"#;
|
|
242
|
+
let genv = analyze(source);
|
|
243
|
+
assert!(
|
|
244
|
+
!genv.type_errors.is_empty(),
|
|
245
|
+
"expected type error for Integer#upcase"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn test_double_not_operator_union() {
|
|
251
|
+
let source = r#"
|
|
252
|
+
class Foo
|
|
253
|
+
def bar
|
|
254
|
+
!!true
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
"#;
|
|
258
|
+
let genv = analyze(source);
|
|
259
|
+
let info = genv
|
|
260
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
261
|
+
.expect("bar should be registered");
|
|
262
|
+
let ty = get_type_show(&genv, info.return_vertex.unwrap());
|
|
263
|
+
assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
|
|
264
|
+
assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn test_not_nil_returns_boolean() {
|
|
269
|
+
let source = r#"
|
|
270
|
+
class Foo
|
|
271
|
+
def bar
|
|
272
|
+
!nil
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
"#;
|
|
276
|
+
let genv = analyze(source);
|
|
277
|
+
let info = genv
|
|
278
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
279
|
+
.expect("bar should be registered");
|
|
280
|
+
let ty = get_type_show(&genv, info.return_vertex.unwrap());
|
|
281
|
+
assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
|
|
282
|
+
assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
|
|
283
|
+
}
|
|
192
284
|
}
|