method-ray 0.1.8 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/{rust → core}/Cargo.toml +1 -1
  4. data/core/src/analyzer/assignments.rs +499 -0
  5. data/{rust → core}/src/analyzer/blocks.rs +140 -0
  6. data/{rust → core}/src/analyzer/definitions.rs +6 -5
  7. data/{rust → core}/src/analyzer/dispatch.rs +295 -31
  8. data/{rust → core}/src/analyzer/exceptions.rs +104 -3
  9. data/{rust → core}/src/analyzer/install.rs +16 -1
  10. data/{rust → core}/src/analyzer/literals.rs +3 -17
  11. data/{rust → core}/src/analyzer/loops.rs +126 -1
  12. data/{rust → core}/src/analyzer/mod.rs +1 -0
  13. data/{rust → core}/src/analyzer/parameters.rs +160 -0
  14. data/core/src/analyzer/super_calls.rs +285 -0
  15. data/{rust → core}/src/env/global_env.rs +18 -4
  16. data/{rust → core}/src/env/method_registry.rs +109 -23
  17. data/{rust → core}/src/env/scope.rs +78 -0
  18. data/{rust → core}/src/types.rs +11 -0
  19. data/ext/Cargo.toml +2 -2
  20. data/lib/methodray/binary_locator.rb +2 -2
  21. data/lib/methodray/commands.rb +1 -1
  22. data/lib/methodray/version.rb +1 -1
  23. metadata +54 -53
  24. data/rust/src/analyzer/assignments.rs +0 -152
  25. /data/{rust → core}/src/analyzer/attributes.rs +0 -0
  26. /data/{rust → core}/src/analyzer/calls.rs +0 -0
  27. /data/{rust → core}/src/analyzer/conditionals.rs +0 -0
  28. /data/{rust → core}/src/analyzer/operators.rs +0 -0
  29. /data/{rust → core}/src/analyzer/parentheses.rs +0 -0
  30. /data/{rust → core}/src/analyzer/returns.rs +0 -0
  31. /data/{rust → core}/src/analyzer/variables.rs +0 -0
  32. /data/{rust → core}/src/cache/mod.rs +0 -0
  33. /data/{rust → core}/src/cache/rbs_cache.rs +0 -0
  34. /data/{rust → core}/src/checker.rs +0 -0
  35. /data/{rust → core}/src/cli/args.rs +0 -0
  36. /data/{rust → core}/src/cli/commands.rs +0 -0
  37. /data/{rust → core}/src/cli/mod.rs +0 -0
  38. /data/{rust → core}/src/diagnostics/diagnostic.rs +0 -0
  39. /data/{rust → core}/src/diagnostics/formatter.rs +0 -0
  40. /data/{rust → core}/src/diagnostics/mod.rs +0 -0
  41. /data/{rust → core}/src/env/box_manager.rs +0 -0
  42. /data/{rust → core}/src/env/local_env.rs +0 -0
  43. /data/{rust → core}/src/env/mod.rs +0 -0
  44. /data/{rust → core}/src/env/type_error.rs +0 -0
  45. /data/{rust → core}/src/env/vertex_manager.rs +0 -0
  46. /data/{rust → core}/src/graph/box.rs +0 -0
  47. /data/{rust → core}/src/graph/change_set.rs +0 -0
  48. /data/{rust → core}/src/graph/mod.rs +0 -0
  49. /data/{rust → core}/src/graph/vertex.rs +0 -0
  50. /data/{rust → core}/src/lib.rs +0 -0
  51. /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
  52. /data/{rust → core}/src/lsp/main.rs +0 -0
  53. /data/{rust → core}/src/lsp/mod.rs +0 -0
  54. /data/{rust → core}/src/lsp/server.rs +0 -0
  55. /data/{rust → core}/src/main.rs +0 -0
  56. /data/{rust → core}/src/parser.rs +0 -0
  57. /data/{rust → core}/src/rbs/converter.rs +0 -0
  58. /data/{rust → core}/src/rbs/error.rs +0 -0
  59. /data/{rust → core}/src/rbs/loader.rs +0 -0
  60. /data/{rust → core}/src/rbs/mod.rs +0 -0
  61. /data/{rust → core}/src/source_map.rs +0 -0
