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.
@@ -0,0 +1,538 @@
1
+ //! Conditionals - if/unless/case type inference
2
+ //!
3
+ //! Collects types from each branch and merges them into a Union
4
+ //! via edges into a single result Vertex.
5
+
6
+ use crate::env::{GlobalEnv, LocalEnv};
7
+ use crate::graph::{ChangeSet, VertexId};
8
+ use crate::types::Type;
9
+ use ruby_prism::{CaseNode, ElseNode, IfNode, Node, UnlessNode, WhenNode};
10
+
11
+ use super::install::{install_node, install_statements};
12
+
13
+ /// Process IfNode: if/elsif/else chain
14
+ pub(crate) fn process_if_node(
15
+ genv: &mut GlobalEnv,
16
+ lenv: &mut LocalEnv,
17
+ changes: &mut ChangeSet,
18
+ source: &str,
19
+ if_node: &IfNode,
20
+ ) -> Option<VertexId> {
21
+ // Process predicate for side effects
22
+ install_node(genv, lenv, changes, source, &if_node.predicate());
23
+
24
+ let result_vtx = genv.new_vertex();
25
+
26
+ // then branch
27
+ let vtx_then = if_node
28
+ .statements()
29
+ .and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts));
30
+ if let Some(vtx) = vtx_then {
31
+ genv.add_edge(vtx, result_vtx);
32
+ }
33
+
34
+ // elsif/else branch (subsequent)
35
+ let has_else = if let Some(subsequent) = if_node.subsequent() {
36
+ let vtx_sub = process_subsequent(genv, lenv, changes, source, &subsequent);
37
+ if let Some(vtx) = vtx_sub {
38
+ genv.add_edge(vtx, result_vtx);
39
+ }
40
+ true
41
+ } else {
42
+ false
43
+ };
44
+
45
+ // No else clause → add nil
46
+ if !has_else {
47
+ let nil_vtx = genv.new_source(Type::Nil);
48
+ genv.add_edge(nil_vtx, result_vtx);
49
+ }
50
+
51
+ Some(result_vtx)
52
+ }
53
+
54
+ /// Process UnlessNode: unless/else
55
+ pub(crate) fn process_unless_node(
56
+ genv: &mut GlobalEnv,
57
+ lenv: &mut LocalEnv,
58
+ changes: &mut ChangeSet,
59
+ source: &str,
60
+ unless_node: &UnlessNode,
61
+ ) -> Option<VertexId> {
62
+ // Process predicate for side effects
63
+ install_node(genv, lenv, changes, source, &unless_node.predicate());
64
+
65
+ let result_vtx = genv.new_vertex();
66
+
67
+ // body branch
68
+ let vtx_body = unless_node
69
+ .statements()
70
+ .and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts));
71
+ if let Some(vtx) = vtx_body {
72
+ genv.add_edge(vtx, result_vtx);
73
+ }
74
+
75
+ // else clause
76
+ let has_else = if let Some(else_node) = unless_node.else_clause() {
77
+ let vtx_else = process_else_clause(genv, lenv, changes, source, &else_node);
78
+ if let Some(vtx) = vtx_else {
79
+ genv.add_edge(vtx, result_vtx);
80
+ }
81
+ true
82
+ } else {
83
+ false
84
+ };
85
+
86
+ // No else clause → add nil
87
+ if !has_else {
88
+ let nil_vtx = genv.new_source(Type::Nil);
89
+ genv.add_edge(nil_vtx, result_vtx);
90
+ }
91
+
92
+ Some(result_vtx)
93
+ }
94
+
95
+ /// Process CaseNode: case/when/else
96
+ pub(crate) fn process_case_node(
97
+ genv: &mut GlobalEnv,
98
+ lenv: &mut LocalEnv,
99
+ changes: &mut ChangeSet,
100
+ source: &str,
101
+ case_node: &CaseNode,
102
+ ) -> Option<VertexId> {
103
+ // Process predicate for side effects
104
+ if let Some(pred) = case_node.predicate() {
105
+ install_node(genv, lenv, changes, source, &pred);
106
+ }
107
+
108
+ let result_vtx = genv.new_vertex();
109
+
110
+ // Process each when clause
111
+ for condition in &case_node.conditions() {
112
+ if let Some(when_node) = condition.as_when_node() {
113
+ let vtx_when = process_when_clause(genv, lenv, changes, source, &when_node);
114
+ if let Some(vtx) = vtx_when {
115
+ genv.add_edge(vtx, result_vtx);
116
+ }
117
+ }
118
+ }
119
+
120
+ // else clause
121
+ let has_else = if let Some(else_node) = case_node.else_clause() {
122
+ let vtx_else = process_else_clause(genv, lenv, changes, source, &else_node);
123
+ if let Some(vtx) = vtx_else {
124
+ genv.add_edge(vtx, result_vtx);
125
+ }
126
+ true
127
+ } else {
128
+ false
129
+ };
130
+
131
+ // No else clause → add nil
132
+ if !has_else {
133
+ let nil_vtx = genv.new_source(Type::Nil);
134
+ genv.add_edge(nil_vtx, result_vtx);
135
+ }
136
+
137
+ Some(result_vtx)
138
+ }
139
+
140
+ /// Process subsequent node (elsif chain or else)
141
+ fn process_subsequent(
142
+ genv: &mut GlobalEnv,
143
+ lenv: &mut LocalEnv,
144
+ changes: &mut ChangeSet,
145
+ source: &str,
146
+ node: &Node,
147
+ ) -> Option<VertexId> {
148
+ // elsif: subsequent is another IfNode
149
+ if let Some(if_node) = node.as_if_node() {
150
+ return process_if_node(genv, lenv, changes, source, &if_node);
151
+ }
152
+
153
+ // else: subsequent is an ElseNode
154
+ if let Some(else_node) = node.as_else_node() {
155
+ return process_else_clause(genv, lenv, changes, source, &else_node);
156
+ }
157
+
158
+ None
159
+ }
160
+
161
+ /// Process ElseNode
162
+ fn process_else_clause(
163
+ genv: &mut GlobalEnv,
164
+ lenv: &mut LocalEnv,
165
+ changes: &mut ChangeSet,
166
+ source: &str,
167
+ else_node: &ElseNode,
168
+ ) -> Option<VertexId> {
169
+ else_node
170
+ .statements()
171
+ .and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts))
172
+ }
173
+
174
+ /// Process WhenNode
175
+ fn process_when_clause(
176
+ genv: &mut GlobalEnv,
177
+ lenv: &mut LocalEnv,
178
+ changes: &mut ChangeSet,
179
+ source: &str,
180
+ when_node: &WhenNode,
181
+ ) -> Option<VertexId> {
182
+ // Process when conditions for side effects
183
+ for cond in &when_node.conditions() {
184
+ install_node(genv, lenv, changes, source, &cond);
185
+ }
186
+
187
+ when_node
188
+ .statements()
189
+ .and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts))
190
+ }
191
+
192
+ #[cfg(test)]
193
+ mod tests {
194
+ use crate::analyzer::install::AstInstaller;
195
+ use crate::env::{GlobalEnv, LocalEnv};
196
+ use crate::graph::VertexId;
197
+ use crate::parser::ParseSession;
198
+ use crate::types::Type;
199
+
200
+ /// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
201
+ fn analyze(source: &str) -> GlobalEnv {
202
+ let session = ParseSession::new();
203
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
204
+ let root = parse_result.node();
205
+ let program = root.as_program_node().unwrap();
206
+
207
+ let mut genv = GlobalEnv::new();
208
+ let mut lenv = LocalEnv::new();
209
+
210
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
211
+ for stmt in &program.statements().body() {
212
+ installer.install_node(&stmt);
213
+ }
214
+ installer.finish();
215
+
216
+ genv
217
+ }
218
+
219
+ /// Helper: get the type string for a vertex ID (checks both Vertex and Source)
220
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
221
+ if let Some(vertex) = genv.get_vertex(vtx) {
222
+ vertex.show()
223
+ } else if let Some(source) = genv.get_source(vtx) {
224
+ source.ty.show()
225
+ } else {
226
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
227
+ }
228
+ }
229
+
230
+ // Test 1: if/else basic - different types in branches
231
+ #[test]
232
+ fn test_if_else_basic() {
233
+ let source = r#"
234
+ class Foo
235
+ def bar
236
+ if true
237
+ "hello"
238
+ else
239
+ 42
240
+ end
241
+ end
242
+ end
243
+ "#;
244
+ let genv = analyze(source);
245
+ let info = genv
246
+ .resolve_method(&Type::instance("Foo"), "bar")
247
+ .expect("Foo#bar should be registered");
248
+ let ret_vtx = info.return_vertex.unwrap();
249
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
250
+ }
251
+
252
+ // Test 2: if only (no else) → includes nil
253
+ #[test]
254
+ fn test_if_without_else() {
255
+ let source = r#"
256
+ class Foo
257
+ def bar
258
+ if true
259
+ "hello"
260
+ end
261
+ end
262
+ end
263
+ "#;
264
+ let genv = analyze(source);
265
+ let info = genv
266
+ .resolve_method(&Type::instance("Foo"), "bar")
267
+ .expect("Foo#bar should be registered");
268
+ let ret_vtx = info.return_vertex.unwrap();
269
+ assert_eq!(get_type_show(&genv, ret_vtx), "(String | nil)");
270
+ }
271
+
272
+ // Test 3: if/elsif/else chain → 3 types
273
+ #[test]
274
+ fn test_if_elsif_else() {
275
+ let source = r#"
276
+ class Foo
277
+ def bar
278
+ if true
279
+ "hello"
280
+ elsif false
281
+ 42
282
+ else
283
+ true
284
+ end
285
+ end
286
+ end
287
+ "#;
288
+ let genv = analyze(source);
289
+ let info = genv
290
+ .resolve_method(&Type::instance("Foo"), "bar")
291
+ .expect("Foo#bar should be registered");
292
+ let ret_vtx = info.return_vertex.unwrap();
293
+ let type_str = get_type_show(&genv, ret_vtx);
294
+ assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
295
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
296
+ assert!(type_str.contains("TrueClass"), "should contain TrueClass: {}", type_str);
297
+ }
298
+
299
+ // Test 4: unless/else
300
+ #[test]
301
+ fn test_unless_else() {
302
+ let source = r#"
303
+ class Foo
304
+ def bar
305
+ unless true
306
+ "a"
307
+ else
308
+ 1
309
+ end
310
+ end
311
+ end
312
+ "#;
313
+ let genv = analyze(source);
314
+ let info = genv
315
+ .resolve_method(&Type::instance("Foo"), "bar")
316
+ .expect("Foo#bar should be registered");
317
+ let ret_vtx = info.return_vertex.unwrap();
318
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
319
+ }
320
+
321
+ // Test 5: unless without else → includes nil
322
+ #[test]
323
+ fn test_unless_without_else() {
324
+ let source = r#"
325
+ class Foo
326
+ def bar
327
+ unless true
328
+ "hello"
329
+ end
330
+ end
331
+ end
332
+ "#;
333
+ let genv = analyze(source);
334
+ let info = genv
335
+ .resolve_method(&Type::instance("Foo"), "bar")
336
+ .expect("Foo#bar should be registered");
337
+ let ret_vtx = info.return_vertex.unwrap();
338
+ assert_eq!(get_type_show(&genv, ret_vtx), "(String | nil)");
339
+ }
340
+
341
+ // Test 6: case/when/else
342
+ #[test]
343
+ fn test_case_when_else() {
344
+ let source = r#"
345
+ class Foo
346
+ def bar
347
+ case :status
348
+ when :active
349
+ "active"
350
+ when :inactive
351
+ "inactive"
352
+ else
353
+ nil
354
+ end
355
+ end
356
+ end
357
+ "#;
358
+ let genv = analyze(source);
359
+ let info = genv
360
+ .resolve_method(&Type::instance("Foo"), "bar")
361
+ .expect("Foo#bar should be registered");
362
+ let ret_vtx = info.return_vertex.unwrap();
363
+ let type_str = get_type_show(&genv, ret_vtx);
364
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
365
+ assert!(type_str.contains("nil"), "should contain nil: {}", type_str);
366
+ }
367
+
368
+ // Test 7: case without else → includes nil
369
+ #[test]
370
+ fn test_case_without_else() {
371
+ let source = r#"
372
+ class Foo
373
+ def bar
374
+ case :status
375
+ when :active
376
+ "active"
377
+ when :inactive
378
+ 42
379
+ end
380
+ end
381
+ end
382
+ "#;
383
+ let genv = analyze(source);
384
+ let info = genv
385
+ .resolve_method(&Type::instance("Foo"), "bar")
386
+ .expect("Foo#bar should be registered");
387
+ let ret_vtx = info.return_vertex.unwrap();
388
+ let type_str = get_type_show(&genv, ret_vtx);
389
+ assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
390
+ assert!(type_str.contains("String"), "should contain String: {}", type_str);
391
+ assert!(type_str.contains("nil"), "should contain nil: {}", type_str);
392
+ }
393
+
394
+ // Test 8: conditional inside method → return type reflects union
395
+ #[test]
396
+ fn test_conditional_in_method_return() {
397
+ let source = r#"
398
+ class Converter
399
+ def convert(x)
400
+ if true
401
+ "text"
402
+ else
403
+ 100
404
+ end
405
+ end
406
+ end
407
+ "#;
408
+ let genv = analyze(source);
409
+ let info = genv
410
+ .resolve_method(&Type::instance("Converter"), "convert")
411
+ .expect("Converter#convert should be registered");
412
+ let ret_vtx = info.return_vertex.unwrap();
413
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
414
+ }
415
+
416
+ // Test 9: nested conditionals
417
+ #[test]
418
+ fn test_nested_conditionals() {
419
+ let source = r#"
420
+ class Foo
421
+ def bar
422
+ if true
423
+ if false
424
+ "inner"
425
+ else
426
+ 42
427
+ end
428
+ else
429
+ :sym
430
+ end
431
+ end
432
+ end
433
+ "#;
434
+ let genv = analyze(source);
435
+ let info = genv
436
+ .resolve_method(&Type::instance("Foo"), "bar")
437
+ .expect("Foo#bar should be registered");
438
+ let ret_vtx = info.return_vertex.unwrap();
439
+ let type_str = get_type_show(&genv, ret_vtx);
440
+ // Outer if: inner_if_result | Symbol
441
+ // Inner if: (Integer | String) → propagates through
442
+ assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
443
+ }
444
+
445
+ // Test 10: all branches same type → single type (not union)
446
+ #[test]
447
+ fn test_same_type_branches() {
448
+ let source = r#"
449
+ class Foo
450
+ def bar
451
+ if true
452
+ "hello"
453
+ else
454
+ "world"
455
+ end
456
+ end
457
+ end
458
+ "#;
459
+ let genv = analyze(source);
460
+ let info = genv
461
+ .resolve_method(&Type::instance("Foo"), "bar")
462
+ .expect("Foo#bar should be registered");
463
+ let ret_vtx = info.return_vertex.unwrap();
464
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
465
+ }
466
+
467
+ // Test 11: ternary operator - different types → union
468
+ #[test]
469
+ fn test_ternary_union_type() {
470
+ let source = r#"
471
+ class Foo
472
+ def bar
473
+ true ? "hello" : 42
474
+ end
475
+ end
476
+ "#;
477
+ let genv = analyze(source);
478
+ let info = genv
479
+ .resolve_method(&Type::instance("Foo"), "bar")
480
+ .expect("Foo#bar should be registered");
481
+ let ret_vtx = info.return_vertex.unwrap();
482
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
483
+ }
484
+
485
+ // Test 12: ternary operator - same type → single type
486
+ #[test]
487
+ fn test_ternary_same_type() {
488
+ let source = r#"
489
+ class Foo
490
+ def bar
491
+ true ? "hello" : "world"
492
+ end
493
+ end
494
+ "#;
495
+ let genv = analyze(source);
496
+ let info = genv
497
+ .resolve_method(&Type::instance("Foo"), "bar")
498
+ .expect("Foo#bar should be registered");
499
+ let ret_vtx = info.return_vertex.unwrap();
500
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
501
+ }
502
+
503
+ // Test 13: ternary operator - nil branch → union with nil
504
+ #[test]
505
+ fn test_ternary_nil_branch() {
506
+ let source = r#"
507
+ class Foo
508
+ def bar
509
+ true ? "hello" : nil
510
+ end
511
+ end
512
+ "#;
513
+ let genv = analyze(source);
514
+ let info = genv
515
+ .resolve_method(&Type::instance("Foo"), "bar")
516
+ .expect("Foo#bar should be registered");
517
+ let ret_vtx = info.return_vertex.unwrap();
518
+ assert_eq!(get_type_show(&genv, ret_vtx), "(String | nil)");
519
+ }
520
+
521
+ // Test 14: nested ternary operator
522
+ #[test]
523
+ fn test_ternary_nested() {
524
+ let source = r#"
525
+ class Foo
526
+ def bar
527
+ true ? (false ? "inner" : 42) : :sym
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
+ assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String | Symbol)");
537
+ }
538
+ }