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
@@ -0,0 +1,205 @@
1
+ //! Definition Handlers - Processing Ruby class/method/module definitions
2
+ //!
3
+ //! This module is responsible for:
4
+ //! - Class definition scope management (class Foo ... end)
5
+ //! - Module definition scope management (module Bar ... end)
6
+ //! - Method definition scope management (def baz ... end)
7
+ //! - Extracting class/module names from AST nodes (including qualified names like Api::User)
8
+
9
+ use std::collections::HashMap;
10
+
11
+ use crate::env::{GlobalEnv, LocalEnv};
12
+ use crate::graph::{ChangeSet, VertexId};
13
+ use crate::types::Type;
14
+ use ruby_prism::Node;
15
+
16
+ use super::bytes_to_name;
17
+ use super::install::install_statements;
18
+ use super::parameters::install_parameters;
19
+
20
+ /// Process class definition node
21
+ pub(crate) fn process_class_node(
22
+ genv: &mut GlobalEnv,
23
+ lenv: &mut LocalEnv,
24
+ changes: &mut ChangeSet,
25
+ source: &str,
26
+ class_node: &ruby_prism::ClassNode,
27
+ ) -> Option<VertexId> {
28
+ let class_name = extract_class_name(class_node);
29
+ let superclass = class_node.superclass().and_then(|sup| extract_constant_path(&sup));
30
+
31
+ // Warn if superclass is a dynamic expression (not a constant path)
32
+ // TODO: Replace eprintln! with structured diagnostic (record_type_error or warning)
33
+ // so this is visible in LSP mode and includes source location.
34
+ if class_node.superclass().is_some() && superclass.is_none() {
35
+ eprintln!(
36
+ "[methodray] warning: dynamic superclass expression in class {}; inheritance will be ignored",
37
+ class_name
38
+ );
39
+ }
40
+
41
+ install_class(genv, class_name, superclass.as_deref());
42
+
43
+ if let Some(body) = class_node.body() {
44
+ if let Some(statements) = body.as_statements_node() {
45
+ install_statements(genv, lenv, changes, source, &statements);
46
+ }
47
+ }
48
+
49
+ exit_scope(genv);
50
+ None
51
+ }
52
+
53
+ /// Process module definition node
54
+ pub(crate) fn process_module_node(
55
+ genv: &mut GlobalEnv,
56
+ lenv: &mut LocalEnv,
57
+ changes: &mut ChangeSet,
58
+ source: &str,
59
+ module_node: &ruby_prism::ModuleNode,
60
+ ) -> Option<VertexId> {
61
+ let module_name = extract_module_name(module_node);
62
+ install_module(genv, module_name);
63
+
64
+ if let Some(body) = module_node.body() {
65
+ if let Some(statements) = body.as_statements_node() {
66
+ install_statements(genv, lenv, changes, source, &statements);
67
+ }
68
+ }
69
+
70
+ exit_scope(genv);
71
+ None
72
+ }
73
+
74
+ /// Process method definition node
75
+ pub(crate) fn process_def_node(
76
+ genv: &mut GlobalEnv,
77
+ lenv: &mut LocalEnv,
78
+ changes: &mut ChangeSet,
79
+ source: &str,
80
+ def_node: &ruby_prism::DefNode,
81
+ ) -> Option<VertexId> {
82
+ let method_name = bytes_to_name(def_node.name().as_slice());
83
+
84
+ // Check if this is a class method (def self.foo)
85
+ let is_class_method = def_node
86
+ .receiver()
87
+ .map(|r| r.as_self_node().is_some())
88
+ .unwrap_or(false);
89
+
90
+ install_method(genv, method_name.clone());
91
+
92
+ let merge_vtx = genv.scope_manager.current_method_return_vertex();
93
+
94
+ // Process parameters BEFORE processing body
95
+ let (param_vtxs, keyword_param_vtxs) = if let Some(params_node) = def_node.parameters() {
96
+ install_parameters(genv, lenv, changes, source, &params_node)
97
+ } else {
98
+ (vec![], HashMap::new())
99
+ };
100
+
101
+ let mut last_vtx = None;
102
+ if let Some(body) = def_node.body() {
103
+ if let Some(statements) = body.as_statements_node() {
104
+ last_vtx = install_statements(genv, lenv, changes, source, &statements);
105
+ }
106
+ }
107
+
108
+ // Connect last expression to merge vertex so that implicit return
109
+ // (Ruby's last-expression-is-return-value) is included in the union type
110
+ if let (Some(last), Some(merge)) = (last_vtx, merge_vtx) {
111
+ genv.add_edge(last, merge);
112
+ }
113
+
114
+ // Register user-defined method with merge vertex as return vertex
115
+ let return_vtx = merge_vtx.or(last_vtx);
116
+ if let Some(ret_vtx) = return_vtx {
117
+ let recv_type_name = genv.scope_manager.current_qualified_name();
118
+
119
+ if let Some(name) = recv_type_name {
120
+ let recv_type = if is_class_method {
121
+ Type::singleton(&name)
122
+ } else {
123
+ Type::instance(&name)
124
+ };
125
+ let kw_params = (!keyword_param_vtxs.is_empty()).then_some(keyword_param_vtxs);
126
+ genv.register_user_method(
127
+ recv_type,
128
+ &method_name,
129
+ ret_vtx,
130
+ param_vtxs,
131
+ kw_params,
132
+ );
133
+ }
134
+ }
135
+
136
+ exit_scope(genv);
137
+ None
138
+ }
139
+
140
+ /// Install class definition
141
+ fn install_class(genv: &mut GlobalEnv, class_name: String, superclass: Option<&str>) {
142
+ genv.enter_class(class_name, superclass);
143
+ }
144
+
145
+ /// Install module definition
146
+ fn install_module(genv: &mut GlobalEnv, module_name: String) {
147
+ genv.enter_module(module_name);
148
+ }
149
+
150
+ /// Install method definition
151
+ fn install_method(genv: &mut GlobalEnv, method_name: String) {
152
+ genv.enter_method(method_name);
153
+ }
154
+
155
+ /// Exit current scope (class, module, or method)
156
+ fn exit_scope(genv: &mut GlobalEnv) {
157
+ genv.exit_scope();
158
+ }
159
+
160
+ /// Extract class name from ClassNode
161
+ /// Supports both simple names (User) and qualified names (Api::V1::User)
162
+ fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
163
+ extract_constant_path(&class_node.constant_path()).unwrap_or_else(|| "UnknownClass".to_string())
164
+ }
165
+
166
+ /// Extract module name from ModuleNode
167
+ /// Supports both simple names (Utils) and qualified names (Api::V1::Utils)
168
+ fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
169
+ extract_constant_path(&module_node.constant_path())
170
+ .unwrap_or_else(|| "UnknownModule".to_string())
171
+ }
172
+
173
+ /// Extract constant path from a Node (handles both ConstantReadNode and ConstantPathNode)
174
+ ///
175
+ /// Examples:
176
+ /// - `User` (ConstantReadNode) → "User"
177
+ /// - `Api::User` (ConstantPathNode) → "Api::User"
178
+ /// - `Api::V1::User` (nested ConstantPathNode) → "Api::V1::User"
179
+ /// - `::Api::User` (absolute path with COLON3) → "Api::User"
180
+ pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
181
+ // Simple constant read: `User`
182
+ if let Some(constant_read) = node.as_constant_read_node() {
183
+ return Some(bytes_to_name(constant_read.name().as_slice()));
184
+ }
185
+
186
+ // Constant path: `Api::User` or `Api::V1::User`
187
+ if let Some(constant_path) = node.as_constant_path_node() {
188
+ // name() returns Option<ConstantId>, use as_slice() to get &[u8]
189
+ let name = constant_path
190
+ .name()
191
+ .map(|id| bytes_to_name(id.as_slice()))?;
192
+
193
+ // Get parent path if exists
194
+ if let Some(parent_node) = constant_path.parent() {
195
+ if let Some(parent_path) = extract_constant_path(&parent_node) {
196
+ return Some(format!("{}::{}", parent_path, name));
197
+ }
198
+ }
199
+
200
+ // No parent (absolute path like `::User`)
201
+ return Some(name);
202
+ }
203
+
204
+ None
205
+ }
@@ -0,0 +1,455 @@
1
+ //! Node Dispatch - Dispatch AST nodes to appropriate handlers
2
+ //!
3
+ //! This module handles the pattern matching of Ruby AST nodes
4
+ //! and dispatches them to specialized handlers.
5
+
6
+ use std::collections::HashMap;
7
+
8
+ use crate::env::{GlobalEnv, LocalEnv};
9
+ use crate::graph::{BlockParameterTypeBox, ChangeSet, VertexId};
10
+ use crate::source_map::SourceLocation;
11
+ use crate::types::Type;
12
+ use ruby_prism::Node;
13
+
14
+ use super::bytes_to_name;
15
+ use super::calls::install_method_call;
16
+ use super::variables::{
17
+ install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
18
+ install_self,
19
+ };
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
+
60
+ /// Kind of attr_* declaration
61
+ #[derive(Debug, Clone, Copy)]
62
+ pub(crate) enum AttrKind {
63
+ Reader,
64
+ Writer,
65
+ Accessor,
66
+ }
67
+
68
+ /// Result of dispatching a simple node (no child processing needed)
69
+ pub(crate) enum DispatchResult {
70
+ /// Node produced a vertex
71
+ Vertex(VertexId),
72
+ /// Node was not handled
73
+ NotHandled,
74
+ }
75
+
76
+ /// Kind of child processing needed
77
+ pub(crate) enum NeedsChildKind<'a> {
78
+ /// Instance variable write: need to process value, then call finish_ivar_write
79
+ IvarWrite { ivar_name: String, value: Node<'a> },
80
+ /// Local variable write: need to process value, then call finish_local_var_write
81
+ LocalVarWrite { var_name: String, value: Node<'a> },
82
+ /// Method call: need to process receiver, then call finish_method_call
83
+ MethodCall {
84
+ receiver: Node<'a>,
85
+ method_name: String,
86
+ location: SourceLocation,
87
+ /// Optional block attached to the method call
88
+ block: Option<Node<'a>>,
89
+ /// Arguments to the method call
90
+ arguments: Vec<Node<'a>>,
91
+ /// Whether this is a safe navigation call (`&.`)
92
+ safe_navigation: bool,
93
+ },
94
+ /// Implicit self method call: method call without explicit receiver (implicit self)
95
+ ImplicitSelfCall {
96
+ method_name: String,
97
+ location: SourceLocation,
98
+ block: Option<Node<'a>>,
99
+ arguments: Vec<Node<'a>>,
100
+ },
101
+ /// attr_reader / attr_writer / attr_accessor declaration
102
+ AttrDeclaration {
103
+ kind: AttrKind,
104
+ attr_names: Vec<String>,
105
+ },
106
+ /// include / extend declaration: `include Greetable`, `extend ClassMethods`
107
+ ModuleMixinDeclaration {
108
+ module_names: Vec<String>,
109
+ kind: MixinKind,
110
+ },
111
+ }
112
+
113
+ /// Kind of module mixin (include or extend)
114
+ #[derive(Debug, Clone, Copy)]
115
+ pub(crate) enum MixinKind {
116
+ Include,
117
+ Extend,
118
+ }
119
+
120
+ /// First pass: check if node can be handled immediately without child processing
121
+ ///
122
+ /// Note: Literals (including Array) are handled in install.rs via install_literal
123
+ /// because Array literals need child processing for element type inference.
124
+ pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -> DispatchResult {
125
+ // Instance variable read: @name
126
+ if let Some(ivar_read) = node.as_instance_variable_read_node() {
127
+ let ivar_name = bytes_to_name(ivar_read.name().as_slice());
128
+ return match install_ivar_read(genv, &ivar_name) {
129
+ Some(vtx) => DispatchResult::Vertex(vtx),
130
+ None => DispatchResult::NotHandled,
131
+ };
132
+ }
133
+
134
+ // self
135
+ if node.as_self_node().is_some() {
136
+ return DispatchResult::Vertex(install_self(genv));
137
+ }
138
+
139
+ // Local variable read: x
140
+ if let Some(read_node) = node.as_local_variable_read_node() {
141
+ let var_name = bytes_to_name(read_node.name().as_slice());
142
+ return match install_local_var_read(lenv, &var_name) {
143
+ Some(vtx) => DispatchResult::Vertex(vtx),
144
+ None => DispatchResult::NotHandled,
145
+ };
146
+ }
147
+
148
+ // ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
149
+ if let Some(const_read) = node.as_constant_read_node() {
150
+ let name = bytes_to_name(const_read.name().as_slice());
151
+ let resolved_name = genv.scope_manager.lookup_constant(&name)
152
+ .unwrap_or(name);
153
+ let vtx = genv.new_source(Type::singleton(&resolved_name));
154
+ return DispatchResult::Vertex(vtx);
155
+ }
156
+
157
+ // ConstantPathNode: Api::User → Type::Singleton("Api::User")
158
+ if node.as_constant_path_node().is_some() {
159
+ if let Some(name) = super::definitions::extract_constant_path(node) {
160
+ let vtx = genv.new_source(Type::singleton(&name));
161
+ return DispatchResult::Vertex(vtx);
162
+ }
163
+ }
164
+
165
+ DispatchResult::NotHandled
166
+ }
167
+
168
+ /// Extract symbol names from attr_* arguments (e.g., `attr_reader :name, :email`)
169
+ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
170
+ call_node
171
+ .arguments()
172
+ .map(|args| {
173
+ args.arguments()
174
+ .iter()
175
+ .filter_map(|arg| {
176
+ arg.as_symbol_node().map(|sym| {
177
+ bytes_to_name(sym.unescaped())
178
+ })
179
+ })
180
+ .collect()
181
+ })
182
+ .unwrap_or_default()
183
+ }
184
+
185
+ /// Extract module names from include/extend arguments
186
+ fn extract_mixin_module_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
187
+ call_node
188
+ .arguments()
189
+ .map(|args| {
190
+ args.arguments()
191
+ .iter()
192
+ .filter_map(|arg| super::definitions::extract_constant_path(&arg))
193
+ .collect()
194
+ })
195
+ .unwrap_or_default()
196
+ }
197
+
198
+ /// Check if node needs child processing
199
+ pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsChildKind<'a>> {
200
+ // Instance variable write: @name = value
201
+ if let Some(ivar_write) = node.as_instance_variable_write_node() {
202
+ let ivar_name = bytes_to_name(ivar_write.name().as_slice());
203
+ return Some(NeedsChildKind::IvarWrite {
204
+ ivar_name,
205
+ value: ivar_write.value(),
206
+ });
207
+ }
208
+
209
+ // Local variable write: x = value
210
+ if let Some(write_node) = node.as_local_variable_write_node() {
211
+ let var_name = bytes_to_name(write_node.name().as_slice());
212
+ return Some(NeedsChildKind::LocalVarWrite {
213
+ var_name,
214
+ value: write_node.value(),
215
+ });
216
+ }
217
+
218
+ // Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
219
+ if let Some(call_node) = node.as_call_node() {
220
+ let method_name = bytes_to_name(call_node.name().as_slice());
221
+ let block = call_node.block();
222
+ let arguments: Vec<Node<'a>> = call_node
223
+ .arguments()
224
+ .map(|args| args.arguments().iter().collect())
225
+ .unwrap_or_default();
226
+
227
+ if let Some(receiver) = call_node.receiver() {
228
+ // Explicit receiver: x.upcase, x.each { |i| ... }
229
+ let prism_location = call_node
230
+ .call_operator_loc()
231
+ .unwrap_or_else(|| node.location());
232
+ let location =
233
+ SourceLocation::from_prism_location_with_source(&prism_location, source);
234
+
235
+ return Some(NeedsChildKind::MethodCall {
236
+ receiver,
237
+ method_name,
238
+ location,
239
+ block,
240
+ arguments,
241
+ safe_navigation: call_node.is_safe_navigation(),
242
+ });
243
+ } else {
244
+ // No receiver: implicit self method call (e.g., `name`, `puts "hello"`)
245
+
246
+ if let Some(kind) = match method_name.as_str() {
247
+ "attr_reader" => Some(AttrKind::Reader),
248
+ "attr_writer" => Some(AttrKind::Writer),
249
+ "attr_accessor" => Some(AttrKind::Accessor),
250
+ _ => None,
251
+ } {
252
+ let attr_names = extract_symbol_names(&call_node);
253
+ if !attr_names.is_empty() {
254
+ return Some(NeedsChildKind::AttrDeclaration { kind, attr_names });
255
+ }
256
+ return None;
257
+ }
258
+
259
+ let mixin_kind = match method_name.as_str() {
260
+ "include" => Some(MixinKind::Include),
261
+ "extend" => Some(MixinKind::Extend),
262
+ _ => None,
263
+ };
264
+
265
+ if let Some(kind) = mixin_kind {
266
+ let module_names = extract_mixin_module_names(&call_node);
267
+ if !module_names.is_empty() {
268
+ return Some(NeedsChildKind::ModuleMixinDeclaration { module_names, kind });
269
+ }
270
+ return None;
271
+ }
272
+
273
+ let prism_location = call_node
274
+ .message_loc()
275
+ .unwrap_or_else(|| node.location());
276
+ let location =
277
+ SourceLocation::from_prism_location_with_source(&prism_location, source);
278
+
279
+ return Some(NeedsChildKind::ImplicitSelfCall {
280
+ method_name,
281
+ location,
282
+ block,
283
+ arguments,
284
+ });
285
+ }
286
+ }
287
+
288
+ None
289
+ }
290
+
291
+ /// Process a node that needs child processing
292
+ ///
293
+ /// This function handles the second phase of two-phase dispatch:
294
+ /// 1. `dispatch_needs_child` identifies the node kind and extracts data
295
+ /// 2. `process_needs_child` processes child nodes and completes the operation
296
+ pub(crate) fn process_needs_child(
297
+ genv: &mut GlobalEnv,
298
+ lenv: &mut LocalEnv,
299
+ changes: &mut ChangeSet,
300
+ source: &str,
301
+ kind: NeedsChildKind,
302
+ ) -> Option<VertexId> {
303
+ match kind {
304
+ NeedsChildKind::IvarWrite { ivar_name, value } => {
305
+ let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
306
+ Some(finish_ivar_write(genv, ivar_name, value_vtx))
307
+ }
308
+ NeedsChildKind::LocalVarWrite { var_name, value } => {
309
+ let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
310
+ Some(finish_local_var_write(genv, lenv, changes, var_name, value_vtx))
311
+ }
312
+ NeedsChildKind::MethodCall {
313
+ receiver,
314
+ method_name,
315
+ location,
316
+ block,
317
+ arguments,
318
+ safe_navigation,
319
+ } => {
320
+ let recv_vtx = super::install::install_node(genv, lenv, changes, source, &receiver)?;
321
+ process_method_call_common(
322
+ genv, lenv, changes, source,
323
+ MethodCallContext { recv_vtx, method_name, location, block, arguments, safe_navigation },
324
+ )
325
+ }
326
+ NeedsChildKind::ImplicitSelfCall {
327
+ method_name,
328
+ location,
329
+ block,
330
+ arguments,
331
+ } => {
332
+ // Use qualified name to match method registration in definitions.rs
333
+ let recv_vtx = if let Some(name) = genv.scope_manager.current_qualified_name() {
334
+ genv.new_source(Type::instance(&name))
335
+ } else {
336
+ genv.new_source(Type::instance("Object"))
337
+ };
338
+ process_method_call_common(
339
+ genv, lenv, changes, source,
340
+ // Implicit self calls cannot use safe navigation (`&.` requires explicit receiver)
341
+ MethodCallContext { recv_vtx, method_name, location, block, arguments, safe_navigation: false },
342
+ )
343
+ }
344
+ NeedsChildKind::AttrDeclaration { kind, attr_names } => {
345
+ super::attributes::process_attr_declaration(genv, kind, attr_names);
346
+ None
347
+ }
348
+ NeedsChildKind::ModuleMixinDeclaration { module_names, kind } => {
349
+ if let Some(class_name) = genv.scope_manager.current_qualified_name() {
350
+ // Ruby processes `include/extend A, B` right-to-left (B first, then A on top),
351
+ // so A ends up with higher MRO priority. Reverse to match this behavior.
352
+ for module_name in module_names.iter().rev() {
353
+ match kind {
354
+ MixinKind::Include => genv.record_include(&class_name, module_name),
355
+ MixinKind::Extend => genv.record_extend(&class_name, module_name),
356
+ }
357
+ }
358
+ }
359
+ None
360
+ }
361
+ }
362
+ }
363
+
364
+ /// Finish instance variable write after child is processed
365
+ fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
366
+ install_ivar_write(genv, ivar_name, value_vtx)
367
+ }
368
+
369
+ /// Finish local variable write after child is processed
370
+ fn finish_local_var_write(
371
+ genv: &mut GlobalEnv,
372
+ lenv: &mut LocalEnv,
373
+ changes: &mut ChangeSet,
374
+ var_name: String,
375
+ value_vtx: VertexId,
376
+ ) -> VertexId {
377
+ install_local_var_write(genv, lenv, changes, var_name, value_vtx)
378
+ }
379
+
380
+ /// Bundled parameters for method call processing
381
+ struct MethodCallContext<'a> {
382
+ recv_vtx: VertexId,
383
+ method_name: String,
384
+ location: SourceLocation,
385
+ block: Option<Node<'a>>,
386
+ arguments: Vec<Node<'a>>,
387
+ safe_navigation: bool,
388
+ }
389
+
390
+ /// MethodCall / ImplicitSelfCall common processing:
391
+ /// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
392
+ fn process_method_call_common<'a>(
393
+ genv: &mut GlobalEnv,
394
+ lenv: &mut LocalEnv,
395
+ changes: &mut ChangeSet,
396
+ source: &str,
397
+ ctx: MethodCallContext<'a>,
398
+ ) -> Option<VertexId> {
399
+ let MethodCallContext {
400
+ recv_vtx,
401
+ method_name,
402
+ location,
403
+ block,
404
+ arguments,
405
+ safe_navigation,
406
+ } = ctx;
407
+ if method_name == "!" {
408
+ return Some(super::operators::process_not_operator(genv));
409
+ }
410
+
411
+ let (positional_arg_vtxs, kwarg_vtxs) =
412
+ collect_arguments(genv, lenv, changes, source, arguments.into_iter());
413
+
414
+ if let Some(block_node) = block {
415
+ if let Some(block) = block_node.as_block_node() {
416
+ let param_vtxs = super::blocks::process_block_node_with_params(
417
+ genv, lenv, changes, source, &block,
418
+ );
419
+
420
+ if !param_vtxs.is_empty() {
421
+ let box_id = genv.alloc_box_id();
422
+ let block_box = BlockParameterTypeBox::new(
423
+ box_id,
424
+ recv_vtx,
425
+ method_name.clone(),
426
+ param_vtxs,
427
+ );
428
+ genv.register_box(box_id, Box::new(block_box));
429
+ }
430
+ }
431
+ }
432
+
433
+ Some(finish_method_call(
434
+ genv,
435
+ recv_vtx,
436
+ method_name,
437
+ positional_arg_vtxs,
438
+ kwarg_vtxs,
439
+ location,
440
+ safe_navigation,
441
+ ))
442
+ }
443
+
444
+ /// Finish method call after receiver is processed
445
+ fn finish_method_call(
446
+ genv: &mut GlobalEnv,
447
+ recv_vtx: VertexId,
448
+ method_name: String,
449
+ arg_vtxs: Vec<VertexId>,
450
+ kwarg_vtxs: Option<HashMap<String, VertexId>>,
451
+ location: SourceLocation,
452
+ safe_navigation: bool,
453
+ ) -> VertexId {
454
+ install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location), safe_navigation)
455
+ }