@@ -128,6 +128,55 @@ fn install_block_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: Stri
128
128
  #[cfg(test)]
129
129
  mod tests {
130
130
  use super::*;
131
+ use crate::analyzer::install::AstInstaller;
132
+ use crate::env::LocalEnv;
133
+ use crate::parser::ParseSession;
134
+ use crate::types::Type;
135
+
136
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
137
+ if let Some(vertex) = genv.get_vertex(vtx) {
138
+ vertex.show()
139
+ } else if let Some(source) = genv.get_source(vtx) {
140
+ source.ty.show()
141
+ } else {
142
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
143
+ }
144
+ }
145
+
146
+ fn analyze_with_stdlib(source: &str) -> GlobalEnv {
147
+ let session = ParseSession::new();
148
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
149
+ let root = parse_result.node();
150
+ let program = root.as_program_node().unwrap();
151
+
152
+ let mut genv = GlobalEnv::new();
153
+
154
+ // Register stdlib methods needed for block tests
155
+ genv.register_builtin_method_with_block(
156
+ Type::array(),
157
+ "each",
158
+ Type::array(),
159
+ Some(vec![Type::instance("Elem")]),
160
+ );
161
+ genv.register_builtin_method_with_block(
162
+ Type::string(),
163
+ "each_char",
164
+ Type::string(),
165
+ Some(vec![Type::string()]),
166
+ );
167
+ genv.register_builtin_method(Type::integer(), "even?", Type::instance("TrueClass"));
168
+ genv.register_builtin_method(Type::string(), "upcase", Type::string());
169
+
170
+ let mut lenv = LocalEnv::new();
171
+
172
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
173
+ for stmt in &program.statements().body() {
174
+ installer.install_node(&stmt);
175
+ }
176
+ installer.finish();
177
+
178
+ genv
179
+ }
131
180
 
132
181
  #[test]
133
182
  fn test_enter_exit_block_scope() {
@@ -173,4 +222,95 @@ mod tests {
173
222
 
174
223
  exit_block_scope(&mut genv);
175
224
  }
225
+
226
+ #[test]
227
+ fn test_block_parameter_type_from_array() {
228
+ let source = r#"
229
+ class Foo
230
+ def bar
231
+ [1, 2, 3].each { |x| x.even? }
232
+ end
233
+ end
234
+ "#;
235
+ let genv = analyze_with_stdlib(source);
236
+ assert!(
237
+ genv.type_errors.is_empty(),
238
+ "x.even? should not produce type errors: {:?}",
239
+ genv.type_errors
240
+ );
241
+ // Verify bar returns Array (each returns its receiver)
242
+ let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap();
243
+ let ret_vtx = info.return_vertex.unwrap();
244
+ assert_eq!(get_type_show(&genv, ret_vtx), "Array");
245
+ }
246
+
247
+ #[test]
248
+ fn test_block_external_variable_access() {
249
+ let source = r#"
250
+ class Foo
251
+ def bar
252
+ y = "hello"
253
+ [1].each { y.upcase }
254
+ end
255
+ end
256
+ "#;
257
+ let genv = analyze_with_stdlib(source);
258
+ assert!(
259
+ genv.type_errors.is_empty(),
260
+ "y.upcase should not produce type errors: {:?}",
261
+ genv.type_errors
262
+ );
263
+ }
264
+
265
+ #[test]
266
+ fn test_block_parameter_from_each_char() {
267
+ let source = r#"
268
+ class Foo
269
+ def bar
270
+ "hello".each_char { |c| c.upcase }
271
+ end
272
+ end
273
+ "#;
274
+ let genv = analyze_with_stdlib(source);
275
+ assert!(
276
+ genv.type_errors.is_empty(),
277
+ "c.upcase should not produce type errors: {:?}",
278
+ genv.type_errors
279
+ );
280
+ }
281
+
282
+ #[test]
283
+ fn test_block_body_does_not_affect_method_return() {
284
+ let source = r#"
285
+ class Foo
286
+ def bar
287
+ [1, 2].each { |x| "string" }
288
+ end
289
+ end
290
+ "#;
291
+ let genv = analyze_with_stdlib(source);
292
+ // each returns its receiver (Array), not the block body result (String)
293
+ let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap();
294
+ let ret_vtx = info.return_vertex.unwrap();
295
+ assert_eq!(get_type_show(&genv, ret_vtx), "Array");
296
+ }
297
+
298
+ #[test]
299
+ fn test_nested_blocks() {
300
+ let source = r#"
301
+ class Foo
302
+ def bar
303
+ [1, 2].each { |x|
304
+ "hello".each_char { |c| c.upcase }
305
+ }
306
+ end
307
+ end
308
+ "#;
309
+ let genv = analyze_with_stdlib(source);
310
+ assert!(
311
+ genv.type_errors.is_empty(),
312
+ "nested block should not produce type errors: {:?}",
313
+ genv.type_errors
314
+ );
315
+ }
176
316
  }
