method-ray 0.1.8 → 0.1.10

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +9 -11
  4. data/{rust → core}/Cargo.toml +1 -1
  5. data/core/src/analyzer/assignments.rs +219 -0
  6. data/{rust → core}/src/analyzer/blocks.rs +0 -50
  7. data/{rust → core}/src/analyzer/calls.rs +3 -32
  8. data/core/src/analyzer/conditionals.rs +190 -0
  9. data/core/src/analyzer/definitions.rs +205 -0
  10. data/core/src/analyzer/dispatch.rs +455 -0
  11. data/core/src/analyzer/exceptions.rs +168 -0
  12. data/{rust → core}/src/analyzer/install.rs +16 -1
  13. data/{rust → core}/src/analyzer/literals.rs +3 -71
  14. data/core/src/analyzer/loops.rs +94 -0
  15. data/{rust → core}/src/analyzer/mod.rs +1 -15
  16. data/core/src/analyzer/operators.rs +79 -0
  17. data/{rust → core}/src/analyzer/parameters.rs +4 -67
  18. data/core/src/analyzer/parentheses.rs +25 -0
  19. data/core/src/analyzer/returns.rs +39 -0
  20. data/core/src/analyzer/super_calls.rs +74 -0
  21. data/{rust → core}/src/analyzer/variables.rs +5 -25
  22. data/{rust → core}/src/checker.rs +0 -13
  23. data/{rust → core}/src/diagnostics/diagnostic.rs +0 -41
  24. data/{rust → core}/src/diagnostics/formatter.rs +0 -38
  25. data/{rust → core}/src/env/box_manager.rs +0 -30
  26. data/{rust → core}/src/env/global_env.rs +67 -80
  27. data/core/src/env/local_env.rs +42 -0
  28. data/core/src/env/method_registry.rs +173 -0
  29. data/core/src/env/scope.rs +299 -0
  30. data/{rust → core}/src/env/vertex_manager.rs +0 -73
  31. data/core/src/graph/box.rs +347 -0
  32. data/{rust → core}/src/graph/change_set.rs +0 -65
  33. data/{rust → core}/src/graph/vertex.rs +0 -69
  34. data/{rust → core}/src/parser.rs +0 -77
  35. data/{rust → core}/src/types.rs +11 -0
  36. data/ext/Cargo.toml +2 -2
  37. data/lib/methodray/binary_locator.rb +2 -2
  38. data/lib/methodray/commands.rb +1 -1
  39. data/lib/methodray/version.rb +1 -1
  40. metadata +58 -56
  41. data/rust/src/analyzer/assignments.rs +0 -152
  42. data/rust/src/analyzer/conditionals.rs +0 -538
  43. data/rust/src/analyzer/definitions.rs +0 -719
  44. data/rust/src/analyzer/dispatch.rs +0 -1137
  45. data/rust/src/analyzer/exceptions.rs +0 -521
  46. data/rust/src/analyzer/loops.rs +0 -176
  47. data/rust/src/analyzer/operators.rs +0 -284
  48. data/rust/src/analyzer/parentheses.rs +0 -113
  49. data/rust/src/analyzer/returns.rs +0 -191
  50. data/rust/src/env/local_env.rs +0 -92
  51. data/rust/src/env/method_registry.rs +0 -268
  52. data/rust/src/env/scope.rs +0 -596
  53. data/rust/src/graph/box.rs +0 -766
  54. /data/{rust → core}/src/analyzer/attributes.rs +0 -0
  55. /data/{rust → core}/src/cache/mod.rs +0 -0
  56. /data/{rust → core}/src/cache/rbs_cache.rs +0 -0
  57. /data/{rust → core}/src/cli/args.rs +0 -0
  58. /data/{rust → core}/src/cli/commands.rs +0 -0
  59. /data/{rust → core}/src/cli/mod.rs +0 -0
  60. /data/{rust → core}/src/diagnostics/mod.rs +0 -0
  61. /data/{rust → core}/src/env/mod.rs +0 -0
  62. /data/{rust → core}/src/env/type_error.rs +0 -0
  63. /data/{rust → core}/src/graph/mod.rs +0 -0
  64. /data/{rust → core}/src/lib.rs +0 -0
  65. /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
  66. /data/{rust → core}/src/lsp/main.rs +0 -0
  67. /data/{rust → core}/src/lsp/mod.rs +0 -0
  68. /data/{rust → core}/src/lsp/server.rs +0 -0
  69. /data/{rust → core}/src/main.rs +0 -0
  70. /data/{rust → core}/src/rbs/converter.rs +0 -0
  71. /data/{rust → core}/src/rbs/error.rs +0 -0
  72. /data/{rust → core}/src/rbs/loader.rs +0 -0
  73. /data/{rust → core}/src/rbs/mod.rs +0 -0
  74. /data/{rust → core}/src/source_map.rs +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f44709f8354439522722c8ea65da30463fae9de68d9be1aa07c8dac9627a03ce
