method-ray 0.1.4 → 0.1.6
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 +28 -0
- data/ext/Cargo.toml +1 -1
- data/ext/src/lib.rs +38 -59
- data/lib/methodray/cli.rb +1 -0
- data/lib/methodray/commands.rb +0 -1
- data/lib/methodray/version.rb +1 -1
- data/rust/Cargo.toml +1 -1
- data/rust/src/analyzer/attributes.rs +2 -2
- data/rust/src/analyzer/conditionals.rs +72 -0
- data/rust/src/analyzer/definitions.rs +385 -8
- data/rust/src/analyzer/dispatch.rs +31 -12
- data/rust/src/analyzer/install.rs +18 -0
- data/rust/src/analyzer/literals.rs +45 -0
- data/rust/src/analyzer/mod.rs +3 -0
- data/rust/src/analyzer/operators.rs +192 -0
- data/rust/src/analyzer/parentheses.rs +113 -0
- data/rust/src/analyzer/returns.rs +191 -0
- data/rust/src/checker.rs +2 -4
- data/rust/src/cli/args.rs +1 -1
- data/rust/src/env/global_env.rs +3 -4
- data/rust/src/env/scope.rs +21 -0
- data/rust/src/graph/box.rs +4 -2
- data/rust/src/main.rs +1 -0
- data/rust/src/rbs/loader.rs +3 -4
- data/rust/src/rbs/mod.rs +29 -0
- metadata +4 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//! Operators - logical operator type inference (&&, ||)
|
|
2
|
+
|
|
3
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
4
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
5
|
+
use ruby_prism::{AndNode, OrNode};
|
|
6
|
+
|
|
7
|
+
use super::install::install_node;
|
|
8
|
+
|
|
9
|
+
/// Process AndNode (a && b): Union(type(a), type(b))
|
|
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(
|
|
15
|
+
genv: &mut GlobalEnv,
|
|
16
|
+
lenv: &mut LocalEnv,
|
|
17
|
+
changes: &mut ChangeSet,
|
|
18
|
+
source: &str,
|
|
19
|
+
and_node: &AndNode,
|
|
20
|
+
) -> Option<VertexId> {
|
|
21
|
+
let result_vtx = genv.new_vertex();
|
|
22
|
+
|
|
23
|
+
let left_vtx = install_node(genv, lenv, changes, source, &and_node.left());
|
|
24
|
+
if let Some(vtx) = left_vtx {
|
|
25
|
+
genv.add_edge(vtx, result_vtx);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let right_vtx = install_node(genv, lenv, changes, source, &and_node.right());
|
|
29
|
+
if let Some(vtx) = right_vtx {
|
|
30
|
+
genv.add_edge(vtx, result_vtx);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Some(result_vtx)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Process OrNode (a || b): Union(type(a), type(b))
|
|
37
|
+
///
|
|
38
|
+
/// Short-circuit semantics: if `a` is truthy, returns `a`; otherwise returns `b`.
|
|
39
|
+
/// Static approximation: identical to AndNode — Union of both sides.
|
|
40
|
+
pub(crate) fn process_or_node(
|
|
41
|
+
genv: &mut GlobalEnv,
|
|
42
|
+
lenv: &mut LocalEnv,
|
|
43
|
+
changes: &mut ChangeSet,
|
|
44
|
+
source: &str,
|
|
45
|
+
or_node: &OrNode,
|
|
46
|
+
) -> Option<VertexId> {
|
|
47
|
+
let result_vtx = genv.new_vertex();
|
|
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
|
+
}
|
|
58
|
+
|
|
59
|
+
Some(result_vtx)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[cfg(test)]
|
|
63
|
+
mod tests {
|
|
64
|
+
use crate::analyzer::install::AstInstaller;
|
|
65
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
66
|
+
use crate::graph::VertexId;
|
|
67
|
+
use crate::parser::ParseSession;
|
|
68
|
+
use crate::types::Type;
|
|
69
|
+
|
|
70
|
+
/// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
|
|
71
|
+
fn analyze(source: &str) -> GlobalEnv {
|
|
72
|
+
let session = ParseSession::new();
|
|
73
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
74
|
+
let root = parse_result.node();
|
|
75
|
+
let program = root.as_program_node().unwrap();
|
|
76
|
+
|
|
77
|
+
let mut genv = GlobalEnv::new();
|
|
78
|
+
let mut lenv = LocalEnv::new();
|
|
79
|
+
|
|
80
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
81
|
+
for stmt in &program.statements().body() {
|
|
82
|
+
installer.install_node(&stmt);
|
|
83
|
+
}
|
|
84
|
+
installer.finish();
|
|
85
|
+
|
|
86
|
+
genv
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Helper: get the type string for a vertex ID
|
|
90
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
91
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
92
|
+
vertex.show()
|
|
93
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
94
|
+
source.ty.show()
|
|
95
|
+
} else {
|
|
96
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn test_and_node_union_type() {
|
|
102
|
+
let source = r#"
|
|
103
|
+
class Foo
|
|
104
|
+
def bar
|
|
105
|
+
true && "hello"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
"#;
|
|
109
|
+
let genv = analyze(source);
|
|
110
|
+
let info = genv
|
|
111
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
112
|
+
.expect("Foo#bar should be registered");
|
|
113
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
114
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
115
|
+
assert!(type_str.contains("TrueClass"), "should contain TrueClass: {}", type_str);
|
|
116
|
+
assert!(type_str.contains("String"), "should contain String: {}", type_str);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn test_and_node_same_type() {
|
|
121
|
+
let source = r#"
|
|
122
|
+
class Foo
|
|
123
|
+
def bar
|
|
124
|
+
"a" && "b"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
"#;
|
|
128
|
+
let genv = analyze(source);
|
|
129
|
+
let info = genv
|
|
130
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
131
|
+
.expect("Foo#bar should be registered");
|
|
132
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
133
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn test_or_node_union_type() {
|
|
138
|
+
let source = r#"
|
|
139
|
+
class Foo
|
|
140
|
+
def bar
|
|
141
|
+
42 || "hello"
|
|
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
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
151
|
+
assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
|
|
152
|
+
assert!(type_str.contains("String"), "should contain String: {}", type_str);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_or_node_same_type() {
|
|
157
|
+
let source = r#"
|
|
158
|
+
class Foo
|
|
159
|
+
def bar
|
|
160
|
+
1 || 2
|
|
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), "Integer");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn test_nested_logical_operators() {
|
|
174
|
+
let source = r#"
|
|
175
|
+
class Foo
|
|
176
|
+
def bar
|
|
177
|
+
1 && "a" || :b
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
"#;
|
|
181
|
+
let genv = analyze(source);
|
|
182
|
+
let info = genv
|
|
183
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
184
|
+
.expect("Foo#bar should be registered");
|
|
185
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
186
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
187
|
+
assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
|
|
188
|
+
assert!(type_str.contains("String"), "should contain String: {}", type_str);
|
|
189
|
+
assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//! Parentheses - pass-through type propagation for parenthesized expressions
|
|
2
|
+
|
|
3
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
4
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
5
|
+
|
|
6
|
+
use super::install::{install_node, install_statements};
|
|
7
|
+
|
|
8
|
+
/// Process ParenthesesNode: propagate inner expression's type
|
|
9
|
+
pub(crate) fn process_parentheses_node(
|
|
10
|
+
genv: &mut GlobalEnv,
|
|
11
|
+
lenv: &mut LocalEnv,
|
|
12
|
+
changes: &mut ChangeSet,
|
|
13
|
+
source: &str,
|
|
14
|
+
paren_node: &ruby_prism::ParenthesesNode,
|
|
15
|
+
) -> Option<VertexId> {
|
|
16
|
+
let body = paren_node.body()?;
|
|
17
|
+
|
|
18
|
+
if let Some(stmts) = body.as_statements_node() {
|
|
19
|
+
// (expr1; expr2) → process all, return last expression's type
|
|
20
|
+
install_statements(genv, lenv, changes, source, &stmts)
|
|
21
|
+
} else {
|
|
22
|
+
// (expr) → propagate inner expression's type directly
|
|
23
|
+
install_node(genv, lenv, changes, source, &body)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[cfg(test)]
|
|
28
|
+
mod tests {
|
|
29
|
+
use crate::analyzer::install::AstInstaller;
|
|
30
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
31
|
+
use crate::graph::VertexId;
|
|
32
|
+
use crate::parser::ParseSession;
|
|
33
|
+
use crate::types::Type;
|
|
34
|
+
|
|
35
|
+
fn analyze(source: &str) -> GlobalEnv {
|
|
36
|
+
let session = ParseSession::new();
|
|
37
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
38
|
+
let root = parse_result.node();
|
|
39
|
+
let program = root.as_program_node().unwrap();
|
|
40
|
+
|
|
41
|
+
let mut genv = GlobalEnv::new();
|
|
42
|
+
let mut lenv = LocalEnv::new();
|
|
43
|
+
|
|
44
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
45
|
+
for stmt in &program.statements().body() {
|
|
46
|
+
installer.install_node(&stmt);
|
|
47
|
+
}
|
|
48
|
+
installer.finish();
|
|
49
|
+
|
|
50
|
+
genv
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
54
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
55
|
+
vertex.show()
|
|
56
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
57
|
+
source.ty.show()
|
|
58
|
+
} else {
|
|
59
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn test_parenthesized_integer() {
|
|
65
|
+
let source = r#"
|
|
66
|
+
class Foo
|
|
67
|
+
def bar
|
|
68
|
+
x = (42)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
"#;
|
|
72
|
+
let genv = analyze(source);
|
|
73
|
+
let info = genv
|
|
74
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
75
|
+
.expect("Foo#bar should be registered");
|
|
76
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
77
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn test_parenthesized_string() {
|
|
82
|
+
let source = r#"
|
|
83
|
+
class Foo
|
|
84
|
+
def bar
|
|
85
|
+
x = ("hello")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
"#;
|
|
89
|
+
let genv = analyze(source);
|
|
90
|
+
let info = genv
|
|
91
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
92
|
+
.expect("Foo#bar should be registered");
|
|
93
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
94
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn test_parenthesized_multiple_statements() {
|
|
99
|
+
let source = r#"
|
|
100
|
+
class Foo
|
|
101
|
+
def bar
|
|
102
|
+
x = (a = 1; "hello")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
"#;
|
|
106
|
+
let genv = analyze(source);
|
|
107
|
+
let info = genv
|
|
108
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
109
|
+
.expect("Foo#bar should be registered");
|
|
110
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
111
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//! Return statement handling
|
|
2
|
+
//!
|
|
3
|
+
//! Processes `return expr` by connecting the expression's vertex
|
|
4
|
+
//! to the enclosing method's merge vertex.
|
|
5
|
+
|
|
6
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
7
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
8
|
+
|
|
9
|
+
use super::install::install_node;
|
|
10
|
+
|
|
11
|
+
/// Process ReturnNode: connect return value to method's merge vertex
|
|
12
|
+
pub(crate) fn process_return_node(
|
|
13
|
+
genv: &mut GlobalEnv,
|
|
14
|
+
lenv: &mut LocalEnv,
|
|
15
|
+
changes: &mut ChangeSet,
|
|
16
|
+
source: &str,
|
|
17
|
+
return_node: &ruby_prism::ReturnNode,
|
|
18
|
+
) -> Option<VertexId> {
|
|
19
|
+
// Process return value (first argument only; multi-value return not yet supported)
|
|
20
|
+
let value_vtx = if let Some(arguments) = return_node.arguments() {
|
|
21
|
+
arguments
|
|
22
|
+
.arguments()
|
|
23
|
+
.iter()
|
|
24
|
+
.next()
|
|
25
|
+
.and_then(|arg| install_node(genv, lenv, changes, source, &arg))
|
|
26
|
+
} else {
|
|
27
|
+
// `return` without value → nil
|
|
28
|
+
Some(genv.new_source(crate::types::Type::Nil))
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Connect return value to method's merge vertex
|
|
32
|
+
if let Some(vtx) = value_vtx {
|
|
33
|
+
if let Some(merge_vtx) = genv.scope_manager.current_method_return_vertex() {
|
|
34
|
+
genv.add_edge(vtx, merge_vtx);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
None
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
mod tests {
|
|
43
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
44
|
+
use crate::graph::ChangeSet;
|
|
45
|
+
use crate::parser::ParseSession;
|
|
46
|
+
use crate::types::Type;
|
|
47
|
+
|
|
48
|
+
fn setup_and_infer(source: &str) -> GlobalEnv {
|
|
49
|
+
let session = ParseSession::new();
|
|
50
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
51
|
+
let root = parse_result.node();
|
|
52
|
+
let program = root.as_program_node().unwrap();
|
|
53
|
+
|
|
54
|
+
let mut genv = GlobalEnv::new();
|
|
55
|
+
let mut lenv = LocalEnv::new();
|
|
56
|
+
let mut changes = ChangeSet::new();
|
|
57
|
+
|
|
58
|
+
for stmt in &program.statements().body() {
|
|
59
|
+
crate::analyzer::install::install_node(
|
|
60
|
+
&mut genv, &mut lenv, &mut changes, source, &stmt,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
genv.apply_changes(changes);
|
|
65
|
+
genv.run_all();
|
|
66
|
+
genv
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn get_return_type(genv: &GlobalEnv, class_name: &str, method_name: &str) -> String {
|
|
70
|
+
let info = genv
|
|
71
|
+
.resolve_method(&Type::instance(class_name), method_name)
|
|
72
|
+
.unwrap_or_else(|| panic!("{}#{} should be registered", class_name, method_name));
|
|
73
|
+
let vtx = info
|
|
74
|
+
.return_vertex
|
|
75
|
+
.expect("return_vertex should be Some");
|
|
76
|
+
|
|
77
|
+
if let Some(source) = genv.get_source(vtx) {
|
|
78
|
+
source.ty.show()
|
|
79
|
+
} else if let Some(vertex) = genv.get_vertex(vtx) {
|
|
80
|
+
vertex.show()
|
|
81
|
+
} else {
|
|
82
|
+
panic!("return_vertex not found");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_simple_return() {
|
|
88
|
+
let source = r#"
|
|
89
|
+
class Foo
|
|
90
|
+
def bar
|
|
91
|
+
return "hello"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
"#;
|
|
95
|
+
let genv = setup_and_infer(source);
|
|
96
|
+
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn test_return_with_implicit_return_union() {
|
|
101
|
+
let source = r#"
|
|
102
|
+
class Foo
|
|
103
|
+
def bar
|
|
104
|
+
return "hello" if true
|
|
105
|
+
42
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
"#;
|
|
109
|
+
let genv = setup_and_infer(source);
|
|
110
|
+
let ty = get_return_type(&genv, "Foo", "bar");
|
|
111
|
+
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
|
|
112
|
+
assert!(ty.contains("String"), "should contain String, got: {}", ty);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn test_multiple_returns() {
|
|
117
|
+
let source = r#"
|
|
118
|
+
class Foo
|
|
119
|
+
def bar
|
|
120
|
+
return "a" if true
|
|
121
|
+
return :b if false
|
|
122
|
+
42
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
"#;
|
|
126
|
+
let genv = setup_and_infer(source);
|
|
127
|
+
let ty = get_return_type(&genv, "Foo", "bar");
|
|
128
|
+
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
|
|
129
|
+
assert!(ty.contains("String"), "should contain String, got: {}", ty);
|
|
130
|
+
assert!(ty.contains("Symbol"), "should contain Symbol, got: {}", ty);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn test_return_without_value() {
|
|
135
|
+
let source = r#"
|
|
136
|
+
class Foo
|
|
137
|
+
def bar
|
|
138
|
+
return if true
|
|
139
|
+
42
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
"#;
|
|
143
|
+
let genv = setup_and_infer(source);
|
|
144
|
+
let ty = get_return_type(&genv, "Foo", "bar");
|
|
145
|
+
assert!(ty.contains("Integer"), "should contain Integer, got: {}", ty);
|
|
146
|
+
assert!(ty.contains("nil"), "should contain nil, got: {}", ty);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn test_no_return_backward_compat() {
|
|
151
|
+
let source = r#"
|
|
152
|
+
class Foo
|
|
153
|
+
def bar
|
|
154
|
+
"hello"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
"#;
|
|
158
|
+
let genv = setup_and_infer(source);
|
|
159
|
+
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn test_return_only_method() {
|
|
164
|
+
let source = r#"
|
|
165
|
+
class Foo
|
|
166
|
+
def bar
|
|
167
|
+
return "hello"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
"#;
|
|
171
|
+
let genv = setup_and_infer(source);
|
|
172
|
+
assert_eq!(get_return_type(&genv, "Foo", "bar"), "String");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn test_return_dead_code_over_approximation() {
|
|
177
|
+
let source = r#"
|
|
178
|
+
class Foo
|
|
179
|
+
def bar
|
|
180
|
+
return "hello"
|
|
181
|
+
42
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
"#;
|
|
185
|
+
let genv = setup_and_infer(source);
|
|
186
|
+
let ty = get_return_type(&genv, "Foo", "bar");
|
|
187
|
+
// Dead code after return is still processed (over-approximation)
|
|
188
|
+
assert!(ty.contains("Integer"), "should contain Integer (dead code), got: {}", ty);
|
|
189
|
+
assert!(ty.contains("String"), "should contain String, got: {}", ty);
|
|
190
|
+
}
|
|
191
|
+
}
|
data/rust/src/checker.rs
CHANGED
|
@@ -17,8 +17,7 @@ impl FileChecker {
|
|
|
17
17
|
// Just verify cache exists
|
|
18
18
|
use crate::cache::RbsCache;
|
|
19
19
|
RbsCache::load().context(
|
|
20
|
-
"Failed to load RBS cache.
|
|
21
|
-
ruby -rmethodray -e 'MethodRay::Analyzer.new(\".\").infer_types(\"x=1\")'",
|
|
20
|
+
"Failed to load RBS cache.",
|
|
22
21
|
)?;
|
|
23
22
|
|
|
24
23
|
Ok(Self {})
|
|
@@ -70,8 +69,7 @@ fn load_rbs_from_cache(genv: &mut GlobalEnv) -> Result<()> {
|
|
|
70
69
|
use crate::types::Type;
|
|
71
70
|
|
|
72
71
|
let cache = RbsCache::load().context(
|
|
73
|
-
"Failed to load RBS cache.
|
|
74
|
-
ruby -rmethodray -e 'MethodRay::Analyzer.new(\".\").infer_types(\"x=1\")'",
|
|
72
|
+
"Failed to load RBS cache.",
|
|
75
73
|
)?;
|
|
76
74
|
|
|
77
75
|
let methods = cache.methods();
|
data/rust/src/cli/args.rs
CHANGED
data/rust/src/env/global_env.rs
CHANGED
|
@@ -209,13 +209,12 @@ impl GlobalEnv {
|
|
|
209
209
|
/// Enter a method scope
|
|
210
210
|
pub fn enter_method(&mut self, name: String) -> ScopeId {
|
|
211
211
|
// Look for class or module context
|
|
212
|
-
let receiver_type = self
|
|
213
|
-
|
|
214
|
-
.current_class_name()
|
|
215
|
-
.or_else(|| self.scope_manager.current_module_name());
|
|
212
|
+
let receiver_type = self.scope_manager.current_qualified_name();
|
|
213
|
+
let return_vertex = Some(self.new_vertex());
|
|
216
214
|
let scope_id = self.scope_manager.new_scope(ScopeKind::Method {
|
|
217
215
|
name,
|
|
218
216
|
receiver_type,
|
|
217
|
+
return_vertex,
|
|
219
218
|
});
|
|
220
219
|
self.scope_manager.enter_scope(scope_id);
|
|
221
220
|
scope_id
|
data/rust/src/env/scope.rs
CHANGED
|
@@ -20,6 +20,7 @@ pub enum ScopeKind {
|
|
|
20
20
|
Method {
|
|
21
21
|
name: String,
|
|
22
22
|
receiver_type: Option<String>, // Receiver class/module name
|
|
23
|
+
return_vertex: Option<VertexId>, // Merge vertex for return statements
|
|
23
24
|
},
|
|
24
25
|
Block,
|
|
25
26
|
}
|
|
@@ -309,6 +310,22 @@ impl ScopeManager {
|
|
|
309
310
|
Some(result)
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
/// Get return_vertex from the nearest enclosing method scope
|
|
314
|
+
pub fn current_method_return_vertex(&self) -> Option<VertexId> {
|
|
315
|
+
let mut current = Some(self.current_scope);
|
|
316
|
+
while let Some(scope_id) = current {
|
|
317
|
+
if let Some(scope) = self.scopes.get(&scope_id) {
|
|
318
|
+
if let ScopeKind::Method { return_vertex, .. } = &scope.kind {
|
|
319
|
+
return *return_vertex;
|
|
320
|
+
}
|
|
321
|
+
current = scope.parent;
|
|
322
|
+
} else {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
None
|
|
327
|
+
}
|
|
328
|
+
|
|
312
329
|
/// Lookup instance variable in enclosing module scope
|
|
313
330
|
pub fn lookup_instance_var_in_module(&self, name: &str) -> Option<VertexId> {
|
|
314
331
|
let mut current = Some(self.current_scope);
|
|
@@ -448,6 +465,7 @@ mod tests {
|
|
|
448
465
|
let method_id = sm.new_scope(ScopeKind::Method {
|
|
449
466
|
name: "test".to_string(),
|
|
450
467
|
receiver_type: None,
|
|
468
|
+
return_vertex: None,
|
|
451
469
|
});
|
|
452
470
|
sm.enter_scope(method_id);
|
|
453
471
|
|
|
@@ -472,6 +490,7 @@ mod tests {
|
|
|
472
490
|
let method_id = sm.new_scope(ScopeKind::Method {
|
|
473
491
|
name: "helper".to_string(),
|
|
474
492
|
receiver_type: Some("Utils".to_string()),
|
|
493
|
+
return_vertex: None,
|
|
475
494
|
});
|
|
476
495
|
sm.enter_scope(method_id);
|
|
477
496
|
|
|
@@ -500,6 +519,7 @@ mod tests {
|
|
|
500
519
|
let method_id = sm.new_scope(ScopeKind::Method {
|
|
501
520
|
name: "get_setting".to_string(),
|
|
502
521
|
receiver_type: Some("Config".to_string()),
|
|
522
|
+
return_vertex: None,
|
|
503
523
|
});
|
|
504
524
|
sm.enter_scope(method_id);
|
|
505
525
|
|
|
@@ -559,6 +579,7 @@ mod tests {
|
|
|
559
579
|
let method_id = sm.new_scope(ScopeKind::Method {
|
|
560
580
|
name: "greet".to_string(),
|
|
561
581
|
receiver_type: None,
|
|
582
|
+
return_vertex: None,
|
|
562
583
|
});
|
|
563
584
|
sm.enter_scope(method_id);
|
|
564
585
|
|
data/rust/src/graph/box.rs
CHANGED
|
@@ -132,8 +132,10 @@ impl BoxTrait for MethodCallBox {
|
|
|
132
132
|
self.location.clone(),
|
|
133
133
|
);
|
|
134
134
|
} else if matches!(&recv_ty, Type::Singleton { .. }) {
|
|
135
|
-
// Skip error for unknown class methods on Singleton types
|
|
136
|
-
//
|
|
135
|
+
// Skip error for unknown class methods on Singleton types.
|
|
136
|
+
// User-defined class methods (def self.foo) are resolved by
|
|
137
|
+
// resolve_method above. Only unresolved methods reach here
|
|
138
|
+
// (e.g., RBS class methods not yet supported).
|
|
137
139
|
continue;
|
|
138
140
|
} else {
|
|
139
141
|
// Record type error for diagnostic reporting
|
data/rust/src/main.rs
CHANGED
data/rust/src/rbs/loader.rs
CHANGED
|
@@ -37,12 +37,11 @@ impl<'a> RbsLoader<'a> {
|
|
|
37
37
|
|
|
38
38
|
/// Load all method definitions from RBS
|
|
39
39
|
pub fn load_methods(&self) -> Result<Vec<RbsMethodInfo>, RbsError> {
|
|
40
|
-
// Load method_loader.rb
|
|
41
|
-
let
|
|
42
|
-
let load_code = format!("require '{}'", rb_path);
|
|
40
|
+
// Load method_loader.rb (embedded at compile time to avoid hardcoded paths)
|
|
41
|
+
let ruby_code = include_str!("method_loader.rb");
|
|
43
42
|
let _: Value = self
|
|
44
43
|
.ruby
|
|
45
|
-
.eval(
|
|
44
|
+
.eval(ruby_code)
|
|
46
45
|
.map_err(|e| RbsError::LoadError(format!("Failed to load method_loader.rb: {}", e)))?;
|
|
47
46
|
|
|
48
47
|
// Instantiate Rbs::MethodLoader class and call method
|
data/rust/src/rbs/mod.rs
CHANGED
|
@@ -14,3 +14,32 @@ pub mod loader;
|
|
|
14
14
|
pub use error::RbsError;
|
|
15
15
|
#[cfg(feature = "ruby-ffi")]
|
|
16
16
|
pub use loader::{register_rbs_methods, RbsLoader, RbsMethodInfo};
|
|
17
|
+
|
|
18
|
+
#[cfg(test)]
|
|
19
|
+
mod tests {
|
|
20
|
+
#[test]
|
|
21
|
+
fn test_embedded_method_loader_contains_expected_class() {
|
|
22
|
+
let ruby_code = include_str!("method_loader.rb");
|
|
23
|
+
assert!(
|
|
24
|
+
ruby_code.contains("class MethodLoader"),
|
|
25
|
+
"Embedded Ruby code should contain MethodLoader class definition"
|
|
26
|
+
);
|
|
27
|
+
assert!(
|
|
28
|
+
ruby_code.contains("def load_methods"),
|
|
29
|
+
"Embedded Ruby code should contain load_methods method"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn test_embedded_method_loader_has_no_absolute_paths() {
|
|
35
|
+
let ruby_code = include_str!("method_loader.rb");
|
|
36
|
+
let forbidden_patterns = ["/home/runner/", "/Users/", "/tmp/build/"];
|
|
37
|
+
for pattern in &forbidden_patterns {
|
|
38
|
+
assert!(
|
|
39
|
+
!ruby_code.contains(pattern),
|
|
40
|
+
"Embedded Ruby code should not contain absolute path: {}",
|
|
41
|
+
pattern
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: method-ray
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dak2
|
|
@@ -57,7 +57,10 @@ files:
|
|
|
57
57
|
- rust/src/analyzer/install.rs
|
|
58
58
|
- rust/src/analyzer/literals.rs
|
|
59
59
|
- rust/src/analyzer/mod.rs
|
|
60
|
+
- rust/src/analyzer/operators.rs
|
|
60
61
|
- rust/src/analyzer/parameters.rs
|
|
62
|
+
- rust/src/analyzer/parentheses.rs
|
|
63
|
+
- rust/src/analyzer/returns.rs
|
|
61
64
|
- rust/src/analyzer/variables.rs
|
|
62
65
|
- rust/src/cache/mod.rs
|
|
63
66
|
- rust/src/cache/rbs_cache.rs
|