@@ -26,7 +26,8 @@ pub(crate) fn process_class_node(
26
26
  class_node: &ruby_prism::ClassNode,
27
27
  ) -> Option<VertexId> {
28
28
  let class_name = extract_class_name(class_node);
29
- install_class(genv, class_name);
29
+ let superclass = class_node.superclass().and_then(|sup| extract_constant_path(&sup));
30
+ install_class(genv, class_name, superclass.as_deref());
30
31
 
31
32
  if let Some(body) = class_node.body() {
32
33
  if let Some(statements) = body.as_statements_node() {
@@ -126,8 +127,8 @@ pub(crate) fn process_def_node(
126
127
  }
127
128
 
128
129
  /// Install class definition
129
- fn install_class(genv: &mut GlobalEnv, class_name: String) {
130
- genv.enter_class(class_name);
130
+ fn install_class(genv: &mut GlobalEnv, class_name: String, superclass: Option<&str>) {
131
+ genv.enter_class(class_name, superclass);
131
132
  }
132
133
 
133
134
  /// Install module definition
@@ -203,7 +204,7 @@ mod tests {
203
204
  fn test_enter_exit_class_scope() {
204
205
  let mut genv = GlobalEnv::new();
205
206
 
206
- install_class(&mut genv, "User".to_string());
207
+ install_class(&mut genv, "User".to_string(), None);
207
208
  assert_eq!(
208
209
  genv.scope_manager.current_class_name(),
209
210
  Some("User".to_string())
@@ -231,7 +232,7 @@ mod tests {
231
232
  fn test_nested_method_scope() {
232
233
  let mut genv = GlobalEnv::new();
233
234
 
234
- install_class(&mut genv, "User".to_string());
235
+ install_class(&mut genv, "User".to_string(), None);
235
236
  install_method(&mut genv, "greet".to_string());
236
237
 
237
238
  // Still in User class context
@@ -18,6 +18,45 @@ use super::variables::{
18
18
  install_self,
19
19
  };
20
20
 
21
+ /// Collect positional and keyword arguments from AST argument nodes.
22
+ ///
23
+ /// Shared by method calls (`dispatch.rs`) and super calls (`super_calls.rs`).
24
+ pub(crate) fn collect_arguments<'a>(
25
+ genv: &mut GlobalEnv,
26
+ lenv: &mut LocalEnv,
27
+ changes: &mut ChangeSet,
28
+ source: &str,
29
+ args: impl Iterator<Item = ruby_prism::Node<'a>>,
30
+ ) -> (Vec<VertexId>, Option<HashMap<String, VertexId>>) {
31
+ let mut positional: Vec<VertexId> = Vec::new();
32
+ let mut keyword: HashMap<String, VertexId> = HashMap::new();
33
+
34
+ for arg in args {
35
+ if let Some(kw_hash) = arg.as_keyword_hash_node() {
36
+ for element in kw_hash.elements().iter() {
37
+ let assoc = match element.as_assoc_node() {
38
+ Some(a) => a,
39
+ None => continue,
40
+ };
41
+ let name = match assoc.key().as_symbol_node() {
42
+ Some(sym) => bytes_to_name(sym.unescaped()),
43
+ None => continue,
44
+ };
45
+ if let Some(vtx) =
46
+ super::install::install_node(genv, lenv, changes, source, &assoc.value())
47
+ {
48
+ keyword.insert(name, vtx);
49
+ }
50
+ }
51
+ } else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, &arg) {
52
+ positional.push(vtx);
53
+ }
54
+ }
55
+
56
+ let kw = (!keyword.is_empty()).then_some(keyword);
57
+ (positional, kw)
58
+ }
59
+
21
60
  /// Kind of attr_* declaration
22
61
  #[derive(Debug, Clone, Copy)]
23
62
  pub enum AttrKind {
@@ -62,6 +101,10 @@ pub enum NeedsChildKind<'a> {
62
101
  kind: AttrKind,
63
102
  attr_names: Vec<String>,
64
103
  },
104
+ /// include declaration: `include Greetable, Enumerable`
105
+ IncludeDeclaration {
106
+ module_names: Vec<String>,
107
+ },
65
108
  }
66
109
 
67
110
  /// First pass: check if node can be handled immediately without child processing
@@ -189,6 +232,25 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
189
232
  return None;
190
233
  }
191
234
 
235
+ if method_name == "include" {
236
+ let module_names: Vec<String> = call_node
237
+ .arguments()
238
+ .map(|args| {
239
+ args.arguments()
240
+ .iter()
241
+ .filter_map(|arg| {
242
+ super::definitions::extract_constant_path(&arg)
243
+ })
244
+ .collect()
245
+ })
246
+ .unwrap_or_default();
247
+
248
+ if !module_names.is_empty() {
249
+ return Some(NeedsChildKind::IncludeDeclaration { module_names });
250
+ }
251
+ return None;
252
+ }
253
+
192
254
  let prism_location = call_node
193
255
  .message_loc()
194
256
  .unwrap_or_else(|| node.location());
@@ -262,6 +324,16 @@ pub(crate) fn process_needs_child(
262
324
  super::attributes::process_attr_declaration(genv, kind, attr_names);
263
325
  None
264
326
  }
327
+ NeedsChildKind::IncludeDeclaration { module_names } => {
328
+ if let Some(class_name) = genv.scope_manager.current_qualified_name() {
329
+ // Ruby processes `include A, B` right-to-left (B first, then A on top),
330
+ // so A ends up with higher MRO priority. Reverse to match this behavior.
331
+ for module_name in module_names.iter().rev() {
332
+ genv.record_include(&class_name, module_name);
333
+ }
334
+ }
335
+ None
336
+ }
265
337
  }
266
338
  }
267
339
 
@@ -310,31 +382,8 @@ fn process_method_call_common<'a>(
310
382
  return Some(super::operators::process_not_operator(genv));
311
383
  }
312
384
 
313
- // Separate positional arguments and keyword arguments
314
- let mut positional_arg_vtxs: Vec<VertexId> = Vec::new();
315
- let mut keyword_arg_vtxs: HashMap<String, VertexId> = HashMap::new();
316
-
317
- for arg in &arguments {
318
- if let Some(kw_hash) = arg.as_keyword_hash_node() {
319
- for element in kw_hash.elements().iter() {
320
- let assoc = match element.as_assoc_node() {
321
- Some(a) => a,
322
- None => continue,
323
- };
324
- let name = match assoc.key().as_symbol_node() {
325
- Some(sym) => bytes_to_name(sym.unescaped()),
326
- None => continue,
327
- };
328
- if let Some(vtx) =
329
- super::install::install_node(genv, lenv, changes, source, &assoc.value())
330
- {
331
- keyword_arg_vtxs.insert(name, vtx);
332
- }
333
- }
334
- } else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, arg) {
335
- positional_arg_vtxs.push(vtx);
336
- }
337
- }
385
+ let (positional_arg_vtxs, kwarg_vtxs) =
386
+ collect_arguments(genv, lenv, changes, source, arguments.into_iter());
338
387
 
339
388
  if let Some(block_node) = block {
340
389
  if let Some(block) = block_node.as_block_node() {
@@ -355,12 +404,6 @@ fn process_method_call_common<'a>(
355
404
  }
356
405
  }
357
406
 
358
- let kwarg_vtxs = if keyword_arg_vtxs.is_empty() {
359
- None
360
- } else {
361
- Some(keyword_arg_vtxs)
362
- };
363
-
364
407
  Some(finish_method_call(
365
408
  genv,
366
409
  recv_vtx,
@@ -1134,4 +1177,225 @@ User.new.profile(name: "Alice", age: 30)
1134
1177
  let ret_vtx = info.return_vertex.unwrap();
1135
1178
  assert_eq!(get_type_show(&genv, ret_vtx), "String");
1136
1179
  }
1180
+
1181
+ // === Include (mixin) tests ===
1182
+
1183
+ // Test 31: Basic include — module method resolved on class instance
1184
+ #[test]
1185
+ fn test_include_basic() {
1186
+ let source = r#"
1187
+ module Greetable
1188
+ def greet
1189
+ "Hello!"
1190
+ end
1191
+ end
1192
+
1193
+ class User
1194
+ include Greetable
1195
+ end
1196
+
1197
+ User.new.greet
1198
+ "#;
1199
+ let genv = analyze(source);
1200
+ assert!(
1201
+ genv.type_errors.is_empty(),
1202
+ "include should resolve Greetable#greet: {:?}",
1203
+ genv.type_errors
1204
+ );
1205
+ }
1206
+
1207
+ // Test 32: Include method return type inference
1208
+ #[test]
1209
+ fn test_include_method_return_type() {
1210
+ let source = r#"
1211
+ module Greetable
1212
+ def greet
1213
+ "Hello!"
1214
+ end
1215
+ end
1216
+
1217
+ class User
1218
+ include Greetable
1219
+
1220
+ def say_hello
1221
+ greet
1222
+ end
1223
+ end
1224
+ "#;
1225
+ let genv = analyze(source);
1226
+
1227
+ let info = genv
1228
+ .resolve_method(&Type::instance("User"), "say_hello")
1229
+ .expect("User#say_hello should be registered");
1230
+ let ret_vtx = info.return_vertex.unwrap();
1231
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1232
+ }
1233
+
1234
+ // Test 33: Multiple includes — last included module has priority
1235
+ #[test]
1236
+ fn test_include_multiple_modules() {
1237
+ let source = r#"
1238
+ module A
1239
+ def foo
1240
+ "from A"
1241
+ end
1242
+ end
1243
+
1244
+ module B
1245
+ def foo
1246
+ 42
1247
+ end
1248
+ end
1249
+
1250
+ class User
1251
+ include A
1252
+ include B
1253
+ end
1254
+
1255
+ User.new.foo
1256
+ "#;
1257
+ let genv = analyze(source);
1258
+ assert!(genv.type_errors.is_empty());
1259
+
1260
+ // B is included last → B#foo (Integer) should be resolved
1261
+ let info = genv
1262
+ .resolve_method(&Type::instance("User"), "foo")
1263
+ .expect("User#foo should be resolved via include");
1264
+ let ret_vtx = info.return_vertex.unwrap();
1265
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1266
+ }
1267
+
1268
+ // Test 34: Class's own method takes priority over included module
1269
+ #[test]
1270
+ fn test_include_class_method_priority() {
1271
+ let source = r#"
1272
+ module Greetable
1273
+ def greet
1274
+ "Hello from module!"
1275
+ end
1276
+ end
1277
+
1278
+ class User
1279
+ include Greetable
1280
+
1281
+ def greet
1282
+ 42
1283
+ end
1284
+ end
1285
+
1286
+ User.new.greet
1287
+ "#;
1288
+ let genv = analyze(source);
1289
+
1290
+ let info = genv
1291
+ .resolve_method(&Type::instance("User"), "greet")
1292
+ .expect("User#greet should be resolved");
1293
+ let ret_vtx = info.return_vertex.unwrap();
1294
+ // Class's own method (Integer) takes priority
1295
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1296
+ }
1297
+
1298
+ // Test 35: Include with qualified module name
1299
+ #[test]
1300
+ fn test_include_qualified_module() {
1301
+ let source = r#"
1302
+ module Api
1303
+ module Helpers
1304
+ def help
1305
+ "help"
1306
+ end
1307
+ end
1308
+ end
1309
+
1310
+ class User
1311
+ include Api::Helpers
1312
+ end
1313
+
1314
+ User.new.help
1315
+ "#;
1316
+ let genv = analyze(source);
1317
+ assert!(
1318
+ genv.type_errors.is_empty(),
1319
+ "include Api::Helpers should resolve: {:?}",
1320
+ genv.type_errors
1321
+ );
1322
+ }
1323
+
1324
+ // Test 36: Include with simultaneous multiple modules
1325
+ #[test]
1326
+ fn test_include_simultaneous_multiple() {
1327
+ let source = r#"
1328
+ module A
1329
+ def a_method
1330
+ "a"
1331
+ end
1332
+ end
1333
+
1334
+ module B
1335
+ def b_method
1336
+ 42
1337
+ end
1338
+ end
1339
+
1340
+ class User
1341
+ include A, B
1342
+ end
1343
+
1344
+ User.new.a_method
1345
+ User.new.b_method
1346
+ "#;
1347
+ let genv = analyze(source);
1348
+ assert!(
1349
+ genv.type_errors.is_empty(),
1350
+ "include A, B should resolve both modules: {:?}",
1351
+ genv.type_errors
1352
+ );
1353
+ }
1354
+
1355
+ // Test 37: Include with unknown module (no crash)
1356
+ #[test]
1357
+ fn test_include_unknown_module() {
1358
+ let source = r#"
1359
+ class User
1360
+ include UnknownModule
1361
+ end
1362
+ "#;
1363
+ let genv = analyze(source);
1364
+ // Should not panic; unknown module is recorded but methods won't resolve
1365
+ let _ = genv;
1366
+ }
1367
+
1368
+ // Test 38: Simultaneous include A, B — A has higher MRO priority (Ruby semantics)
1369
+ #[test]
1370
+ fn test_include_simultaneous_order() {
1371
+ let source = r#"
1372
+ module A
1373
+ def foo
1374
+ "from A"
1375
+ end
1376
+ end
1377
+
1378
+ module B
1379
+ def foo
1380
+ 42
1381
+ end
1382
+ end
1383
+
1384
+ class User
1385
+ include A, B
1386
+ end
1387
+
1388
+ User.new.foo
1389
+ "#;
1390
+ let genv = analyze(source);
1391
+ assert!(genv.type_errors.is_empty());
1392
+
1393
+ // Ruby's `include A, B` processes right-to-left: B first, then A on top.
1394
+ // A has higher MRO priority → A#foo (String) should be resolved.
1395
+ let info = genv
1396
+ .resolve_method(&Type::instance("User"), "foo")
1397
+ .expect("User#foo should be resolved via include");
1398
+ let ret_vtx = info.return_vertex.unwrap();
1399
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1400
+ }
1137
1401
  }
@@ -81,6 +81,24 @@ fn process_rescue_chain(
81
81
  }
82
82
  }