4
- data.tar.gz: 2a09885854020892cf93a1c85e83735a23be60240fcbee09f77fa8c9fbf58801
3
+ metadata.gz: 228f256ea9848bd9b2447e097aee95f59f47db2bd252611a7dfa30363be34a23
4
+ data.tar.gz: 0ff2217ee68021004331bd580f26739ce067f223ea113b741ab0636f2771e30e
5
5
  SHA512:
6
- metadata.gz: 4be6a23881adf54e88039c4e62255f09aa52005cf4085e6cbdc1910f642dae8e763ec7993445051bd3cff20693b5576e417f47b0864359c60db6becf1c21e204
7
- data.tar.gz: cc9b502af18acc8463e9cddf95cc16b8ed6095ffc34d826642fe8193b2f4cc16aa7490a96f930493fe30fc4af21412cf2ff515f8f45e4d04bf9ec704f256ea26
6
+ metadata.gz: 3c274c68e498eed4f432b60c4a68a6ca469b7850db539954f584d89bd96cbf3612e03a9a9f43a1b8a8941be6302142e7f34e886b279cde41bc22a0376ba48ac7
7
+ data.tar.gz: 57e66add8decf8c951f890f140f76fca86b642af91a7df9d7622c5144414b20f158444838aeca5082a46efa185280747a32117980654ae5f03d0c1dc23bbcd35
data/CHANGELOG.md CHANGED
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.10] - 2026-03-24
9
+
10
+ ### Added
11
+
12
+ - Add safe navigation operator (`&.`) support ([#65](https://github.com/dak2/method-ray/pull/65))
13
+ - Add inheritance chain method resolution for user-defined classes ([#66](https://github.com/dak2/method-ray/pull/66))
14
+ - Add extend support for module methods as class methods ([#67](https://github.com/dak2/method-ray/pull/67))
15
+ - Add pull request template ([#82](https://github.com/dak2/method-ray/pull/82))
16
+
17
+ ### Changed
18
+
19
+ - Migrate Rust integration tests to Ruby integration tests ([#68](https://github.com/dak2/method-ray/pull/68), [#69](https://github.com/dak2/method-ray/pull/69), [#70](https://github.com/dak2/method-ray/pull/70), [#71](https://github.com/dak2/method-ray/pull/71), [#72](https://github.com/dak2/method-ray/pull/72), [#73](https://github.com/dak2/method-ray/pull/73), [#74](https://github.com/dak2/method-ray/pull/74), [#75](https://github.com/dak2/method-ray/pull/75))
20
+ - Remove redundant Rust unit tests ([#77](https://github.com/dak2/method-ray/pull/77), [#78](https://github.com/dak2/method-ray/pull/78), [#80](https://github.com/dak2/method-ray/pull/80), [#83](https://github.com/dak2/method-ray/pull/83), [#84](https://github.com/dak2/method-ray/pull/84), [#85](https://github.com/dak2/method-ray/pull/85), [#86](https://github.com/dak2/method-ray/pull/86))
21
+ - Simplify README to focus on core value proposition ([#76](https://github.com/dak2/method-ray/pull/76))
22
+
23
+ ### Fixed
24
+
25
+ - Fix release workflow to include gem assets in GitHub Release ([#63](https://github.com/dak2/method-ray/pull/63), [#64](https://github.com/dak2/method-ray/pull/64))
26
+
27
+ ## [0.1.9] - 2026-03-15
28
+
29
+ ### Added
30
+
31
+ - Add linter CI workflow for Clippy and RuboCop ([#61](https://github.com/dak2/method-ray/pull/61))
32
+ - Add unit tests for blocks and parameters analyzers ([#60](https://github.com/dak2/method-ray/pull/60))
33
+ - Add super call type inference support ([#59](https://github.com/dak2/method-ray/pull/59))
34
+ - Add for loop type inference support ([#58](https://github.com/dak2/method-ray/pull/58))
35
+ - Add module include support for mixin method resolution ([#57](https://github.com/dak2/method-ray/pull/57))
36
+ - Add complete multi-assignment type inference ([#56](https://github.com/dak2/method-ray/pull/56))
37
+ - Add auto-tagging workflow for release PR merges ([#53](https://github.com/dak2/method-ray/pull/53))
38
+
39
+ ### Changed
40
+
41
+ - Rename rust/ directory to core/ ([#55](https://github.com/dak2/method-ray/pull/55))
42
+ - Infer actual exception types in rescue clauses ([#54](https://github.com/dak2/method-ray/pull/54))
43
+
8
44
  ## [0.1.8] - 2026-03-09
9
45
 
10
46
  ### Added
@@ -123,6 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
123
159
  - Initial release
124
160
  - `methodray check` - Static type checking for Ruby files
125
161
 
162
+ [0.1.10]: https://github.com/dak2/method-ray/releases/tag/v0.1.10
163
+ [0.1.9]: https://github.com/dak2/method-ray/releases/tag/v0.1.9
126
164
  [0.1.8]: https://github.com/dak2/method-ray/releases/tag/v0.1.8
127
165
  [0.1.7]: https://github.com/dak2/method-ray/releases/tag/v0.1.7
128
166
  [0.1.6]: https://github.com/dak2/method-ray/releases/tag/v0.1.6
data/README.md CHANGED
@@ -16,28 +16,26 @@ gem install methodray
16
16
 
17
17
  ## Quick Start
18
18
 
19
- ### VSCode Extension (under development)
20
-
21
- 1. Install the [Method-Ray VSCode extension](https://github.com/dak2/method-ray-vscode)
22
- 2. Open a Ruby file in VSCode
23
- 3. Errors will be highlighted automatically
24
-
25
- ### CLI
19
+ ### Checking Methods
26
20
 
27
21
  ```bash
28
22
  # Check a single file
29
23
  bundle exec methodray check app/models/user.rb
24
+ ```
30
25
 
31
- # Watch mode - auto re-check on file changes
26
+ ### Watching for File Changes, Re-checking Methods
27
+
28
+ ```bash
29
+ # Watch a file for changes and re-check on save
32
30
  bundle exec methodray watch app/models/user.rb
33
31
  ```
34
32
 
35
- #### Example
36
-
37
- `methodray check <file>`: Performs static type checking on the specified Ruby file.
33
+ #### Example Usage
38
34
 
35
+ `bundle exec methodray check app/models/user.rb`
39
36
 
40
37
  ```ruby
38
+ # app/models/user.rb
41
39
  class User
42
40
  def greeting
43
41
  name = "Alice"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray-core"
3
- version = "0.1.8"
3
+ version = "0.1.10"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -0,0 +1,219 @@
1
+ //! Multiple Assignment Handlers - Processing Ruby multiple assignment
2
+ //!
3
+ //! Supports: ArrayNode RHS with 1:1 mapping, LHS > RHS nil fill,
4
+ //! splat targets (*rest) as Array type, and basic single-expression RHS decomposition.
5
+ //! TODO: Support RHS as method return value decomposition (requires graph lazy resolution)
6
+
7
+ use crate::env::{GlobalEnv, LocalEnv};
8
+ use crate::graph::{ChangeSet, VertexId};
9
+ use crate::types::Type;
10
+
11
+ use super::bytes_to_name;
12
+ use super::variables::install_local_var_write;
13
+
14
+ /// Install an RHS node and assign it to a named local variable.
15
+ /// Falls back to an untyped vertex when `install_node` returns `None`.
16
+ fn install_target(
17
+ genv: &mut GlobalEnv,
18
+ lenv: &mut LocalEnv,
19
+ changes: &mut ChangeSet,
20
+ source: &str,
21
+ var_name: String,
22
+ rhs_node: &ruby_prism::Node,
23
+ ) -> VertexId {
24
+ if let Some(rv) = super::install::install_node(genv, lenv, changes, source, rhs_node) {
25
+ install_local_var_write(genv, lenv, changes, var_name, rv)
26
+ } else {
27
+ let vtx = genv.new_vertex();
28
+ lenv.new_var(var_name, vtx);
29
+ vtx
30
+ }
31
+ }
32
+
33
+ /// Assign `Type::Nil` to a named local variable.
34
+ fn install_nil_target(
35
+ genv: &mut GlobalEnv,
36
+ lenv: &mut LocalEnv,
37
+ changes: &mut ChangeSet,
38
+ var_name: String,
39
+ ) -> VertexId {
40
+ let nil_src = genv.new_source(Type::Nil);
41
+ install_local_var_write(genv, lenv, changes, var_name, nil_src)
42
+ }
43
+
44
+ /// Extract the splat target variable name from a `node.rest()` result.
45
+ fn splat_var_name(rest_node: &ruby_prism::Node) -> Option<String> {
46
+ let splat = rest_node.as_splat_node()?;
47
+ let expr = splat.expression()?;
48
+ let target = expr.as_local_variable_target_node()?;
49
+ Some(bytes_to_name(target.name().as_slice()))
50
+ }
51
+
52
+ /// Collect unique types from a slice of elements, returning element type for Array[T].
53
+ /// Only nodes that resolve to a Source (fixed type) contribute; Vertex-type nodes
54
+ /// are excluded (known limitation — requires lazy resolution for method return values).
55
+ fn collect_element_type(
56
+ genv: &mut GlobalEnv,
57
+ lenv: &mut LocalEnv,
58
+ changes: &mut ChangeSet,
59
+ source: &str,
60
+ elements: &[ruby_prism::Node],
61
+ ) -> Type {
62
+ let mut types: Vec<Type> = Vec::new();
63
+ for elem in elements {
64
+ if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, elem) {
65
+ if let Some(src) = genv.get_source(vtx) {
66
+ if !types.contains(&src.ty) {
67
+ types.push(src.ty.clone());
68
+ }
69
+ }
70
+ }
71
+ }
72
+ if types.is_empty() {
73
+ Type::Bot
74
+ } else {
75
+ Type::union_of(types)
76
+ }
77
+ }
78
+
79
+ /// Process multiple assignment node (e.g., `a, b = 1, "hello"`)
80
+ pub(crate) fn process_multi_write_node(
81
+ genv: &mut GlobalEnv,
82
+ lenv: &mut LocalEnv,
83
+ changes: &mut ChangeSet,
84
+ source: &str,
85
+ node: &ruby_prism::MultiWriteNode,
86
+ ) -> Option<VertexId> {
87
+ let value = node.value();
88
+ let mut last_vtx = None;
89
+
90
+ if let Some(array_node) = value.as_array_node() {
91
+ let lefts = node.lefts();
92
+ let elements: Vec<_> = array_node.elements().iter().collect();
93
+ let total = elements.len();
94
+ let lefts_count = lefts.len();
95
+ let rights = node.rights();
96
+ let rights_count = rights.len();
97
+
98
+ // Phase 1: Left targets — assign from start, nil for missing RHS
99
+ for (i, target) in lefts.iter().enumerate() {
100
+ if let Some(target_node) = target.as_local_variable_target_node() {
101
+ let var_name = bytes_to_name(target_node.name().as_slice());
102
+ if i < total {
103
+ last_vtx = Some(install_target(
104
+ genv,
105
+ lenv,
106
+ changes,
107
+ source,
108
+ var_name,
109
+ &elements[i],
110
+ ));
111
+ } else {
112
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
113
+ }
114
+ }
115
+ }
116
+
117
+ // Phase 2: Splat target (*rest) — collect middle elements (after lefts, before rights) into Array[T]
118
+ if let Some(rest_node) = node.rest() {
119
+ if let Some(var_name) = splat_var_name(&rest_node) {
120
+ let splat_start = lefts_count;
121
+ let splat_end = total.saturating_sub(rights_count);
122
+ let splat_elements = if splat_start < splat_end {
123
+ &elements[splat_start..splat_end]
124
+ } else {
125
+ &elements[0..0]
126
+ };
127
+ let element_type = collect_element_type(
128
+ genv,
129
+ lenv,
130
+ changes,
131
+ source,
132
+ splat_elements,
133
+ );
134
+ let array_src = genv.new_source(Type::array_of(element_type));
135
+ last_vtx = Some(install_local_var_write(
136
+ genv, lenv, changes, var_name, array_src,
137
+ ));
138
+ }
139
+ }
140
+
141
+ // Phase 3: Right targets — assigned from end of elements, nil if overlapping with lefts
142
+ for (i, target) in rights.iter().enumerate() {
143
+ if let Some(target_node) = target.as_local_variable_target_node() {
144
+ let var_name = bytes_to_name(target_node.name().as_slice());
145
+ let signed_idx = total as isize - rights_count as isize + i as isize;
146
+ if signed_idx >= lefts_count as isize && (signed_idx as usize) < total {
147
+ last_vtx = Some(install_target(
148
+ genv,
149
+ lenv,
150
+ changes,
151
+ source,
152
+ var_name,
153
+ &elements[signed_idx as usize],
154
+ ));
155
+ } else {
156
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
157
+ }
158
+ }
159
+ }
160
+ } else {
161
+ // RHS is a single expression (not comma-separated)
162
+ let rhs_vtx = super::install::install_node(genv, lenv, changes, source, &value);
163
+
164
+ let rhs_type = rhs_vtx
165
+ .and_then(|vtx| genv.get_source(vtx))
166
+ .map(|src| src.ty.clone());
167
+
168
+ // If RHS is Array[T], each target gets T; otherwise first target gets RHS, rest get nil
169
+ let element_type = rhs_type
170
+ .as_ref()
171
+ .and_then(|ty| ty.type_args())
172
+ .and_then(|args| args.first().cloned());
173
+
174
+ for (i, target) in node.lefts().iter().enumerate() {
175
+ if let Some(target_node) = target.as_local_variable_target_node() {
176
+ let var_name = bytes_to_name(target_node.name().as_slice());
177
+ if let Some(ref elem_ty) = element_type {
178
+ let src = genv.new_source(elem_ty.clone());
179
+ last_vtx = Some(install_local_var_write(genv, lenv, changes, var_name, src));
180
+ } else if i == 0 {
181
+ if let Some(rv) = rhs_vtx {
182
+ last_vtx = Some(install_local_var_write(genv, lenv, changes, var_name, rv));
183
+ } else {
184
+ let vtx = genv.new_vertex();
185
+ lenv.new_var(var_name, vtx);
186
+ last_vtx = Some(vtx);
187
+ }
188
+ } else if rhs_type.is_some() {
189
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
190
+ } else {
191
+ let vtx = genv.new_vertex();
192
+ lenv.new_var(var_name, vtx);
193
+ last_vtx = Some(vtx);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Splat in single-expression RHS
199
+ if let Some(rest_node) = node.rest() {
200
+ if let Some(var_name) = splat_var_name(&rest_node) {
201
+ let elem_ty = element_type.unwrap_or(Type::Bot);
202
+ let array_src = genv.new_source(Type::array_of(elem_ty));
203
+ last_vtx = Some(install_local_var_write(
204
+ genv, lenv, changes, var_name, array_src,
205
+ ));
206
+ }
207
+ }
208
+
209
+ // Right targets in single-expression RHS → nil
210
+ for target in node.rights().iter() {
211
+ if let Some(target_node) = target.as_local_variable_target_node() {
212
+ let var_name = bytes_to_name(target_node.name().as_slice());
213
+ last_vtx = Some(install_nil_target(genv, lenv, changes, var_name));
214
+ }
215
+ }
216
+ }
217
+
218
+ last_vtx
219
+ }
@@ -124,53 +124,3 @@ fn exit_block_scope(genv: &mut GlobalEnv) {
124
124
  fn install_block_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
125
125
  install_required_parameter(genv, lenv, name)
126
126
  }
127
-
128
- #[cfg(test)]
129
- mod tests {
130
- use super::*;
131
-
132
- #[test]
133
- fn test_enter_exit_block_scope() {
134
- let mut genv = GlobalEnv::new();
135
-
136
- let initial_scope_id = genv.scope_manager.current_scope().id;
137
-
138
- enter_block_scope(&mut genv);
139
- let block_scope_id = genv.scope_manager.current_scope().id;
140
-
141
- assert_ne!(initial_scope_id, block_scope_id);
142
-
143
- exit_block_scope(&mut genv);
144
-
145
- assert_eq!(genv.scope_manager.current_scope().id, initial_scope_id);
146
- }
147
-
148
- #[test]
149
- fn test_install_block_parameter() {
150
- let mut genv = GlobalEnv::new();
151
- let mut lenv = LocalEnv::new();
152
-
153
- enter_block_scope(&mut genv);
154
-
155
- let vtx = install_block_parameter(&mut genv, &mut lenv, "x".to_string());
156
-
157
- assert_eq!(lenv.get_var("x"), Some(vtx));
158
-
159
- exit_block_scope(&mut genv);
160
- }
161
-
162
- #[test]
163
- fn test_block_inherits_parent_scope_vars() {
164
- let mut genv = GlobalEnv::new();
165
-
166
- genv.scope_manager
167
- .current_scope_mut()
168
- .set_local_var("outer".to_string(), VertexId(100));
169
-
170
- enter_block_scope(&mut genv);
171
-
172
- assert_eq!(genv.scope_manager.lookup_var("outer"), Some(VertexId(100)));
173
-
174
- exit_block_scope(&mut genv);
175
- }
176
- }
@@ -12,13 +12,14 @@ use crate::graph::{MethodCallBox, VertexId};
12
12
  use crate::source_map::SourceLocation;
13
13
 
14
14
  /// Install method call and return the return value's VertexId
15
- pub fn install_method_call(
15
+ pub(crate) fn install_method_call(
16
16
  genv: &mut GlobalEnv,
17
17
  recv_vtx: VertexId,
18
18
  method_name: String,
19
19
  arg_vtxs: Vec<VertexId>,
20
20
  kwarg_vtxs: Option<HashMap<String, VertexId>>,
21
21
  location: Option<SourceLocation>,
22
+ safe_navigation: bool,
22
23
  ) -> VertexId {
23
24
  // Create Vertex for return value
24
25
  let ret_vtx = genv.new_vertex();
@@ -26,38 +27,8 @@ pub fn install_method_call(
26
27
  // Create MethodCallBox with location and argument vertices
27
28
  let box_id = genv.alloc_box_id();
28
29
  let call_box =
29
- MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, kwarg_vtxs, location);
30
+ MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, kwarg_vtxs, location, safe_navigation);
30
31
  genv.register_box(box_id, Box::new(call_box));
31
32
 
32
33
  ret_vtx
33
34
  }
34
-
35
- #[cfg(test)]
36
- mod tests {
37
- use super::*;
38
- use crate::types::Type;
39
-
40
- #[test]
41
- fn test_install_method_call_creates_vertex() {
42
- let mut genv = GlobalEnv::new();
43
-
44
- let recv_vtx = genv.new_source(Type::string());
45
- let ret_vtx =
46
- install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None, None);
47
-
48
- // Return vertex should exist
49
- assert!(genv.get_vertex(ret_vtx).is_some());
50
- }
51
-
52
- #[test]
53
- fn test_install_method_call_adds_box() {
54
- let mut genv = GlobalEnv::new();
55
-
56
- let recv_vtx = genv.new_source(Type::string());
57
- let _ret_vtx =
58
- install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None, None);
59
-
60
- // Box should be added
61
- assert_eq!(genv.box_count(), 1);
62
- }
63
- }
@@ -0,0 +1,190 @@
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
+ }