type-guessr 0.0.2 → 0.0.3

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/ruby_lsp/type_guessr/addon.rb +4 -5
  4. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +17 -0
  5. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +3 -3
  6. data/lib/ruby_lsp/type_guessr/debug_server.rb +2 -2
  7. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  8. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  9. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  10. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  11. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  12. data/lib/ruby_lsp/type_guessr/hover.rb +46 -40
  13. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  14. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +90 -16
  15. data/lib/type-guessr.rb +2 -13
  16. data/lib/type_guessr/core/cache/gem_signature_cache.rb +3 -2
  17. data/lib/type_guessr/core/cache.rb +5 -0
  18. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +2 -2
  19. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  20. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  21. data/lib/type_guessr/core/converter/context.rb +144 -0
  22. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  23. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  24. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  25. data/lib/type_guessr/core/converter/prism_converter.rb +9 -1682
  26. data/lib/type_guessr/core/converter/rbs_converter.rb +15 -1
  27. data/lib/type_guessr/core/converter/registration.rb +100 -0
  28. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  29. data/lib/type_guessr/core/converter.rb +4 -0
  30. data/lib/type_guessr/core/index.rb +3 -0
  31. data/lib/type_guessr/core/inference/resolver.rb +206 -208
  32. data/lib/type_guessr/core/inference.rb +4 -0
  33. data/lib/type_guessr/core/ir.rb +3 -0
  34. data/lib/type_guessr/core/logger.rb +3 -5
  35. data/lib/type_guessr/core/registry/method_registry.rb +9 -0
  36. data/lib/type_guessr/core/registry/signature_registry.rb +73 -16
  37. data/lib/type_guessr/core/registry.rb +6 -0
  38. data/lib/type_guessr/core/type_serializer.rb +18 -14
  39. data/lib/type_guessr/core/type_simplifier.rb +5 -5
  40. data/lib/type_guessr/core/types.rb +64 -22
  41. data/lib/type_guessr/core.rb +29 -0
  42. data/lib/type_guessr/mcp/server.rb +55 -46
  43. data/lib/type_guessr/mcp/standalone_runtime.rb +70 -110
  44. data/lib/type_guessr/version.rb +1 -1
  45. metadata +25 -5
  46. data/.mcp.json +0 -9