83
83
 
84
+ /// Extract the exception type from rescue node's exception class list.
85
+ /// Falls back to StandardError when no exceptions are specified or none can be resolved.
86
+ // TODO: Non-constant exception expressions (method calls, splats, variables) are silently skipped.
87
+ fn extract_exception_type(rescue_node: &RescueNode) -> Type {
88
+ let types: Vec<Type> = rescue_node
89
+ .exceptions()
90
+ .iter()
91
+ .filter_map(|exc| super::definitions::extract_constant_path(&exc))
92
+ .map(|name| Type::instance(&name))
93
+ .collect();
94
+
95
+ if types.is_empty() {
96
+ Type::instance("StandardError")
97
+ } else {
98
+ Type::union_of(types)
99
+ }
100
+ }
101
+
84
102
  /// Process a single RescueNode body.
85
103
  /// Registers the rescue variable (=> e), processes the body,
86
104
  /// then removes the variable from scope.
@@ -97,14 +115,14 @@ fn process_rescue_body(
97
115
 
98
116
  // Save/restore rescue variable binding (=> e)
99
117
  // TODO: Only LocalVariableTargetNode is handled; instance/global/class vars are not yet supported.
100
- // TODO: Always typed as StandardError regardless of declared exception class.
101
118
  let var_binding = if let Some(ref_node) = rescue_node.reference() {
102
119
  ref_node.as_local_variable_target_node().map(|target| {
103
120
  let name = bytes_to_name(target.name().as_slice());
104
121
  let saved = lenv.get_var(&name);
105
122
  let exception_vtx = genv.new_vertex();
106
- let std_err_src = genv.new_source(Type::instance("StandardError"));
107
- genv.add_edge(std_err_src, exception_vtx);
123
+ let exception_type = extract_exception_type(rescue_node);
124
+ let exception_src = genv.new_source(exception_type);
125
+ genv.add_edge(exception_src, exception_vtx);
108
126
  lenv.new_var(name.clone(), exception_vtx);
109
127
  (name, saved)
110
128
  })
@@ -497,6 +515,89 @@ end
497
515
  );
498
516
  }
