method-ray 0.1.7 → 0.1.8

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.
@@ -0,0 +1,521 @@
1
+ //! Exceptions - begin/rescue/ensure type inference
2
+ //!
3
+ //! Collects types from each branch and merges them into a Union
4
+ //! via edges into a single result Vertex.
5
+ //! Applies the same MergeVertex pattern as conditionals.rs.
6
+
7
+ use crate::env::{GlobalEnv, LocalEnv};
8
+ use crate::graph::{ChangeSet, VertexId};
9
+ use crate::types::Type;
10
+ use ruby_prism::{BeginNode, RescueModifierNode, RescueNode};
11
+
12
+ use super::bytes_to_name;
13
+ use super::install::{install_node, install_statements};
14
+
15
+ /// Process BeginNode: begin/rescue/else/ensure
16
+ ///
17
+ /// Type aggregation rules:
18
+ /// - No rescue clause: return begin body type directly
19
+ /// - With else clause: else type + all rescue types → Union (begin body excluded)
20
+ /// - Without else clause: begin body type + all rescue types → Union
21
+ /// - Ensure clause: processed for side effects only, does not affect return type
22
+ pub(crate) fn process_begin_node(
23
+ genv: &mut GlobalEnv,
24
+ lenv: &mut LocalEnv,
25
+ changes: &mut ChangeSet,
26
+ source: &str,
27
+ begin_node: &BeginNode,
28
+ ) -> Option<VertexId> {
29
+ let begin_vtx = begin_node
30
+ .statements()
31
+ .and_then(|s| install_statements(genv, lenv, changes, source, &s));
32
+
33
+ let result = if let Some(rescue_node) = begin_node.rescue_clause() {
34
+ let result_vtx = genv.new_vertex();
35
+
36
+ process_rescue_chain(genv, lenv, changes, source, &rescue_node, result_vtx);
37
+
38
+ if let Some(else_node) = begin_node.else_clause() {
39
+ // With else: else type replaces begin body type (Ruby spec)
40
+ let else_vtx = else_node
41
+ .statements()
42
+ .and_then(|s| install_statements(genv, lenv, changes, source, &s));
43
+ if let Some(vtx) = else_vtx {
44
+ genv.add_edge(vtx, result_vtx);
45
+ }
46
+ } else if let Some(vtx) = begin_vtx {
47
+ genv.add_edge(vtx, result_vtx);
48
+ }
49
+
50
+ Some(result_vtx)
51
+ } else {
52
+ begin_vtx
53
+ };
54
+
55
+ // Ensure: side effects only, does not affect return type
56
+ if let Some(ensure_node) = begin_node.ensure_clause() {
57
+ if let Some(stmts) = ensure_node.statements() {
58
+ let _ = install_statements(genv, lenv, changes, source, &stmts);
59
+ }
60
+ }
61
+
62
+ result
63
+ }
64
+
65
+ /// Process RescueNode chain recursively.
66
+ /// Empty rescue body evaluates to nil.
67
+ fn process_rescue_chain(
68
+ genv: &mut GlobalEnv,
69
+ lenv: &mut LocalEnv,
70
+ changes: &mut ChangeSet,
71
+ source: &str,
72
+ rescue_node: &RescueNode,
73
+ result_vtx: VertexId,
74
+ ) {
75
+ let body_vtx = process_rescue_body(genv, lenv, changes, source, rescue_node);
76
+ let vtx = body_vtx.unwrap_or_else(|| genv.new_source(Type::Nil));
77
+ genv.add_edge(vtx, result_vtx);
78
+
79
+ if let Some(next) = rescue_node.subsequent() {
80
+ process_rescue_chain(genv, lenv, changes, source, &next, result_vtx);
81
+ }
82
+ }
83
+
84
+ /// Process a single RescueNode body.
85
+ /// Registers the rescue variable (=> e), processes the body,
86
+ /// then removes the variable from scope.
87
+ fn process_rescue_body(
88
+ genv: &mut GlobalEnv,
89
+ lenv: &mut LocalEnv,
90
+ changes: &mut ChangeSet,
91
+ source: &str,
92
+ rescue_node: &RescueNode,
93
+ ) -> Option<VertexId> {
94
+ for exc in &rescue_node.exceptions() {
95
+ install_node(genv, lenv, changes, source, &exc);
96
+ }
97
+
98
+ // Save/restore rescue variable binding (=> e)
99
+ // 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
+ let var_binding = if let Some(ref_node) = rescue_node.reference() {
102
+ ref_node.as_local_variable_target_node().map(|target| {
103
+ let name = bytes_to_name(target.name().as_slice());
104
+ let saved = lenv.get_var(&name);
105
+ 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);
108
+ lenv.new_var(name.clone(), exception_vtx);
109
+ (name, saved)
110
+ })
111
+ } else {
112
+ None
113
+ };
114
+
115
+ let body_vtx = rescue_node
116
+ .statements()
117
+ .and_then(|s| install_statements(genv, lenv, changes, source, &s));
118
+
119
+ if let Some((name, saved)) = var_binding {
120
+ match saved {
121
+ Some(prev_vtx) => lenv.new_var(name, prev_vtx),
122
+ None => lenv.remove_var(&name),
123
+ }
124
+ }
125
+
126
+ body_vtx
127
+ }
128
+
129
+ /// Process RescueModifierNode: `expression rescue rescue_expression`
130
+ pub(crate) fn process_rescue_modifier_node(
131
+ genv: &mut GlobalEnv,
132
+ lenv: &mut LocalEnv,
133
+ changes: &mut ChangeSet,
134
+ source: &str,
135
+ node: &RescueModifierNode,
136
+ ) -> Option<VertexId> {
137
+ let result_vtx = genv.new_vertex();
138
+
139
+ let expr_vtx = install_node(genv, lenv, changes, source, &node.expression());
140
+ if let Some(vtx) = expr_vtx {
141
+ genv.add_edge(vtx, result_vtx);
142
+ }
143
+
144
+ let rescue_vtx = install_node(genv, lenv, changes, source, &node.rescue_expression());
145
+ if let Some(vtx) = rescue_vtx {
146
+ genv.add_edge(vtx, result_vtx);
147
+ }
148
+
149
+ Some(result_vtx)
150
+ }
151
+
152
+ #[cfg(test)]
153
+ mod tests {
154
+ use crate::analyzer::install::AstInstaller;
155
+ use crate::env::{GlobalEnv, LocalEnv};
156
+ use crate::graph::VertexId;
157
+ use crate::parser::ParseSession;
158
+ use crate::types::Type;
159
+
160
+ /// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
161
+ fn analyze(source: &str) -> GlobalEnv {
162
+ let session = ParseSession::new();
163
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
164
+ let root = parse_result.node();
165
+ let program = root.as_program_node().unwrap();
166
+
167
+ let mut genv = GlobalEnv::new();
168
+ let mut lenv = LocalEnv::new();
169
+
170
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
171
+ for stmt in &program.statements().body() {
172
+ installer.install_node(&stmt);
173
+ }
174
+ installer.finish();
175
+
176
+ genv
177
+ }
178
+
179
+ /// Helper: get the type string for a vertex ID (checks both Vertex and Source)
180
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
181
+ if let Some(vertex) = genv.get_vertex(vtx) {
182
+ vertex.show()
183
+ } else if let Some(source) = genv.get_source(vtx) {
184
+ source.ty.show()
185
+ } else {
186
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
187
+ }
188
+ }
189
+
190
+ #[test]
191
+ fn test_begin_rescue_basic() {
192
+ let source = r#"
193
+ class Foo
194
+ def bar
195
+ begin
196
+ "hello"
197
+ rescue
198
+ 42
199
+ end
200
+ end
201
+ end
202
+ "#;
203
+ let genv = analyze(source);
204
+ let info = genv
205
+ .resolve_method(&Type::instance("Foo"), "bar")
206
+ .expect("Foo#bar should be registered");
207
+ let ret_vtx = info.return_vertex.unwrap();
208
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
209
+ }
210
+
211
+ #[test]
212
+ fn test_begin_rescue_else() {
213
+ let source = r#"
214
+ class Foo
215
+ def bar
216
+ begin
217
+ "hello"
218
+ rescue
219
+ 42
220
+ else
221
+ :ok
222
+ end
223
+ end
224
+ end
225
+ "#;
226
+ let genv = analyze(source);
227
+ let info = genv
228
+ .resolve_method(&Type::instance("Foo"), "bar")
229
+ .expect("Foo#bar should be registered");
230
+ let ret_vtx = info.return_vertex.unwrap();
231
+ let type_str = get_type_show(&genv, ret_vtx);
232
+ // else present: begin body excluded, else + rescue types
233
+ assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
234
+ assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
235
+ assert!(!type_str.contains("String"), "should NOT contain String: {}", type_str);
236
+ }
237
+
238
+ #[test]
239
+ fn test_begin_ensure_only() {
240
+ let source = r#"
241
+ class Foo
242
+ def bar
243
+ begin
244
+ "hello"
245
+ ensure
246
+ puts "cleanup"
247
+ end
248
+ end
249
+ end
250
+ "#;
251
+ let genv = analyze(source);
252
+ let info = genv
253
+ .resolve_method(&Type::instance("Foo"), "bar")
254
+ .expect("Foo#bar should be registered");
255
+ let ret_vtx = info.return_vertex.unwrap();
256
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
257
+ }
258
+
259
+ #[test]
260
+ fn test_begin_rescue_ensure() {
261
+ let source = r#"
262
+ class Foo
263
+ def bar
264
+ begin
265
+ "hello"
266
+ rescue
267
+ 42
268
+ ensure
269
+ :cleanup
270
+ end
271
+ end
272
+ end
273
+ "#;
274
+ let genv = analyze(source);
275
+ let info = genv
276
+ .resolve_method(&Type::instance("Foo"), "bar")
277
+ .expect("Foo#bar should be registered");
278
+ let ret_vtx = info.return_vertex.unwrap();
279
+ let type_str = get_type_show(&genv, ret_vtx);
280
+ // ensure does not affect return type
281
+ assert_eq!(type_str, "(Integer | String)");
282
+ }
283
+
284
+ #[test]
285
+ fn test_rescue_variable_type() {
286
+ let source = r#"
287
+ class Foo
288
+ def bar
289
+ begin
290
+ "hello"
291
+ rescue => e
292
+ e
293
+ end
294
+ end
295
+ end
296
+ "#;
297
+ let genv = analyze(source);
298
+ let info = genv
299
+ .resolve_method(&Type::instance("Foo"), "bar")
300
+ .expect("Foo#bar should be registered");
301
+ let ret_vtx = info.return_vertex.unwrap();
302
+ let type_str = get_type_show(&genv, ret_vtx);
303
+ assert!(
304
+ type_str.contains("StandardError"),
305
+ "should contain StandardError: {}",
306
+ type_str
307
+ );
308
+ }
309
+
310
+ #[test]
311
+ fn test_multiple_rescue_clauses() {
312
+ let source = r#"
313
+ class Foo
314
+ def bar
315
+ begin
316
+ "hello"
317
+ rescue ArgumentError
318
+ 42
319
+ rescue RuntimeError
320
+ :error
321
+ end
322
+ end
323
+ end
324
+ "#;
325
+ let genv = analyze(source);
326
+ let info = genv
327
+ .resolve_method(&Type::instance("Foo"), "bar")
328
+ .expect("Foo#bar should be registered");
329
+ let ret_vtx = info.return_vertex.unwrap();
330
+ let type_str = get_type_show(&genv, ret_vtx);
331
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
332
+ assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
333
+ assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
334
+ }
335
+
336
+ #[test]
337
+ fn test_rescue_modifier_basic() {
338
+ let source = r#"
339
+ class Foo
340
+ def bar
341
+ "hello" rescue 42
342
+ end
343
+ end
344
+ "#;
345
+ let genv = analyze(source);
346
+ let info = genv
347
+ .resolve_method(&Type::instance("Foo"), "bar")
348
+ .expect("Foo#bar should be registered");
349
+ let ret_vtx = info.return_vertex.unwrap();
350
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
351
+ }
352
+
353
+ #[test]
354
+ fn test_rescue_modifier_same_type() {
355
+ let source = r#"
356
+ class Foo
357
+ def bar
358
+ "hello" rescue "world"
359
+ end
360
+ end
361
+ "#;
362
+ let genv = analyze(source);
363
+ let info = genv
364
+ .resolve_method(&Type::instance("Foo"), "bar")
365
+ .expect("Foo#bar should be registered");
366
+ let ret_vtx = info.return_vertex.unwrap();
367
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
368
+ }
369
+
370
+ #[test]
371
+ fn test_nested_begin_rescue() {
372
+ let source = r#"
373
+ class Foo
374
+ def bar
375
+ begin
376
+ begin
377
+ "inner"
378
+ rescue
379
+ 42
380
+ end
381
+ rescue
382
+ :outer
383
+ end
384
+ end
385
+ end
386
+ "#;
387
+ let genv = analyze(source);
388
+ let info = genv
389
+ .resolve_method(&Type::instance("Foo"), "bar")
390
+ .expect("Foo#bar should be registered");
391
+ let ret_vtx = info.return_vertex.unwrap();
392
+ let type_str = get_type_show(&genv, ret_vtx);
393
+ // Outer: Union(inner_begin_rescue | :outer) = (Integer | String | Symbol)
394
+ assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
395
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
396
+ assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
397
+ }
398
+
399
+ #[test]
400
+ fn test_begin_rescue_in_method() {
401
+ let source = r#"
402
+ class Foo
403
+ def bar
404
+ x = begin
405
+ "hello"
406
+ rescue
407
+ 42
408
+ end
409
+ x
410
+ end
411
+ end
412
+ "#;
413
+ let genv = analyze(source);
414
+ let info = genv
415
+ .resolve_method(&Type::instance("Foo"), "bar")
416
+ .expect("Foo#bar should be registered");
417
+ let ret_vtx = info.return_vertex.unwrap();
418
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
419
+ }
420
+
421
+ #[test]
422
+ fn test_ensure_side_effects() {
423
+ // ensure body should be processed (no panic) but not affect return type
424
+ let source = r#"
425
+ class Foo
426
+ def bar
427
+ begin
428
+ "hello"
429
+ rescue
430
+ 42
431
+ ensure
432
+ x = "side_effect"
433
+ end
434
+ end
435
+ end
436
+ "#;
437
+ let genv = analyze(source);
438
+ let info = genv
439
+ .resolve_method(&Type::instance("Foo"), "bar")
440
+ .expect("Foo#bar should be registered");
441
+ let ret_vtx = info.return_vertex.unwrap();
442
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
443
+ }
444
+
445
+ #[test]
446
+ fn test_rescue_variable_scope_restore() {
447
+ // Rescue variable should not destroy outer binding
448
+ let source = r#"
449
+ class Foo
450
+ def bar
451
+ e = "outer"
452
+ begin
453
+ "hello"
454
+ rescue => e
455
+ e
456
+ end
457
+ e
458
+ end
459
+ end
460
+ "#;
461
+ let genv = analyze(source);
462
+ let info = genv
463
+ .resolve_method(&Type::instance("Foo"), "bar")
464
+ .expect("Foo#bar should be registered");
465
+ let ret_vtx = info.return_vertex.unwrap();
466
+ let type_str = get_type_show(&genv, ret_vtx);
467
+ // After rescue block, e should be restored to outer binding (String)
468
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
469
+ }
470
+
471
+ #[test]
472
+ fn test_rescue_variable_scope_removal() {
473
+ // When rescue variable has no prior binding, it should be removed after rescue block
474
+ let source = r#"
475
+ class Foo
476
+ def bar
477
+ begin
478
+ "hello"
479
+ rescue => e
480
+ e
481
+ end
482
+ end
483
+ end
484
+ "#;
485
+ let genv = analyze(source);
486
+ let info = genv
487
+ .resolve_method(&Type::instance("Foo"), "bar")
488
+ .expect("Foo#bar should be registered");
489
+ let ret_vtx = info.return_vertex.unwrap();
490
+ let type_str = get_type_show(&genv, ret_vtx);
491
+ // begin body (String) + rescue body where e = StandardError
492
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
493
+ assert!(
494
+ type_str.contains("StandardError"),
495
+ "should contain StandardError: {}",
496
+ type_str
497
+ );
498
+ }
499
+
500
+ #[test]
501
+ fn test_empty_rescue_body_is_nil() {
502
+ let source = r#"
503
+ class Foo
504
+ def bar
505
+ begin
506
+ "hello"
507
+ rescue
508
+ end
509
+ end
510
+ end
511
+ "#;
512
+ let genv = analyze(source);
513
+ let info = genv
514
+ .resolve_method(&Type::instance("Foo"), "bar")
515
+ .expect("Foo#bar should be registered");
516
+ let ret_vtx = info.return_vertex.unwrap();
517
+ let type_str = get_type_show(&genv, ret_vtx);
518
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
519
+ assert!(type_str.contains("nil"), "should contain nil: {}", type_str);
520
+ }
521
+ }
@@ -8,13 +8,16 @@ use crate::env::{GlobalEnv, LocalEnv};
8
8
  use crate::graph::{ChangeSet, VertexId};