@@ -0,0 +1,425 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeGuessr
4
+ module Core
5
+ module Converter
6
+ # Control flow (if/case/begin/or/and), variable merging, and rescue methods for PrismConverter
7
+ class PrismConverter
8
+ # Register exception variable from rescue clause (=> e)
9
+ # @param rescue_clause [Prism::RescueNode] The rescue clause
10
+ # @param context [Context] Conversion context
11
+ private def register_rescue_variable(rescue_clause, context)
12
+ var_name = rescue_clause.reference.name
13
+ exception_type = infer_rescue_exception_type(rescue_clause.exceptions)
14
+ loc = convert_loc(rescue_clause.reference.location)
15
+
16
+ value_node = IR::LiteralNode.new(exception_type, nil, nil, [], loc)
17
+
18
+ write_node = IR::LocalWriteNode.new(var_name, value_node, [], loc)
19
+
20
+ context.register_variable(var_name, write_node)
21
+ end
22
+
23
+ # Infer exception type from rescue clause's exception list
24
+ # @param exceptions [Array<Prism::Node>] List of exception class nodes
25
+ # @return [Types::ClassInstance, Types::Union] Inferred exception type
26
+ private def infer_rescue_exception_type(exceptions)
27
+ # Default to StandardError if no exception class specified (rescue => e)
28
+ return Types::ClassInstance.new("StandardError") if exceptions.empty?
29
+
30
+ types = exceptions.map do |exc|
31
+ class_name = case exc
32
+ when Prism::ConstantReadNode
33
+ exc.name.to_s
34
+ when Prism::ConstantPathNode
35
+ # Handle namespaced constants like Net::HTTPError
36
+ begin
37
+ exc.full_name
38
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
39
+ "StandardError"
40
+ end
41
+ else
42
+ "StandardError"
43
+ end
44
+ Types::ClassInstance.new(class_name)
45
+ end
46
+
47
+ types.size == 1 ? types.first : Types::Union.new(types)
48
+ end
49
+
50
+ private def convert_if(prism_node, context)
51
+ # Convert then branch
52
+ then_context = context.fork(:then)
53
+ then_node = convert(prism_node.statements, then_context) if prism_node.statements
54
+
55
+ # Convert else branch (could be IfNode, ElseNode, or nil)
56
+ else_context = context.fork(:else)
57
+ else_node = if prism_node.subsequent
58
+ case prism_node.subsequent
59
+ when Prism::IfNode
60
+ convert_if(prism_node.subsequent, else_context)
61
+ when Prism::ElseNode
62
+ convert(prism_node.subsequent.statements, else_context)
63
+ end
64
+ end
65
+
66
+ # Create merge nodes for variables modified in branches
67
+ merge_modified_variables(context, then_context, else_context, then_node, else_node, prism_node.location)
68
+ end
69
+
70
+ private def convert_unless(prism_node, context)
71
+ # Unless is like if with inverted condition
72
+ # We treat the unless body as the "else" branch and the consequent as "then"
73
+
74
+ unless_context = context.fork(:unless)
75
+ unless_node = convert(prism_node.statements, unless_context) if prism_node.statements
76
+
77
+ else_context = context.fork(:else)
78
+ else_node = (convert(prism_node.else_clause.statements, else_context) if prism_node.else_clause)
79
+
80
+ result = merge_modified_variables(context, unless_context, else_context, unless_node, else_node, prism_node.location)
81
+
82
+ # Guard clause narrowing: `return/raise unless x` → x is truthy after
83
+ narrow_guard_variable(prism_node.predicate, :truthy, context, prism_node.location) if guard_clause?(unless_node)
84
+
85
+ result
86
+ end
87
+
88
+ private def convert_case(prism_node, context)
89
+ branches = []
90
+ branch_contexts = []
91
+
92
+ # Convert each when clause
93
+ prism_node.conditions&.each do |when_node|
94
+ when_context = context.fork(:when)
95
+ if when_node.statements
96
+ when_result = convert(when_node.statements, when_context)
97
+ # Skip non-returning branches (raise, fail, etc.)
98
+ unless non_returning?(when_result)
99
+ branches << (when_result || create_nil_literal(prism_node.location))
100
+ branch_contexts << when_context
101
+ end
102
+ else
103
+ # Empty when clause → nil
104
+ branches << create_nil_literal(prism_node.location)
105
+ branch_contexts << when_context
106
+ end
107
+ end
108
+
109
+ # Convert else clause
110
+ if prism_node.else_clause
111
+ else_context = context.fork(:else)
112
+ else_result = convert(prism_node.else_clause.statements, else_context)
113
+ # Skip non-returning else clause (raise, fail, etc.)
114
+ unless non_returning?(else_result)
115
+ branches << (else_result || create_nil_literal(prism_node.location))
116
+ branch_contexts << else_context
117
+ end
118
+ else
119
+ # If no else clause, nil is possible
120
+ branches << create_nil_literal(prism_node.location)
121
+ end
122
+
123
+ # Merge modified variables across all branches
124
+ merge_case_variables(context, branch_contexts, branches, prism_node.location)
125
+ end
126
+
127
+ private def convert_case_match(prism_node, context)
128
+ # Pattern matching case (Ruby 3.0+)
129
+ # For now, treat it similarly to regular case
130
+ convert_case(prism_node, context)
131
+ end
132
+
133
+ private def convert_statements(prism_node, context)
134
+ last_node = nil
135
+ prism_node.body.each do |stmt|
136
+ last_node = convert(stmt, context)
137
+ end
138
+ last_node
139
+ end
140
+
141
+ # Helper to convert an array of statement bodies
142
+ # @param body [Array<Prism::Node>, nil] Array of statement nodes
143
+ # @param context [Context] Conversion context
144
+ # @return [Array<IR::Node>] Array of converted IR nodes
145
+ private def convert_statements_body(body, context)
146
+ return [] unless body
147
+
148
+ nodes = []
149
+ body.each do |stmt|
150
+ node = convert(stmt, context)
151
+ nodes << node if node
152
+ end
153
+ nodes
154
+ end
155
+
156
+ # Convert begin/rescue/ensure block
157
+ private def convert_begin(prism_node, context)
158
+ body_nodes = extract_begin_body_nodes(prism_node, context)
159
+ # Return the last node (represents the value of the begin block)
160
+ body_nodes.last
161
+ end
162
+
163
+ # Convert || (or) operator to OrNode
164
+ # a || b → LHS evaluated first, RHS only if LHS is falsy
165
+ private def convert_or_node(prism_node, context)
166
+ left_node = convert(prism_node.left, context)
167
+ right_node = convert(prism_node.right, context)
168
+
169
+ return nil if left_node.nil? && right_node.nil?
170
+ return left_node if right_node.nil?
171
+ return right_node if left_node.nil?
172
+
173
+ IR::OrNode.new(left_node, right_node, [], convert_loc(prism_node.location))
174
+ end
175
+
176
+ # Convert && (and) operator to MergeNode
177
+ # a && b → result is either a or b (short-circuit evaluation)
178
+ private def convert_and_node(prism_node, context)
179
+ left_node = convert(prism_node.left, context)
180
+ right_node = convert(prism_node.right, context)
181
+
182
+ branches = [left_node, right_node].compact
183
+ return nil if branches.empty?
184
+ return branches.first if branches.size == 1
185
+
186
+ IR::MergeNode.new(branches, [], convert_loc(prism_node.location))
187
+ end
188
+
189
+ # Convert h[:key] ||= value → OrNode(h.[](:key), value)
190
+ private def convert_index_or_write(prism_node, context)
191
+ receiver_node = convert(prism_node.receiver, context)
192
+ args = prism_node.arguments&.arguments&.map { |arg| convert(arg, context) } || []
193
+ value_node = convert(prism_node.value, context)
194
+
195
+ read_call = IR::CallNode.new(:[], receiver_node, args, [], nil, false, [], convert_loc(prism_node.opening_loc))
196
+
197
+ IR::OrNode.new(read_call, value_node, [], convert_loc(prism_node.location))
198
+ end
199
+
200
+ # Convert multiple assignment (a, b, c = expr)
201
+ # Creates synthetic value[index] calls for each target variable
202
+ private def convert_multi_write(prism_node, context)
203
+ value_node = convert(prism_node.value, context)
204
+
205
+ # lefts: variables before splat → value[0], value[1], ...
206
+ prism_node.lefts.each_with_index do |target, index|
207
+ assign_multi_write_target(target, value_node, index, context)
208
+ end
209
+
210
+ # rest: splat variable → ArrayType(Unknown)
211
+ if prism_node.rest.is_a?(Prism::SplatNode) && prism_node.rest.expression
212
+ splat_target = prism_node.rest.expression
213
+ splat_value = IR::LiteralNode.new(
214
+ Types::ArrayType.new, nil, nil, [], convert_loc(splat_target.location)
215
+ )
216
+ register_multi_write_variable(splat_target, splat_value, context)
217
+ end
218
+
219
+ # rights: variables after splat → value[-n], value[-(n-1)], ...
220
+ prism_node.rights.each_with_index do |target, index|
221
+ negative_index = -(prism_node.rights.size - index)
222
+ assign_multi_write_target(target, value_node, negative_index, context)
223
+ end
224
+
225
+ value_node
226
+ end
227
+
228
+ # Create synthetic value[index] call and register the target variable
229
+ private def assign_multi_write_target(target, value_node, index, context)
230
+ loc = convert_loc(target.location)
231
+ index_literal = IR::LiteralNode.new(
232
+ Types::ClassInstance.for("Integer"), index, nil, [], loc
233
+ )
234
+ call_node = IR::CallNode.new(:[], value_node, [index_literal], [], nil, false, [], loc)
235
+ register_multi_write_variable(target, call_node, context)
236
+ end
237
+
238
+ # Register a multi-write target variable (local or instance variable)
239
+ private def register_multi_write_variable(target, value_node, context)
240
+ loc = convert_loc(target.location)
241
+ case target
242
+ when Prism::LocalVariableTargetNode
243
+ write_node = IR::LocalWriteNode.new(target.name, value_node, [], loc)
244
+ context.register_variable(target.name, write_node)
245
+ when Prism::InstanceVariableTargetNode
246
+ write_node = IR::InstanceVariableWriteNode.new(
247
+ target.name, context.current_class_name, value_node, [], loc
248
+ )
249
+ context.register_instance_variable(target.name, write_node)
250
+ end
251
+ end
252
+
253
+ # Extract all body nodes from a BeginNode (for DefNode bodies with rescue/ensure)
254
+ # @param begin_node [Prism::BeginNode] The begin node
255
+ # @param context [Context] Conversion context
256
+ # @return [Array<IR::Node>] Array of all body nodes
257
+ private def extract_begin_body_nodes(begin_node, context)
258
+ body_nodes = []
259
+
260
+ # Convert main body statements
261
+ body_nodes.concat(convert_statements_body(begin_node.statements.body, context)) if begin_node.statements
262
+
263
+ # Convert rescue clause(s)
264
+ rescue_clause = begin_node.rescue_clause
265
+ while rescue_clause
266
+ # Register exception variable (=> e) if present
267
+ register_rescue_variable(rescue_clause, context) if rescue_clause.reference.is_a?(Prism::LocalVariableTargetNode)
268
+
269
+ rescue_nodes = convert_statements_body(rescue_clause.statements&.body, context)
270
+ body_nodes.concat(rescue_nodes)
271
+ rescue_clause = rescue_clause.subsequent
272
+ end
273
+
274
+ # Convert else clause
275
+ if begin_node.else_clause
276
+ else_nodes = convert_statements_body(begin_node.else_clause.statements&.body, context)
277
+ body_nodes.concat(else_nodes)
278
+ end
279
+
280
+ # Convert ensure clause
281
+ if begin_node.ensure_clause
282
+ ensure_nodes = convert_statements_body(begin_node.ensure_clause.statements&.body, context)
283
+ body_nodes.concat(ensure_nodes)
284
+ end
285
+
286
+ body_nodes
287
+ end
288
+
289
+ private def merge_modified_variables(parent_context, then_context, else_context, then_node, else_node, location)
290
+ # Skip non-returning branches (raise, fail, etc.)
291
+ then_node = nil if non_returning?(then_node)
292
+ else_node = nil if non_returning?(else_node)
293
+
294
+ # Track which variables were modified in each branch
295
+ then_vars = then_context&.local_variables || []
296
+ else_vars = else_context&.local_variables || []
297
+
298
+ # All variables modified in either branch
299
+ modified_vars = (then_vars + else_vars).uniq
300
+
301
+ # Create MergeNode for each modified variable
302
+ modified_vars.each do |var_name|
303
+ then_val = then_context&.variables&.[](var_name)
304
+ else_val = else_context&.variables&.[](var_name)
305
+
306
+ # Get the original value from parent context (before if statement)
307
+ original_val = parent_context.lookup_variable(var_name)
308
+
309
+ # Determine branches for merge
310
+ branches = []
311
+ if then_val
312
+ branches << then_val
313
+ elsif original_val
314
+ # Variable not modified in then branch, use original
315
+ branches << original_val
316
+ end
317
+
318
+ if else_val
319
+ branches << else_val
320
+ elsif original_val
321
+ # Variable not modified in else branch, use original
322
+ branches << original_val
323
+ elsif then_val
324
+ # Inline if/unless: no else branch and no original value
325
+ # Add nil to represent "variable may not be assigned"
326
+ nil_node = IR::LiteralNode.new(
327
+ Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location)
328
+ )
329
+ branches << nil_node
330
+ end
331
+
332
+ # Create MergeNode only if we have multiple branches
333
+ if branches.size > 1
334
+ merge_node = IR::MergeNode.new(branches.uniq, [], convert_loc(location))
335
+ parent_context.register_variable(var_name, merge_node)
336
+ elsif branches.size == 1
337
+ # Only one branch has a value, use it directly
338
+ parent_context.register_variable(var_name, branches.first)
339
+ end
340
+ end
341
+
342
+ # Return MergeNode for the if expression value
343
+ if then_node && else_node
344
+ IR::MergeNode.new([then_node, else_node].compact, [], convert_loc(location))
345
+ elsif then_node || else_node
346
+ # Modifier form: one branch only → value or nil
347
+ branch_node = then_node || else_node
348
+ nil_node = IR::LiteralNode.new(
349
+ Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location)
350
+ )
351
+ IR::MergeNode.new([branch_node, nil_node], [], convert_loc(location))
352
+ end
353
+ end
354
+
355
+ private def merge_case_variables(parent_context, branch_contexts, branches, location)
356
+ # Collect all variables modified in any branch
357
+ all_modified_vars = branch_contexts.flat_map { |ctx| ctx&.local_variables || [] }.uniq
358
+
359
+ # Create MergeNode for each modified variable
360
+ all_modified_vars.each do |var_name|
361
+ # Get original value from parent context
362
+ original_val = parent_context.lookup_variable(var_name)
363
+
364
+ # Build branches array
365
+ merge_branches = branch_contexts.map.with_index do |ctx, _idx|
366
+ ctx&.variables&.[](var_name) || original_val
367
+ end.compact.uniq
368
+
369
+ # Create MergeNode if we have multiple different values
370
+ if merge_branches.size > 1
371
+ merge_node = IR::MergeNode.new(merge_branches, [], convert_loc(location))
372
+ parent_context.register_variable(var_name, merge_node)
373
+ elsif merge_branches.size == 1
374
+ parent_context.register_variable(var_name, merge_branches.first)
375
+ end
376
+ end
377
+
378
+ # Return MergeNode for the case expression value
379
+ if branches.size > 1
380
+ IR::MergeNode.new(branches.compact.uniq, [], convert_loc(location))
381
+ elsif branches.size == 1
382
+ branches.first
383
+ end
384
+ end
385
+
386
+ private def create_nil_literal(location)
387
+ IR::LiteralNode.new(Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location))
388
+ end
389
+
390
+ # Check if a node represents a non-returning expression (raise, fail, exit, abort)
391
+ # These should be excluded from branch type inference
392
+ private def non_returning?(node)
393
+ return false unless node.is_a?(IR::CallNode)
394
+
395
+ node.receiver.nil? && %i[raise fail exit abort].include?(node.method)
396
+ end
397
+
398
+ # Check if a node represents a guard clause body (exits the method)
399
+ # Includes both non-returning expressions (raise/fail) and explicit returns
400
+ private def guard_clause?(node)
401
+ node.is_a?(IR::ReturnNode) || non_returning?(node)
402
+ end
403
+
404
+ # After a guard clause (`return/raise unless x`), narrow the guarded variable
405
+ # to remove falsy types (NilClass, FalseClass)
406
+ private def narrow_guard_variable(predicate, kind, context, location)
407
+ case predicate
408
+ when Prism::LocalVariableReadNode
409
+ write_node = context.lookup_variable(predicate.name)
410
+ return unless write_node
411
+
412
+ narrow = IR::NarrowNode.new(write_node, kind, write_node.called_methods, convert_loc(location))
413
+ context.register_variable(predicate.name, narrow)
414
+ when Prism::InstanceVariableReadNode
415
+ write_node = context.lookup_instance_variable(predicate.name)
416
+ return unless write_node
417
+
418
+ narrow = IR::NarrowNode.new(write_node, kind, write_node.called_methods, convert_loc(location))
419
+ context.narrow_instance_variable(predicate.name, narrow)
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeGuessr
4
+ module Core
5
+ module Converter
6
+ # Method/class/module/constant definition methods for PrismConverter
7
+ class PrismConverter
8
+ private def convert_def(prism_node, context, module_function: false)
9
+ def_context = context.fork(:method)
10
+ def_context.current_method = prism_node.name.to_s
11
+ def_context.in_singleton_method = prism_node.receiver.is_a?(Prism::SelfNode)
12
+
13
+ # Convert parameters
14
+ params = []
15
+ if prism_node.parameters
16
+ parameters_node = prism_node.parameters
17
+
18
+ # Required parameters
19
+ parameters_node.requireds&.each do |param|
20
+ extract_param_nodes(param, :required, def_context).each do |param_node|
21
+ params << param_node
22
+ end
23
+ end
24
+
25
+ # Optional parameters
26
+ parameters_node.optionals&.each do |param|
27
+ default_node = convert(param.value, def_context)
28
+ param_node = IR::ParamNode.new(param.name, :optional, default_node, [], convert_loc(param.location))
29
+ params << param_node
30
+ def_context.register_variable(param.name, param_node)
31
+ end
32
+
33
+ # Rest parameter (*args)
34
+ if parameters_node.rest.is_a?(Prism::RestParameterNode)
35
+ rest = parameters_node.rest
36
+ param_node = IR::ParamNode.new(rest.name || :*, :rest, nil, [], convert_loc(rest.location))
37
+ params << param_node
38
+ def_context.register_variable(rest.name, param_node) if rest.name
39
+ end
40
+
41
+ # Required keyword parameters (name:)
42
+ parameters_node.keywords&.each do |kw|
43
+ case kw
44
+ when Prism::RequiredKeywordParameterNode
45
+ param_node = IR::ParamNode.new(kw.name, :keyword_required, nil, [], convert_loc(kw.location))
46
+ params << param_node
47
+ def_context.register_variable(kw.name, param_node)
48
+ when Prism::OptionalKeywordParameterNode
49
+ default_node = convert(kw.value, def_context)
50
+ param_node = IR::ParamNode.new(kw.name, :keyword_optional, default_node, [], convert_loc(kw.location))
51
+ params << param_node
52
+ def_context.register_variable(kw.name, param_node)
53
+ end
54
+ end
55
+
56
+ # Keyword rest parameter (**kwargs)
57
+ if parameters_node.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
58
+ kwrest = parameters_node.keyword_rest
59
+ param_node = IR::ParamNode.new(kwrest.name || :**, :keyword_rest, nil, [], convert_loc(kwrest.location))
60
+ params << param_node
61
+ def_context.register_variable(kwrest.name, param_node) if kwrest.name
62
+ elsif parameters_node.keyword_rest.is_a?(Prism::ForwardingParameterNode)
63
+ # Forwarding parameter (...)
64
+ fwd = parameters_node.keyword_rest
65
+ param_node = IR::ParamNode.new(:"...", :forwarding, nil, [], convert_loc(fwd.location))
66
+ params << param_node
67
+ end
68
+
69
+ # Block parameter (&block)
70
+ if parameters_node.block
71
+ block = parameters_node.block
72
+ param_node = IR::ParamNode.new(block.name || :&, :block, nil, [], convert_loc(block.location))
73
+ params << param_node
74
+ def_context.register_variable(block.name, param_node) if block.name
75
+ end
76
+ end
77
+
78
+ # Convert method body - collect all body nodes
79
+ body_nodes = []
80
+
81
+ if prism_node.body.is_a?(Prism::StatementsNode)
82
+ prism_node.body.body.each do |stmt|
83
+ node = convert(stmt, def_context)
84
+ body_nodes << node if node
85
+ end
86
+ elsif prism_node.body.is_a?(Prism::BeginNode)
87
+ # Method with rescue/ensure block
88
+ begin_node = prism_node.body
89
+ body_nodes = extract_begin_body_nodes(begin_node, def_context)
90
+ elsif prism_node.body
91
+ node = convert(prism_node.body, def_context)
92
+ body_nodes << node if node
93
+ end
94
+
95
+ # Collect all return points: explicit returns + implicit last expression
96
+ return_node = compute_return_node(body_nodes, prism_node.name_loc)
97
+
98
+ IR::DefNode.new(
99
+ prism_node.name,
100
+ def_context.current_class_name,
101
+ params,
102
+ return_node,
103
+ body_nodes,
104
+ [],
105
+ convert_loc(prism_node.name_loc),
106
+ prism_node.receiver.is_a?(Prism::SelfNode),
107
+ module_function: module_function
108
+ )
109
+ end
110
+
111
+ # Compute the return node for a method by collecting all return points
112
+ # @param body_nodes [Array<IR::Node>] All nodes in the method body
113
+ # @param loc [Prism::Location] Location for the MergeNode if needed
114
+ # @return [IR::Node, nil] The return node (MergeNode if multiple returns)
115
+ private def compute_return_node(body_nodes, loc)
116
+ return nil if body_nodes.empty?
117
+
118
+ # Collect all explicit returns from the body
119
+ explicit_returns = collect_returns(body_nodes)
120
+
121
+ # The implicit return is the last non-ReturnNode in body
122
+ implicit_return = body_nodes.grep_v(IR::ReturnNode).last
123
+
124
+ # Determine all return points
125
+ return_points = explicit_returns.dup
126
+ return_points << implicit_return if implicit_return && !last_node_returns?(body_nodes)
127
+
128
+ case return_points.size
129
+ when 0
130
+ nil
131
+ when 1
132
+ return_points.first
133
+ else
134
+ IR::MergeNode.new(return_points, [], convert_loc(loc))
135
+ end
136
+ end
137
+
138
+ # Collect all ReturnNode instances from body nodes (recursive)
139
+ # Searches inside MergeNode branches to find nested returns from if/case
140
+ # @param nodes [Array<IR::Node>] Nodes to search
141
+ # @return [Array<IR::ReturnNode>] All explicit return nodes
142
+ private def collect_returns(nodes)
143
+ returns = []
144
+ nodes.each do |node|
145
+ case node
146
+ when IR::ReturnNode
147
+ returns << node
148
+ when IR::MergeNode
149
+ returns.concat(collect_returns(node.branches))
150
+ when IR::OrNode
151
+ returns.concat(collect_returns([node.lhs, node.rhs]))
152
+ end
153
+ end
154
+ returns
155
+ end
156
+
157
+ # Check if the last node in body is a ReturnNode
158
+ # @param body_nodes [Array<IR::Node>] Body nodes
159
+ # @return [Boolean]
160
+ private def last_node_returns?(body_nodes)
161
+ body_nodes.last.is_a?(IR::ReturnNode)
162
+ end
163
+
164
+ private def convert_constant_read(prism_node, context)
165
+ name = case prism_node
166
+ when Prism::ConstantReadNode
167
+ prism_node.name.to_s
168
+ when Prism::ConstantPathNode
169
+ prism_node.slice
170
+ else
171
+ prism_node.to_s
172
+ end
173
+
174
+ IR::ConstantNode.new(name, context.lookup_constant(name), [], convert_loc(prism_node.location))
175
+ end
176
+
177
+ private def convert_constant_write(prism_node, context)
178
+ value_node = convert(prism_node.value, context)
179
+ context.register_constant(prism_node.name.to_s, value_node)
180
+ IR::ConstantNode.new(prism_node.name.to_s, value_node, [], convert_loc(prism_node.location))
181
+ end
182
+
183
+ private def convert_class_or_module(prism_node, context)
184
+ # Get class/module name first
185
+ name = case prism_node.constant_path
186
+ when Prism::ConstantReadNode
187
+ prism_node.constant_path.name.to_s
188
+ when Prism::ConstantPathNode
189
+ prism_node.constant_path.slice
190
+ else
191
+ "Anonymous"
192
+ end
193
+
194
+ # Create a new context for class/module scope with the full class path
195
+ class_context = context.fork(:class)
196
+ parent_path = context.current_class_name
197
+ full_name = parent_path ? "#{parent_path}::#{name}" : name
198
+ class_context.current_class = full_name
199
+
200
+ # Collect all method definitions and nested classes from the body
201
+ methods = []
202
+ nested_classes = []
203
+ if prism_node.body.is_a?(Prism::StatementsNode)
204
+ prism_node.body.body.each do |stmt|
205
+ node = convert(stmt, class_context)
206
+ if node.is_a?(IR::DefNode)
207
+ methods << node
208
+ elsif node.is_a?(IR::ClassModuleNode)
209
+ # Store nested class/module for separate indexing with proper scope
210
+ nested_classes << node
211
+ end
212
+ end
213
+ end
214
+ # Store nested classes in methods array (RuntimeAdapter handles both types)
215
+ methods.concat(nested_classes)
216
+
217
+ IR::ClassModuleNode.new(name, methods, [], convert_loc(prism_node.constant_path&.location || prism_node.location))
218
+ end
219
+
220
+ private def convert_singleton_class(prism_node, context)
221
+ # Create a new context for singleton class scope
222
+ singleton_context = context.fork(:class)
223
+
224
+ # Generate singleton class name in format: Parent::<Class:ParentName>
225
+ # This matches the scope convention used by RuntimeAdapter and RubyIndexer
226
+ parent_path = context.current_class_name || ""
227
+ parent_name = IR.extract_last_name(parent_path) || "Object"
228
+ singleton_suffix = "<Class:#{parent_name}>"
229
+ singleton_name = parent_path.empty? ? singleton_suffix : "#{parent_path}::#{singleton_suffix}"
230
+ singleton_context.current_class = singleton_name
231
+
232
+ # Collect all method definitions from the body
233
+ methods = []
234
+ if prism_node.body.is_a?(Prism::StatementsNode)
235
+ prism_node.body.body.each do |stmt|
236
+ node = convert(stmt, singleton_context)
237
+ methods << node if node.is_a?(IR::DefNode)
238
+ end
239
+ end
240
+
241
+ IR::ClassModuleNode.new(singleton_name, methods, [], convert_loc(prism_node.location))
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end