499
517
 
518
+ #[test]
519
+ fn test_rescue_specific_exception_class() {
520
+ let source = r#"
521
+ class Foo
522
+ def bar
523
+ begin
524
+ "hello"
525
+ rescue ArgumentError => e
526
+ e
527
+ end
528
+ end
529
+ end
530
+ "#;
531
+ let genv = analyze(source);
532
+ let info = genv
533
+ .resolve_method(&Type::instance("Foo"), "bar")
534
+ .expect("Foo#bar should be registered");
535
+ let ret_vtx = info.return_vertex.unwrap();
536
+ let type_str = get_type_show(&genv, ret_vtx);
537
+ assert!(
538
+ type_str.contains("ArgumentError"),
539
+ "should contain ArgumentError: {}",
540
+ type_str
541
+ );
542
+ }
543
+
544
+ #[test]
545
+ fn test_rescue_multiple_exception_classes() {
546
+ let source = r#"
547
+ class Foo
548
+ def bar
549
+ begin
550
+ "hello"
551
+ rescue TypeError, NameError => e
552
+ e
553
+ end
554
+ end
555
+ end
556
+ "#;
557
+ let genv = analyze(source);
558
+ let info = genv
559
+ .resolve_method(&Type::instance("Foo"), "bar")
560
+ .expect("Foo#bar should be registered");
561
+ let ret_vtx = info.return_vertex.unwrap();
562
+ let type_str = get_type_show(&genv, ret_vtx);
563
+ assert!(
564
+ type_str.contains("TypeError"),
565
+ "should contain TypeError: {}",
566
+ type_str
567
+ );
568
+ assert!(
569
+ type_str.contains("NameError"),
570
+ "should contain NameError: {}",
571
+ type_str
572
+ );
573
+ }
574
+
575
+ #[test]
576
+ fn test_rescue_qualified_exception_class() {
577
+ let source = r#"
578
+ class Foo
579
+ def bar
580
+ begin
581
+ "hello"
582
+ rescue Net::HTTPError => e
583
+ e
584
+ end
585
+ end
586
+ end
587
+ "#;
588
+ let genv = analyze(source);
589
+ let info = genv
590
+ .resolve_method(&Type::instance("Foo"), "bar")
591
+ .expect("Foo#bar should be registered");
592
+ let ret_vtx = info.return_vertex.unwrap();
593
+ let type_str = get_type_show(&genv, ret_vtx);
594
+ assert!(
595
+ type_str.contains("Net::HTTPError"),
596
+ "should contain Net::HTTPError: {}",
597
+ type_str
598
+ );
599
+ }
600
+
500
601
  #[test]
501
602
  fn test_empty_rescue_body_is_nil() {
502
603
  let source = r#"