method-ray 0.1.3 → 0.1.4

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,466 @@
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
+ }
@@ -6,38 +6,131 @@
6
6
  //! - Method definition scope management (def baz ... end)
7
7
  //! - Extracting class/module names from AST nodes (including qualified names like Api::User)
8
8
 
9
- use crate::env::GlobalEnv;
9
+ use crate::env::{GlobalEnv, LocalEnv};
10
+ use crate::graph::{ChangeSet, VertexId};
11
+ use crate::types::Type;
10
12
  use ruby_prism::Node;
11
13
 
14
+ use super::install::install_statements;
15
+ use super::parameters::install_parameters;
16
+
17
+ /// Process class definition node
18
+ pub(crate) fn process_class_node(
19
+ genv: &mut GlobalEnv,
20
+ lenv: &mut LocalEnv,
21
+ changes: &mut ChangeSet,
22
+ source: &str,
23
+ class_node: &ruby_prism::ClassNode,
24
+ ) -> Option<VertexId> {
25
+ let class_name = extract_class_name(class_node);
26
+ install_class(genv, class_name);
27
+
28
+ if let Some(body) = class_node.body() {
29
+ if let Some(statements) = body.as_statements_node() {
30
+ install_statements(genv, lenv, changes, source, &statements);
31
+ }
32
+ }
33
+
34
+ exit_scope(genv);
35
+ None
36
+ }
37
+
38
+ /// Process module definition node
39
+ pub(crate) fn process_module_node(
40
+ genv: &mut GlobalEnv,
41
+ lenv: &mut LocalEnv,
42
+ changes: &mut ChangeSet,
43
+ source: &str,
44
+ module_node: &ruby_prism::ModuleNode,
45
+ ) -> Option<VertexId> {
46
+ let module_name = extract_module_name(module_node);
47
+ install_module(genv, module_name);
48
+
49
+ if let Some(body) = module_node.body() {
50
+ if let Some(statements) = body.as_statements_node() {
51
+ install_statements(genv, lenv, changes, source, &statements);
52
+ }
53
+ }
54
+
55
+ exit_scope(genv);
56
+ None
57
+ }
58
+
59
+ /// Process method definition node
60
+ pub(crate) fn process_def_node(
61
+ genv: &mut GlobalEnv,
62
+ lenv: &mut LocalEnv,
63
+ changes: &mut ChangeSet,
64
+ source: &str,
65
+ def_node: &ruby_prism::DefNode,
66
+ ) -> Option<VertexId> {
67
+ let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
68
+ install_method(genv, method_name.clone());
69
+
70
+ // Process parameters BEFORE processing body
71
+ let param_vtxs = if let Some(params_node) = def_node.parameters() {
72
+ install_parameters(genv, lenv, changes, source, &params_node)
73
+ } else {
74
+ vec![]
75
+ };
76
+
77
+ let mut last_vtx = None;
78
+ if let Some(body) = def_node.body() {
79
+ if let Some(statements) = body.as_statements_node() {
80
+ last_vtx = install_statements(genv, lenv, changes, source, &statements);
81
+ }
82
+ }
83
+
84
+ // Register user-defined method with return vertex and param vertices (before exiting scope)
85
+ if let Some(return_vtx) = last_vtx {
86
+ let recv_type_name = genv
87
+ .scope_manager
88
+ .current_class_name()
89
+ .or_else(|| genv.scope_manager.current_module_name());
90
+
91
+ if let Some(name) = recv_type_name {
92
+ genv.register_user_method(
93
+ Type::instance(&name),
94
+ &method_name,
95
+ return_vtx,
96
+ param_vtxs,
97
+ );
98
+ }
99
+ }
100
+
101
+ exit_scope(genv);
102
+ None
103
+ }
104
+
12
105
  /// Install class definition
13
- pub fn install_class(genv: &mut GlobalEnv, class_name: String) {
106
+ fn install_class(genv: &mut GlobalEnv, class_name: String) {
14
107
  genv.enter_class(class_name);
15
108
  }
16
109
 
17
110
  /// Install module definition
18
- pub fn install_module(genv: &mut GlobalEnv, module_name: String) {
111
+ fn install_module(genv: &mut GlobalEnv, module_name: String) {
19
112
  genv.enter_module(module_name);
20
113
  }
21
114
 
22
115
  /// Install method definition
23
- pub fn install_method(genv: &mut GlobalEnv, method_name: String) {
116
+ fn install_method(genv: &mut GlobalEnv, method_name: String) {
24
117
  genv.enter_method(method_name);
25
118
  }
26
119
 
27
120
  /// Exit current scope (class, module, or method)
28
- pub fn exit_scope(genv: &mut GlobalEnv) {
121
+ fn exit_scope(genv: &mut GlobalEnv) {
29
122
  genv.exit_scope();
30
123
  }
31
124
 
32
125
  /// Extract class name from ClassNode
33
126
  /// Supports both simple names (User) and qualified names (Api::V1::User)
34
- pub fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
127
+ fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
35
128
  extract_constant_path(&class_node.constant_path()).unwrap_or_else(|| "UnknownClass".to_string())
36
129
  }
37
130
 
38
131
  /// Extract module name from ModuleNode
39
132
  /// Supports both simple names (Utils) and qualified names (Api::V1::Utils)
40
- pub fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
133
+ fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
41
134
  extract_constant_path(&module_node.constant_path())
42
135
  .unwrap_or_else(|| "UnknownModule".to_string())
43
136
  }
@@ -49,7 +142,7 @@ pub fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
49
142
  /// - `Api::User` (ConstantPathNode) → "Api::User"
50
143
  /// - `Api::V1::User` (nested ConstantPathNode) → "Api::V1::User"
51
144
  /// - `::Api::User` (absolute path with COLON3) → "Api::User"
52
- fn extract_constant_path(node: &Node) -> Option<String> {
145
+ pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
53
146
  // Simple constant read: `User`
54
147
  if let Some(constant_read) = node.as_constant_read_node() {
55
148
  return Some(String::from_utf8_lossy(constant_read.name().as_slice()).to_string());
@@ -79,7 +172,9 @@ fn extract_constant_path(node: &Node) -> Option<String> {
79
172
  #[cfg(test)]
80
173
  mod tests {
81
174
  use super::*;
175
+ use crate::graph::ChangeSet;
82
176
  use crate::parser::ParseSession;
177
+ use crate::types::Type;
83
178
 
84
179
  #[test]
85
180
  fn test_enter_exit_class_scope() {
@@ -216,4 +311,27 @@ mod tests {
216
311
  let name = extract_module_name(&module_node);
217
312
  assert_eq!(name, "Api::V1");
218
313
  }
314
+
315
+ #[test]
316
+ fn test_process_def_node_registers_user_method() {
317
+ let source = "class User; def name; \"Alice\"; end; end";
318
+ let session = ParseSession::new();
319
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
320
+ let root = parse_result.node();
321
+ let program = root.as_program_node().unwrap();
322
+
323
+ let mut genv = GlobalEnv::new();
324
+ let mut lenv = LocalEnv::new();
325
+ let mut changes = ChangeSet::new();
326
+
327
+ let stmt = program.statements().body().first().unwrap();
328
+ let class_node = stmt.as_class_node().unwrap();
329
+ process_class_node(&mut genv, &mut lenv, &mut changes, source, &class_node);
330
+
331
+ // User#name should be registered as a user-defined method
332
+ let info = genv
333
+ .resolve_method(&Type::instance("User"), "name")
334
+ .expect("User#name should be registered");
335
+ assert!(info.return_vertex.is_some());
336
+ }
219
337
  }