method-ray 0.1.3 → 0.1.5

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.
@@ -3,60 +3,239 @@
3
3
  //! This module is responsible for:
4
4
  //! - String, Integer, Float, Regexp literals
5
5
  //! - nil, true, false, Symbol literals
6
+ //! - Array, Hash, Range literals with element type inference
6
7
  //! - Creating Source vertices with fixed types
7
- //!
8
- //! Note: Array, Hash, and Range literals are handled in install.rs for element type inference
9
8
 
10
- use crate::env::GlobalEnv;
11
- use crate::graph::VertexId;
9
+ use crate::env::{GlobalEnv, LocalEnv};
10
+ use crate::graph::{ChangeSet, VertexId};
12
11
  use crate::types::Type;
13
12
  use ruby_prism::Node;
13
+ use std::collections::HashSet;
14
14
 
15
- /// Install literal nodes and return their VertexId
16
- ///
17
- /// Note: Array and Hash literals are NOT handled here because they require
18
- /// child processing for element type inference. See install.rs.
19
- pub fn install_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId> {
20
- // "hello"
21
- if node.as_string_node().is_some() {
15
+ /// Install literal node (including complex types: Array, Hash, Range)
16
+ pub(crate) fn install_literal_node(
17
+ genv: &mut GlobalEnv,
18
+ lenv: &mut LocalEnv,
19
+ changes: &mut ChangeSet,
20
+ source: &str,
21
+ node: &Node,
22
+ ) -> Option<VertexId> {
23
+ if node.as_array_node().is_some() {
24
+ let elements: Vec<Node> = node.as_array_node().unwrap().elements().iter().collect();
25
+ return install_array_literal_elements(genv, lenv, changes, source, elements);
26
+ }
27
+
28
+ if node.as_hash_node().is_some() {
29
+ let elements: Vec<Node> = node.as_hash_node().unwrap().elements().iter().collect();
30
+ return install_hash_literal_elements(genv, lenv, changes, source, elements);
31
+ }
32
+
33
+ if let Some(range_node) = node.as_range_node() {
34
+ return install_range_literal(genv, lenv, changes, source, &range_node);
35
+ }
36
+
37
+ // InterpolatedStringNode: "Hello, #{expr}" → String
38
+ if let Some(interp) = node.as_interpolated_string_node() {
39
+ for part in &interp.parts() {
40
+ super::install::install_node(genv, lenv, changes, source, &part);
41
+ }
22
42
  return Some(genv.new_source(Type::string()));
23
43
  }
24
44
 
25
- // 42
45
+ // InterpolatedSymbolNode: :"hello_#{expr}" → Symbol
46
+ if let Some(interp) = node.as_interpolated_symbol_node() {
47
+ for part in &interp.parts() {
48
+ super::install::install_node(genv, lenv, changes, source, &part);
49
+ }
50
+ return Some(genv.new_source(Type::symbol()));
51
+ }
52
+
53
+ // InterpolatedRegularExpressionNode: /hello #{expr}/ → Regexp
54
+ if let Some(interp) = node.as_interpolated_regular_expression_node() {
55
+ for part in &interp.parts() {
56
+ super::install::install_node(genv, lenv, changes, source, &part);
57
+ }
58
+ return Some(genv.new_source(Type::regexp()));
59
+ }
60
+
61
+ install_simple_literal(genv, node)
62
+ }
63
+
64
+ /// Install simple literal nodes (String, Integer, Float, nil, true, false, Symbol, Regexp)
65
+ fn install_simple_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId> {
66
+ if node.as_string_node().is_some() {
67
+ return Some(genv.new_source(Type::string()));
68
+ }
26
69
  if node.as_integer_node().is_some() {
27
70
  return Some(genv.new_source(Type::integer()));
28
71
  }
29
-
30
- // 3.14
31
72
  if node.as_float_node().is_some() {
32
73
  return Some(genv.new_source(Type::float()));
33
74
  }
34
-
35
- // nil
36
75
  if node.as_nil_node().is_some() {
37
76
  return Some(genv.new_source(Type::Nil));
38
77
  }
39
-
40
- // true
41
78
  if node.as_true_node().is_some() {
42
79
  return Some(genv.new_source(Type::instance("TrueClass")));
43
80
  }
44
-
45
- // false
46
81
  if node.as_false_node().is_some() {
47
82
  return Some(genv.new_source(Type::instance("FalseClass")));
48
83
  }
49
-
50
- // :symbol
51
84
  if node.as_symbol_node().is_some() {
52
85
  return Some(genv.new_source(Type::symbol()));
53
86
  }
54
-
55
- // /pattern/
56
87
  if node.as_regular_expression_node().is_some() {
57
88
  return Some(genv.new_source(Type::regexp()));
58
89
  }
90
+ None
91
+ }
92
+
93
+ /// Install array literal with element type inference
94
+ fn install_array_literal_elements(
95
+ genv: &mut GlobalEnv,
96
+ lenv: &mut LocalEnv,
97
+ changes: &mut ChangeSet,
98
+ source: &str,
99
+ elements: Vec<Node>,
100
+ ) -> Option<VertexId> {
101
+ if elements.is_empty() {
102
+ return Some(genv.new_source(Type::array()));
103
+ }
104
+
105
+ let mut element_types: HashSet<Type> = HashSet::new();
59
106
 
107
+ for element in &elements {
108
+ if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, element) {
109
+ if let Some(src) = genv.get_source(vtx) {
110
+ element_types.insert(src.ty.clone());
111
+ } else if let Some(vertex) = genv.get_vertex(vtx) {
112
+ for ty in vertex.types.keys() {
113
+ element_types.insert(ty.clone());
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ let array_type = if element_types.is_empty() {
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
+ } else {
125
+ let types_vec: Vec<Type> = element_types.into_iter().collect();
126
+ let union_type = Type::Union(types_vec);
127
+ Type::array_of(union_type)
128
+ };
129
+
130
+ Some(genv.new_source(array_type))
131
+ }
132
+
133
+ /// Install hash literal with key/value type inference
134
+ fn install_hash_literal_elements(
135
+ genv: &mut GlobalEnv,
136
+ lenv: &mut LocalEnv,
137
+ changes: &mut ChangeSet,
138
+ source: &str,
139
+ elements: Vec<Node>,
140
+ ) -> Option<VertexId> {
141
+ if elements.is_empty() {
142
+ return Some(genv.new_source(Type::hash()));
143
+ }
144
+
145
+ let mut key_types: HashSet<Type> = HashSet::new();
146
+ let mut value_types: HashSet<Type> = HashSet::new();
147
+
148
+ for element in &elements {
149
+ if let Some(assoc_node) = element.as_assoc_node() {
150
+ if let Some(key_vtx) =
151
+ super::install::install_node(genv, lenv, changes, source, &assoc_node.key())
152
+ {
153
+ if let Some(src) = genv.get_source(key_vtx) {
154
+ key_types.insert(src.ty.clone());
155
+ } else if let Some(vertex) = genv.get_vertex(key_vtx) {
156
+ for ty in vertex.types.keys() {
157
+ key_types.insert(ty.clone());
158
+ }
159
+ }
160
+ }
161
+
162
+ if let Some(value_vtx) =
163
+ super::install::install_node(genv, lenv, changes, source, &assoc_node.value())
164
+ {
165
+ if let Some(src) = genv.get_source(value_vtx) {
166
+ value_types.insert(src.ty.clone());
167
+ } else if let Some(vertex) = genv.get_vertex(value_vtx) {
168
+ for ty in vertex.types.keys() {
169
+ value_types.insert(ty.clone());
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ let hash_type = if key_types.is_empty() || value_types.is_empty() {
177
+ Type::hash()
178
+ } else {
179
+ let key_type = if key_types.len() == 1 {
180
+ key_types.into_iter().next().unwrap()
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
+ };
191
+ Type::hash_of(key_type, value_type)
192
+ };
193
+
194
+ Some(genv.new_source(hash_type))
195
+ }
196
+
197
+ /// Install range literal with element type inference
198
+ fn install_range_literal(
199
+ genv: &mut GlobalEnv,
200
+ lenv: &mut LocalEnv,
201
+ changes: &mut ChangeSet,
202
+ source: &str,
203
+ range_node: &ruby_prism::RangeNode,
204
+ ) -> Option<VertexId> {
205
+ let element_type = if let Some(left) = range_node.left() {
206
+ infer_range_element_type(genv, lenv, changes, source, &left)
207
+ } else if let Some(right) = range_node.right() {
208
+ infer_range_element_type(genv, lenv, changes, source, &right)
209
+ } else {
210
+ None
211
+ };
212
+
213
+ let range_type = match element_type {
214
+ Some(ty) => Type::range_of(ty),
215
+ None => Type::range(),
216
+ };
217
+
218
+ Some(genv.new_source(range_type))
219
+ }
220
+
221
+ /// Infer element type from a range endpoint node
222
+ fn infer_range_element_type(
223
+ genv: &mut GlobalEnv,
224
+ lenv: &mut LocalEnv,
225
+ changes: &mut ChangeSet,
226
+ source: &str,
227
+ node: &Node,
228
+ ) -> Option<Type> {
229
+ if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, node) {
230
+ if let Some(src) = genv.get_source(vtx) {
231
+ return Some(src.ty.clone());
232
+ }
233
+ if let Some(vertex) = genv.get_vertex(vtx) {
234
+ if let Some(ty) = vertex.types.keys().next() {
235
+ return Some(ty.clone());
236
+ }
237
+ }
238
+ }
60
239
  None
61
240
  }
62
241
 
@@ -67,9 +246,6 @@ mod tests {
67
246
  #[test]
68
247
  fn test_install_string_literal() {
69
248
  let mut genv = GlobalEnv::new();
70
-
71
- // Create a mock string node - we test via integration instead
72
- // Unit test just verifies the type creation
73
249
  let vtx = genv.new_source(Type::string());
74
250
  assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "String");
75
251
  }
@@ -77,7 +253,6 @@ mod tests {
77
253
  #[test]
78
254
  fn test_install_integer_literal() {
79
255
  let mut genv = GlobalEnv::new();
80
-
81
256
  let vtx = genv.new_source(Type::integer());
82
257
  assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Integer");
83
258
  }
@@ -85,7 +260,6 @@ mod tests {
85
260
  #[test]
86
261
  fn test_install_float_literal() {
87
262
  let mut genv = GlobalEnv::new();
88
-
89
263
  let vtx = genv.new_source(Type::float());
90
264
  assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Float");
91
265
  }
@@ -93,7 +267,27 @@ mod tests {
93
267
  #[test]
94
268
  fn test_install_regexp_literal() {
95
269
  let mut genv = GlobalEnv::new();
270
+ let vtx = genv.new_source(Type::regexp());
271
+ assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
272
+ }
273
+
274
+ #[test]
275
+ fn test_install_interpolated_string_literal() {
276
+ let mut genv = GlobalEnv::new();
277
+ let vtx = genv.new_source(Type::string());
278
+ assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "String");
279
+ }
280
+
281
+ #[test]
282
+ fn test_install_interpolated_symbol_literal() {
283
+ let mut genv = GlobalEnv::new();
284
+ let vtx = genv.new_source(Type::symbol());
285
+ assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Symbol");
286
+ }
96
287
 
288
+ #[test]
289
+ fn test_install_interpolated_regexp_literal() {
290
+ let mut genv = GlobalEnv::new();
97
291
  let vtx = genv.new_source(Type::regexp());
98
292
  assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
99
293
  }
@@ -1,10 +1,15 @@
1
+ mod attributes;
1
2
  mod blocks;
2
3
  mod calls;
4
+ mod conditionals;
3
5
  mod definitions;
4
6
  mod dispatch;
5
7
  mod install;
6
8
  mod literals;
9
+ mod operators;
7
10
  mod parameters;
11
+ mod parentheses;
12
+ mod returns;
8
13
  mod variables;
9
14
 
10
15
  pub use install::AstInstaller;
@@ -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
+ }
@@ -113,6 +113,70 @@ pub fn install_keyword_rest_parameter(
113
113
  param_vtx
114
114
  }
115
115
 
116
+ /// Install method parameters as local variables
117
+ ///
118
+ /// Returns a Vec of VertexId for required and optional parameters (positional),
119
+ /// which can be used for argument-to-parameter type propagation.
120
+ pub(crate) fn install_parameters(
121
+ genv: &mut GlobalEnv,
122
+ lenv: &mut LocalEnv,
123
+ changes: &mut ChangeSet,
124
+ source: &str,
125
+ params_node: &ruby_prism::ParametersNode,
126
+ ) -> Vec<VertexId> {
127
+ let mut param_vtxs = Vec::new();
128
+
129
+ // Required parameters: def foo(a, b)
130
+ for node in params_node.requireds().iter() {
131
+ if let Some(req_param) = node.as_required_parameter_node() {
132
+ let name = String::from_utf8_lossy(req_param.name().as_slice()).to_string();
133
+ let vtx = install_required_parameter(genv, lenv, name);
134
+ param_vtxs.push(vtx);
135
+ }
136
+ }
137
+
138
+ // Optional parameters: def foo(a = 1, b = "hello")
139
+ for node in params_node.optionals().iter() {
140
+ if let Some(opt_param) = node.as_optional_parameter_node() {
141
+ let name = String::from_utf8_lossy(opt_param.name().as_slice()).to_string();
142
+ let default_value = opt_param.value();
143
+
144
+ let vtx = if let Some(default_vtx) =
145
+ super::install::install_node(genv, lenv, changes, source, &default_value)
146
+ {
147
+ install_optional_parameter(genv, lenv, changes, name, default_vtx)
148
+ } else {
149
+ install_required_parameter(genv, lenv, name)
150
+ };
151
+ param_vtxs.push(vtx);
152
+ }
153
+ }
154
+
155
+ // Rest parameter: def foo(*args)
156
+ // Not included in param_vtxs (variadic args need special handling)
157
+ if let Some(rest_node) = params_node.rest() {
158
+ if let Some(rest_param) = rest_node.as_rest_parameter_node() {
159
+ if let Some(name_id) = rest_param.name() {
160
+ let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
161
+ install_rest_parameter(genv, lenv, name);
162
+ }
163
+ }
164
+ }
165
+
166
+ // Keyword rest parameter: def foo(**kwargs)
167
+ // Not included in param_vtxs (keyword args need special handling)
168
+ if let Some(kwrest_node) = params_node.keyword_rest() {
169
+ if let Some(kwrest_param) = kwrest_node.as_keyword_rest_parameter_node() {
170
+ if let Some(name_id) = kwrest_param.name() {
171
+ let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
172
+ install_keyword_rest_parameter(genv, lenv, name);
173
+ }
174
+ }
175
+ }
176
+
177
+ param_vtxs
178
+ }
179
+
116
180
  #[cfg(test)]
117
181
  mod tests {
118
182
  use super::*;
@@ -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
+ }