9
9
  use ruby_prism::Node;
10
10
 
11
+ use super::assignments::process_multi_write_node;
11
12
  use super::blocks::process_block_node;
12
13
  use super::conditionals::{process_case_node, process_if_node, process_unless_node};
13
14
  use super::definitions::{process_class_node, process_def_node, process_module_node};
15
+ use super::exceptions::{process_begin_node, process_rescue_modifier_node};
14
16
  use super::dispatch::{dispatch_needs_child, dispatch_simple, process_needs_child, DispatchResult};
15
17
  use super::literals::install_literal_node;
16
- use super::parentheses::process_parentheses_node;
18
+ use super::loops::{process_until_node, process_while_node};
17
19
  use super::operators::{process_and_node, process_or_node};
20
+ use super::parentheses::process_parentheses_node;
18
21
  use super::returns::process_return_node;
19
22
 
20
23
  /// Build graph from AST (public API wrapper)
@@ -79,6 +82,20 @@ pub(crate) fn install_node(
79
82
  return process_case_node(genv, lenv, changes, source, &case_node);
80
83
  }
81
84
 
85
+ if let Some(begin_node) = node.as_begin_node() {
86
+ return process_begin_node(genv, lenv, changes, source, &begin_node);
87
+ }
88
+ if let Some(rescue_modifier) = node.as_rescue_modifier_node() {
89
+ return process_rescue_modifier_node(genv, lenv, changes, source, &rescue_modifier);
90
+ }
91
+
92
+ if let Some(while_node) = node.as_while_node() {
93
+ return process_while_node(genv, lenv, changes, source, &while_node);
94
+ }
95
+ if let Some(until_node) = node.as_until_node() {
96
+ return process_until_node(genv, lenv, changes, source, &until_node);
97
+ }
98
+
82
99
  if let Some(paren_node) = node.as_parentheses_node() {
83
100
  return process_parentheses_node(genv, lenv, changes, source, &paren_node);
84
101
  }
@@ -94,6 +111,10 @@ pub(crate) fn install_node(
94
111
  return process_or_node(genv, lenv, changes, source, &or_node);
95
112
  }
96
113
 
114
+ if let Some(multi_write) = node.as_multi_write_node() {
115
+ return process_multi_write_node(genv, lenv, changes, source, &multi_write);
116
+ }
117
+
97
118
  match dispatch_simple(genv, lenv, node) {
98
119
  DispatchResult::Vertex(vtx) => return Some(vtx),
99
120
  DispatchResult::NotHandled => {}