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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/lib/ruby_lsp/type_guessr/addon.rb +4 -5
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +17 -0
- data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +3 -3
- data/lib/ruby_lsp/type_guessr/debug_server.rb +2 -2
- data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
- data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
- data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +46 -40
- data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +90 -16
- data/lib/type-guessr.rb +2 -13
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +3 -2
- data/lib/type_guessr/core/cache.rb +5 -0
- data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +2 -2
- data/lib/type_guessr/core/converter/call_converter.rb +161 -0
- data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
- data/lib/type_guessr/core/converter/context.rb +144 -0
- data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
- data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
- data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +9 -1682
- data/lib/type_guessr/core/converter/rbs_converter.rb +15 -1
- data/lib/type_guessr/core/converter/registration.rb +100 -0
- data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
- data/lib/type_guessr/core/converter.rb +4 -0
- data/lib/type_guessr/core/index.rb +3 -0
- data/lib/type_guessr/core/inference/resolver.rb +206 -208
- data/lib/type_guessr/core/inference.rb +4 -0
- data/lib/type_guessr/core/ir.rb +3 -0
- data/lib/type_guessr/core/logger.rb +3 -5
- data/lib/type_guessr/core/registry/method_registry.rb +9 -0
- data/lib/type_guessr/core/registry/signature_registry.rb +73 -16
- data/lib/type_guessr/core/registry.rb +6 -0
- data/lib/type_guessr/core/type_serializer.rb +18 -14
- data/lib/type_guessr/core/type_simplifier.rb +5 -5
- data/lib/type_guessr/core/types.rb +64 -22
- data/lib/type_guessr/core.rb +29 -0
- data/lib/type_guessr/mcp/server.rb +55 -46
- data/lib/type_guessr/mcp/standalone_runtime.rb +70 -110
- data/lib/type_guessr/version.rb +1 -1
- metadata +25 -5
- data/.mcp.json +0 -9
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "context"
|
|
5
|
+
require_relative "literal_converter"
|
|
6
|
+
require_relative "variable_converter"
|
|
7
|
+
require_relative "container_mutation_converter"
|
|
8
|
+
require_relative "call_converter"
|
|
9
|
+
require_relative "control_flow_converter"
|
|
10
|
+
require_relative "definition_converter"
|
|
11
|
+
require_relative "registration"
|
|
12
|
+
require_relative "../ir"
|
|
5
13
|
require_relative "../types"
|
|
6
14
|
|
|
7
15
|
module TypeGuessr
|
|
@@ -10,139 +18,6 @@ module TypeGuessr
|
|
|
10
18
|
# Converts Prism AST to IR graph (reverse dependency graph)
|
|
11
19
|
# Each IR node points to nodes it depends on
|
|
12
20
|
class PrismConverter
|
|
13
|
-
# Context for tracking variable bindings during conversion
|
|
14
|
-
class Context
|
|
15
|
-
attr_reader :variables, :file_path, :location_index, :method_registry, :ivar_registry, :cvar_registry
|
|
16
|
-
attr_accessor :current_class, :current_method, :in_singleton_method
|
|
17
|
-
|
|
18
|
-
def initialize(parent = nil, file_path: nil, location_index: nil,
|
|
19
|
-
method_registry: nil, ivar_registry: nil, cvar_registry: nil)
|
|
20
|
-
@parent = parent
|
|
21
|
-
@variables = {} # name => node
|
|
22
|
-
@instance_variables = {} # @name => node (only for class-level context)
|
|
23
|
-
@narrowed_ivars = {} # @name => narrowed node (method-level, does not pollute class-level)
|
|
24
|
-
@constants = {} # name => dependency node (for constant alias tracking)
|
|
25
|
-
@scope_type = nil # :class, :method, :block, :top_level
|
|
26
|
-
@current_class = nil
|
|
27
|
-
@current_method = nil
|
|
28
|
-
@in_singleton_method = false
|
|
29
|
-
|
|
30
|
-
# Index/registry references (inherited from parent or set directly)
|
|
31
|
-
@file_path = file_path || parent&.file_path
|
|
32
|
-
@location_index = location_index || parent&.location_index
|
|
33
|
-
@method_registry = method_registry || parent&.method_registry
|
|
34
|
-
@ivar_registry = ivar_registry || parent&.ivar_registry
|
|
35
|
-
@cvar_registry = cvar_registry || parent&.cvar_registry
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def register_variable(name, node)
|
|
39
|
-
@variables[name] = node
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def lookup_variable(name)
|
|
43
|
-
@variables[name] || @parent&.lookup_variable(name)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Register an instance variable at the class level
|
|
47
|
-
# Instance variables are shared across all methods in a class
|
|
48
|
-
def register_instance_variable(name, node)
|
|
49
|
-
if @scope_type == :class
|
|
50
|
-
@instance_variables[name] = node
|
|
51
|
-
elsif @parent
|
|
52
|
-
@parent.register_instance_variable(name, node)
|
|
53
|
-
else
|
|
54
|
-
# Top-level instance variable, store locally
|
|
55
|
-
@instance_variables[name] = node
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Narrow an instance variable's type within the current method scope
|
|
60
|
-
# Does not pollute the class-level ivar definition
|
|
61
|
-
def narrow_instance_variable(name, node)
|
|
62
|
-
@narrowed_ivars[name] = node
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Lookup an instance variable, checking narrowed ivars first
|
|
66
|
-
def lookup_instance_variable(name)
|
|
67
|
-
return @narrowed_ivars[name] if @narrowed_ivars.key?(name)
|
|
68
|
-
|
|
69
|
-
if @scope_type == :class
|
|
70
|
-
@instance_variables[name]
|
|
71
|
-
elsif @parent
|
|
72
|
-
@parent.lookup_instance_variable(name)
|
|
73
|
-
else
|
|
74
|
-
@instance_variables[name]
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Register a constant's dependency node for alias tracking
|
|
79
|
-
def register_constant(name, dependency_node)
|
|
80
|
-
@constants[name] = dependency_node
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Lookup a constant's dependency node (for alias resolution)
|
|
84
|
-
def lookup_constant(name)
|
|
85
|
-
@constants[name] || @parent&.lookup_constant(name)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def fork(scope_type)
|
|
89
|
-
child = Context.new(self)
|
|
90
|
-
child.instance_variable_set(:@scope_type, scope_type)
|
|
91
|
-
child.current_class = current_class_name
|
|
92
|
-
child.current_method = current_method_name
|
|
93
|
-
child.in_singleton_method = @in_singleton_method
|
|
94
|
-
child
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def scope_type
|
|
98
|
-
@scope_type || @parent&.scope_type
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Get the current class name (from this context or parent)
|
|
102
|
-
def current_class_name
|
|
103
|
-
@current_class || @parent&.current_class_name
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Get the current method name (from this context or parent)
|
|
107
|
-
def current_method_name
|
|
108
|
-
@current_method || @parent&.current_method_name
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Generate scope_id for node lookup (e.g., "User#save" or "User" or "")
|
|
112
|
-
# For singleton methods, uses "<Class:ClassName>" format to match RubyIndexer convention
|
|
113
|
-
def scope_id
|
|
114
|
-
base_class_path = current_class_name || ""
|
|
115
|
-
class_path = if @in_singleton_method
|
|
116
|
-
# Singleton methods use "<Class:ClassName>" suffix
|
|
117
|
-
parent_name = IR.extract_last_name(base_class_path) || "Object"
|
|
118
|
-
base_class_path.empty? ? "<Class:Object>" : "#{base_class_path}::<Class:#{parent_name}>"
|
|
119
|
-
else
|
|
120
|
-
base_class_path
|
|
121
|
-
end
|
|
122
|
-
method_name = current_method_name
|
|
123
|
-
if method_name
|
|
124
|
-
"#{class_path}##{method_name}"
|
|
125
|
-
else
|
|
126
|
-
class_path
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Check if a variable is defined in this context (not inherited from parent)
|
|
131
|
-
def owns_variable?(name)
|
|
132
|
-
@variables.key?(name)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Register a variable in the parent context (for block mutation propagation)
|
|
136
|
-
def register_variable_in_parent(name, node)
|
|
137
|
-
@parent&.register_variable(name, node)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Get variables that were defined/modified in this context (not from parent)
|
|
141
|
-
def local_variables
|
|
142
|
-
@variables.keys
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
21
|
def initialize
|
|
147
22
|
@literal_type_cache = {}
|
|
148
23
|
end
|
|
@@ -302,1554 +177,6 @@ module TypeGuessr
|
|
|
302
177
|
node
|
|
303
178
|
end
|
|
304
179
|
|
|
305
|
-
private def convert_literal(prism_node)
|
|
306
|
-
type = literal_type_for(prism_node)
|
|
307
|
-
literal_value = extract_literal_value(prism_node)
|
|
308
|
-
IR::LiteralNode.new(type, literal_value, nil, [], convert_loc(prism_node.location))
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
# Extract the actual value from a literal node (for Symbol, Integer, String)
|
|
312
|
-
private def extract_literal_value(prism_node)
|
|
313
|
-
case prism_node
|
|
314
|
-
when Prism::SymbolNode
|
|
315
|
-
prism_node.value.to_sym
|
|
316
|
-
when Prism::IntegerNode
|
|
317
|
-
prism_node.value
|
|
318
|
-
when Prism::StringNode
|
|
319
|
-
prism_node.content
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
private def convert_array_literal(prism_node, context)
|
|
324
|
-
type = array_element_type_for(prism_node)
|
|
325
|
-
|
|
326
|
-
# Convert each element to an IR node
|
|
327
|
-
value_nodes = prism_node.elements.filter_map do |elem|
|
|
328
|
-
next if elem.nil?
|
|
329
|
-
|
|
330
|
-
case elem
|
|
331
|
-
when Prism::SplatNode
|
|
332
|
-
# *arr → convert to CallNode for to_a
|
|
333
|
-
splat_expr = convert(elem.expression, context)
|
|
334
|
-
IR::CallNode.new(:to_a, splat_expr, [], [], nil, false, [], convert_loc(elem.location))
|
|
335
|
-
else
|
|
336
|
-
convert(elem, context)
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
IR::LiteralNode.new(type, nil, value_nodes.empty? ? nil : value_nodes, [], convert_loc(prism_node.location))
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
private def convert_hash_literal(prism_node, context)
|
|
344
|
-
type = hash_element_types_for(prism_node)
|
|
345
|
-
build_hash_literal_node(prism_node, type, context)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# Convert KeywordHashNode (keyword arguments in method calls like `foo(a: 1, b: x)`)
|
|
349
|
-
private def convert_keyword_hash(prism_node, context)
|
|
350
|
-
type = infer_keyword_hash_type(prism_node)
|
|
351
|
-
build_hash_literal_node(prism_node, type, context)
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
# Shared helper for hash-like nodes (HashNode, KeywordHashNode)
|
|
355
|
-
private def build_hash_literal_node(prism_node, type, context)
|
|
356
|
-
value_nodes = prism_node.elements.filter_map do |elem|
|
|
357
|
-
case elem
|
|
358
|
-
when Prism::AssocNode
|
|
359
|
-
convert(elem.value, context)
|
|
360
|
-
when Prism::AssocSplatNode
|
|
361
|
-
convert(elem.value, context)
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
IR::LiteralNode.new(type, nil, value_nodes.empty? ? nil : value_nodes, [], convert_loc(prism_node.location))
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# Infer type for KeywordHashNode (always has symbol keys)
|
|
369
|
-
private def infer_keyword_hash_type(keyword_hash_node)
|
|
370
|
-
return Types::HashShape.new({}) if keyword_hash_node.elements.empty?
|
|
371
|
-
|
|
372
|
-
fields = keyword_hash_node.elements.each_with_object({}) do |elem, hash|
|
|
373
|
-
next unless elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
|
|
374
|
-
|
|
375
|
-
hash[elem.key.value.to_sym] = literal_type_for(elem.value)
|
|
376
|
-
end
|
|
377
|
-
Types::HashShape.new(fields)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
private def literal_type_for(prism_node)
|
|
381
|
-
case prism_node
|
|
382
|
-
when Prism::IntegerNode
|
|
383
|
-
Types::ClassInstance.for("Integer")
|
|
384
|
-
when Prism::FloatNode
|
|
385
|
-
Types::ClassInstance.for("Float")
|
|
386
|
-
when Prism::StringNode, Prism::InterpolatedStringNode
|
|
387
|
-
Types::ClassInstance.for("String")
|
|
388
|
-
when Prism::SymbolNode
|
|
389
|
-
Types::ClassInstance.for("Symbol")
|
|
390
|
-
when Prism::TrueNode
|
|
391
|
-
Types::ClassInstance.for("TrueClass")
|
|
392
|
-
when Prism::FalseNode
|
|
393
|
-
Types::ClassInstance.for("FalseClass")
|
|
394
|
-
when Prism::NilNode
|
|
395
|
-
Types::ClassInstance.for("NilClass")
|
|
396
|
-
when Prism::ArrayNode
|
|
397
|
-
# Infer element type from array contents
|
|
398
|
-
array_element_type_for(prism_node)
|
|
399
|
-
when Prism::HashNode
|
|
400
|
-
hash_element_types_for(prism_node)
|
|
401
|
-
when Prism::RangeNode
|
|
402
|
-
range_element_type_for(prism_node)
|
|
403
|
-
when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
|
|
404
|
-
Types::ClassInstance.for("Regexp")
|
|
405
|
-
when Prism::ImaginaryNode
|
|
406
|
-
Types::ClassInstance.for("Complex")
|
|
407
|
-
when Prism::RationalNode
|
|
408
|
-
Types::ClassInstance.for("Rational")
|
|
409
|
-
when Prism::XStringNode, Prism::InterpolatedXStringNode
|
|
410
|
-
Types::ClassInstance.for("String")
|
|
411
|
-
else
|
|
412
|
-
Types::Unknown.instance
|
|
413
|
-
end
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
private def range_element_type_for(range_node)
|
|
417
|
-
left_type = range_node.left ? literal_type_for(range_node.left) : nil
|
|
418
|
-
right_type = range_node.right ? literal_type_for(range_node.right) : nil
|
|
419
|
-
|
|
420
|
-
types = [left_type, right_type].compact
|
|
421
|
-
|
|
422
|
-
# No bounds at all (shouldn't happen in valid Ruby, but handle gracefully)
|
|
423
|
-
return Types::RangeType.new if types.empty?
|
|
424
|
-
|
|
425
|
-
unique_types = types.uniq
|
|
426
|
-
|
|
427
|
-
element_type = if unique_types.size == 1
|
|
428
|
-
unique_types.first
|
|
429
|
-
else
|
|
430
|
-
Types::Union.new(unique_types)
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
Types::RangeType.new(element_type)
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
private def convert_local_variable_write(prism_node, context)
|
|
437
|
-
value_node = convert(prism_node.value, context)
|
|
438
|
-
write_node = IR::LocalWriteNode.new(prism_node.name, value_node, [], convert_loc(prism_node.location))
|
|
439
|
-
context.register_variable(prism_node.name, write_node)
|
|
440
|
-
write_node
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
private def convert_local_variable_read(prism_node, context)
|
|
444
|
-
# Look up the most recent assignment
|
|
445
|
-
write_node = context.lookup_variable(prism_node.name)
|
|
446
|
-
|
|
447
|
-
IR::LocalReadNode.new(
|
|
448
|
-
prism_node.name,
|
|
449
|
-
write_node,
|
|
450
|
-
# Share called_methods array for method-based inference
|
|
451
|
-
# nil case: rescue binding (=> e), pattern matching binding, etc. (not yet implemented)
|
|
452
|
-
write_node&.called_methods || [],
|
|
453
|
-
convert_loc(prism_node.location)
|
|
454
|
-
)
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
private def convert_instance_variable_write(prism_node, context)
|
|
458
|
-
value_node = convert(prism_node.value, context)
|
|
459
|
-
class_name = context.current_class_name
|
|
460
|
-
|
|
461
|
-
write_node = IR::InstanceVariableWriteNode.new(
|
|
462
|
-
prism_node.name,
|
|
463
|
-
class_name,
|
|
464
|
-
value_node,
|
|
465
|
-
# Share called_methods with value node for type propagation
|
|
466
|
-
# nil case: value is an unhandled node type (convert() returns nil)
|
|
467
|
-
value_node&.called_methods || [],
|
|
468
|
-
convert_loc(prism_node.location)
|
|
469
|
-
)
|
|
470
|
-
# Register at class level so it's visible across methods
|
|
471
|
-
context.register_instance_variable(prism_node.name, write_node)
|
|
472
|
-
write_node
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
private def convert_instance_variable_read(prism_node, context)
|
|
476
|
-
# Look up from class level first
|
|
477
|
-
write_node = context.lookup_instance_variable(prism_node.name)
|
|
478
|
-
|
|
479
|
-
IR::InstanceVariableReadNode.new(
|
|
480
|
-
prism_node.name,
|
|
481
|
-
context.current_class_name,
|
|
482
|
-
write_node,
|
|
483
|
-
# Share called_methods array for method-based inference
|
|
484
|
-
# nil case: instance variable read before any assignment in current file
|
|
485
|
-
write_node&.called_methods || [],
|
|
486
|
-
convert_loc(prism_node.location)
|
|
487
|
-
)
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
private def convert_class_variable_write(prism_node, context)
|
|
491
|
-
value_node = convert(prism_node.value, context)
|
|
492
|
-
|
|
493
|
-
write_node = IR::ClassVariableWriteNode.new(
|
|
494
|
-
prism_node.name,
|
|
495
|
-
context.current_class_name,
|
|
496
|
-
value_node,
|
|
497
|
-
# Share called_methods with value node for type propagation
|
|
498
|
-
# nil case: value is an unhandled node type (e.g., LambdaNode)
|
|
499
|
-
value_node&.called_methods || [],
|
|
500
|
-
convert_loc(prism_node.location)
|
|
501
|
-
)
|
|
502
|
-
context.register_variable(prism_node.name, write_node)
|
|
503
|
-
write_node
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
private def convert_class_variable_read(prism_node, context)
|
|
507
|
-
write_node = context.lookup_variable(prism_node.name)
|
|
508
|
-
|
|
509
|
-
IR::ClassVariableReadNode.new(
|
|
510
|
-
prism_node.name,
|
|
511
|
-
context.current_class_name,
|
|
512
|
-
write_node,
|
|
513
|
-
# Share called_methods array for method-based inference
|
|
514
|
-
# nil case: class variable read before any assignment in current file
|
|
515
|
-
write_node&.called_methods || [],
|
|
516
|
-
convert_loc(prism_node.location)
|
|
517
|
-
)
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
# Compound assignment: x ||= value
|
|
521
|
-
# Result type is union of original and new value type
|
|
522
|
-
private def convert_local_variable_or_write(prism_node, context)
|
|
523
|
-
convert_or_write(prism_node, context, :local)
|
|
524
|
-
end
|
|
525
|
-
|
|
526
|
-
# Compound assignment: x &&= value
|
|
527
|
-
# Result type is union of original and new value type
|
|
528
|
-
private def convert_local_variable_and_write(prism_node, context)
|
|
529
|
-
convert_and_write(prism_node, context, :local)
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
# Compound assignment: x += value, x -= value, etc.
|
|
533
|
-
# Result type depends on the operator method return type
|
|
534
|
-
private def convert_local_variable_operator_write(prism_node, context)
|
|
535
|
-
convert_operator_write(prism_node, context, :local)
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
private def convert_instance_variable_or_write(prism_node, context)
|
|
539
|
-
convert_or_write(prism_node, context, :instance)
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
private def convert_instance_variable_and_write(prism_node, context)
|
|
543
|
-
convert_and_write(prism_node, context, :instance)
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
private def convert_instance_variable_operator_write(prism_node, context)
|
|
547
|
-
convert_operator_write(prism_node, context, :instance)
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# Generic ||= handler
|
|
551
|
-
# x ||= value means: if x is nil/false, x = value, else keep x
|
|
552
|
-
# Uses OrNode to apply truthiness filtering (removes nil/false from LHS)
|
|
553
|
-
private def convert_or_write(prism_node, context, kind)
|
|
554
|
-
original_node = lookup_by_kind(prism_node.name, kind, context)
|
|
555
|
-
value_node = convert(prism_node.value, context)
|
|
556
|
-
|
|
557
|
-
or_node = if original_node
|
|
558
|
-
IR::OrNode.new(
|
|
559
|
-
original_node,
|
|
560
|
-
value_node,
|
|
561
|
-
[],
|
|
562
|
-
convert_loc(prism_node.location)
|
|
563
|
-
)
|
|
564
|
-
else
|
|
565
|
-
value_node
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
write_node = create_write_node(prism_node.name, kind, or_node, context, prism_node.location)
|
|
569
|
-
register_by_kind(prism_node.name, write_node, kind, context)
|
|
570
|
-
write_node
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
# Generic &&= handler
|
|
574
|
-
# x &&= value means: if x is truthy, x = value, else keep x
|
|
575
|
-
# Type is union of original type and value type
|
|
576
|
-
private def convert_and_write(prism_node, context, kind)
|
|
577
|
-
original_node = lookup_by_kind(prism_node.name, kind, context)
|
|
578
|
-
value_node = convert(prism_node.value, context)
|
|
579
|
-
|
|
580
|
-
# Create merge node for union type (original | value)
|
|
581
|
-
branches = []
|
|
582
|
-
branches << original_node if original_node
|
|
583
|
-
branches << value_node
|
|
584
|
-
|
|
585
|
-
merge_node = if branches.size == 1
|
|
586
|
-
branches.first
|
|
587
|
-
else
|
|
588
|
-
IR::MergeNode.new(
|
|
589
|
-
branches,
|
|
590
|
-
[],
|
|
591
|
-
convert_loc(prism_node.location)
|
|
592
|
-
)
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
write_node = create_write_node(prism_node.name, kind, merge_node, context, prism_node.location)
|
|
596
|
-
register_by_kind(prism_node.name, write_node, kind, context)
|
|
597
|
-
write_node
|
|
598
|
-
end
|
|
599
|
-
|
|
600
|
-
# Generic operator write handler (+=, -=, *=, etc.)
|
|
601
|
-
# x += value is equivalent to x = x.+(value)
|
|
602
|
-
# Type is the return type of the operator method
|
|
603
|
-
private def convert_operator_write(prism_node, context, kind)
|
|
604
|
-
original_node = lookup_by_kind(prism_node.name, kind, context)
|
|
605
|
-
value_node = convert(prism_node.value, context)
|
|
606
|
-
|
|
607
|
-
# Create a call node representing x.operator(value)
|
|
608
|
-
call_node = IR::CallNode.new(
|
|
609
|
-
prism_node.binary_operator, original_node, [value_node], [], nil, false, [], convert_loc(prism_node.location)
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
# Create write node with call result as value
|
|
613
|
-
write_node = create_write_node(prism_node.name, kind, call_node, context, prism_node.location)
|
|
614
|
-
register_by_kind(prism_node.name, write_node, kind, context)
|
|
615
|
-
write_node
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
# Helper to create the appropriate write node type based on kind
|
|
619
|
-
private def create_write_node(name, kind, value, context, location)
|
|
620
|
-
loc = convert_loc(location)
|
|
621
|
-
case kind
|
|
622
|
-
when :local
|
|
623
|
-
IR::LocalWriteNode.new(name, value, [], loc)
|
|
624
|
-
when :instance
|
|
625
|
-
IR::InstanceVariableWriteNode.new(name, context.current_class_name, value, [], loc)
|
|
626
|
-
when :class
|
|
627
|
-
IR::ClassVariableWriteNode.new(name, context.current_class_name, value, [], loc)
|
|
628
|
-
end
|
|
629
|
-
end
|
|
630
|
-
|
|
631
|
-
# Helper to lookup variable by kind
|
|
632
|
-
private def lookup_by_kind(name, kind, context)
|
|
633
|
-
case kind
|
|
634
|
-
when :instance
|
|
635
|
-
context.lookup_instance_variable(name)
|
|
636
|
-
else
|
|
637
|
-
context.lookup_variable(name)
|
|
638
|
-
end
|
|
639
|
-
end
|
|
640
|
-
|
|
641
|
-
# Helper to register variable by kind
|
|
642
|
-
private def register_by_kind(name, node, kind, context)
|
|
643
|
-
case kind
|
|
644
|
-
when :instance
|
|
645
|
-
context.register_instance_variable(name, node)
|
|
646
|
-
else
|
|
647
|
-
context.register_variable(name, node)
|
|
648
|
-
end
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
private def convert_call(prism_node, context)
|
|
652
|
-
# Convert receiver - if nil and inside a class, create implicit SelfNode
|
|
653
|
-
receiver_node = if prism_node.receiver
|
|
654
|
-
convert(prism_node.receiver, context)
|
|
655
|
-
elsif context.current_class_name
|
|
656
|
-
IR::SelfNode.new(
|
|
657
|
-
context.current_class_name,
|
|
658
|
-
context.in_singleton_method,
|
|
659
|
-
[],
|
|
660
|
-
convert_loc(prism_node.location)
|
|
661
|
-
)
|
|
662
|
-
end
|
|
663
|
-
|
|
664
|
-
args = prism_node.arguments&.arguments&.map { |arg| convert(arg, context) } || []
|
|
665
|
-
|
|
666
|
-
has_block = !prism_node.block.nil?
|
|
667
|
-
|
|
668
|
-
# Track method call on receiver for method-based type inference
|
|
669
|
-
if variable_node?(receiver_node) && receiver_node.called_methods.none? { |cm| cm.name == prism_node.name }
|
|
670
|
-
receiver_node.called_methods << build_called_method(prism_node)
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
# Handle container mutating methods (Hash#[]=, Array#[]=, Array#<<)
|
|
674
|
-
receiver_node = handle_container_mutation(prism_node, receiver_node, args, context) if container_mutating_method?(prism_node.name, receiver_node)
|
|
675
|
-
|
|
676
|
-
# Use message_loc for method name position to match hover lookup
|
|
677
|
-
call_loc = convert_loc(prism_node.message_loc || prism_node.location)
|
|
678
|
-
call_node = IR::CallNode.new(
|
|
679
|
-
prism_node.name, receiver_node, args, [], nil, has_block, [], call_loc
|
|
680
|
-
)
|
|
681
|
-
|
|
682
|
-
# Handle block if present (but not block arguments like &block)
|
|
683
|
-
if prism_node.block.is_a?(Prism::BlockNode)
|
|
684
|
-
block_body = convert_block(prism_node.block, call_node, context)
|
|
685
|
-
# Update block_body and has_block on mutable Struct
|
|
686
|
-
call_node.block_body = block_body
|
|
687
|
-
call_node.has_block = true
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
call_node
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
# Check if node is any variable node (for method call tracking)
|
|
694
|
-
private def variable_node?(node)
|
|
695
|
-
node.is_a?(IR::LocalWriteNode) ||
|
|
696
|
-
node.is_a?(IR::LocalReadNode) ||
|
|
697
|
-
node.is_a?(IR::InstanceVariableWriteNode) ||
|
|
698
|
-
node.is_a?(IR::InstanceVariableReadNode) ||
|
|
699
|
-
node.is_a?(IR::ClassVariableWriteNode) ||
|
|
700
|
-
node.is_a?(IR::ClassVariableReadNode) ||
|
|
701
|
-
node.is_a?(IR::ParamNode) ||
|
|
702
|
-
node.is_a?(IR::BlockParamSlot)
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
# Register exception variable from rescue clause (=> e)
|
|
706
|
-
# @param rescue_clause [Prism::RescueNode] The rescue clause
|
|
707
|
-
# @param context [Context] Conversion context
|
|
708
|
-
private def register_rescue_variable(rescue_clause, context)
|
|
709
|
-
var_name = rescue_clause.reference.name
|
|
710
|
-
exception_type = infer_rescue_exception_type(rescue_clause.exceptions)
|
|
711
|
-
loc = convert_loc(rescue_clause.reference.location)
|
|
712
|
-
|
|
713
|
-
value_node = IR::LiteralNode.new(exception_type, nil, nil, [], loc)
|
|
714
|
-
|
|
715
|
-
write_node = IR::LocalWriteNode.new(var_name, value_node, [], loc)
|
|
716
|
-
|
|
717
|
-
context.register_variable(var_name, write_node)
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
# Infer exception type from rescue clause's exception list
|
|
721
|
-
# @param exceptions [Array<Prism::Node>] List of exception class nodes
|
|
722
|
-
# @return [Types::ClassInstance, Types::Union] Inferred exception type
|
|
723
|
-
private def infer_rescue_exception_type(exceptions)
|
|
724
|
-
# Default to StandardError if no exception class specified (rescue => e)
|
|
725
|
-
return Types::ClassInstance.new("StandardError") if exceptions.empty?
|
|
726
|
-
|
|
727
|
-
types = exceptions.map do |exc|
|
|
728
|
-
class_name = case exc
|
|
729
|
-
when Prism::ConstantReadNode
|
|
730
|
-
exc.name.to_s
|
|
731
|
-
when Prism::ConstantPathNode
|
|
732
|
-
# Handle namespaced constants like Net::HTTPError
|
|
733
|
-
begin
|
|
734
|
-
exc.full_name
|
|
735
|
-
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
736
|
-
"StandardError"
|
|
737
|
-
end
|
|
738
|
-
else
|
|
739
|
-
"StandardError"
|
|
740
|
-
end
|
|
741
|
-
Types::ClassInstance.new(class_name)
|
|
742
|
-
end
|
|
743
|
-
|
|
744
|
-
types.size == 1 ? types.first : Types::Union.new(types)
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
# Build CalledMethod with signature information from Prism CallNode
|
|
748
|
-
private def build_called_method(prism_node)
|
|
749
|
-
positional_count, has_splat, keywords = extract_call_signature(prism_node)
|
|
750
|
-
|
|
751
|
-
IR::CalledMethod.new(
|
|
752
|
-
name: prism_node.name,
|
|
753
|
-
positional_count: has_splat ? nil : positional_count,
|
|
754
|
-
keywords: keywords
|
|
755
|
-
)
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
# Extract positional count, splat presence, and keywords from call arguments
|
|
759
|
-
# @return [Array(Integer, Boolean, Array<Symbol>)] [positional_count, has_splat, keywords]
|
|
760
|
-
private def extract_call_signature(prism_node)
|
|
761
|
-
arguments = prism_node.arguments&.arguments || []
|
|
762
|
-
positional_count = 0
|
|
763
|
-
has_splat = false
|
|
764
|
-
keywords = []
|
|
765
|
-
|
|
766
|
-
arguments.each do |arg|
|
|
767
|
-
case arg
|
|
768
|
-
when Prism::SplatNode
|
|
769
|
-
has_splat = true
|
|
770
|
-
when Prism::KeywordHashNode
|
|
771
|
-
extract_keywords_from_hash(arg, keywords)
|
|
772
|
-
else
|
|
773
|
-
positional_count += 1
|
|
774
|
-
end
|
|
775
|
-
end
|
|
776
|
-
|
|
777
|
-
[positional_count, has_splat, keywords]
|
|
778
|
-
end
|
|
779
|
-
|
|
780
|
-
# Extract keyword argument names from KeywordHashNode
|
|
781
|
-
private def extract_keywords_from_hash(hash_node, keywords)
|
|
782
|
-
hash_node.elements.each do |element|
|
|
783
|
-
next unless element.is_a?(Prism::AssocNode)
|
|
784
|
-
|
|
785
|
-
key = element.key
|
|
786
|
-
keywords << key.value.to_sym if key.is_a?(Prism::SymbolNode)
|
|
787
|
-
end
|
|
788
|
-
end
|
|
789
|
-
|
|
790
|
-
# Check if node is a local variable node (for indexed assignment)
|
|
791
|
-
private def local_variable_node?(node)
|
|
792
|
-
node.is_a?(IR::LocalWriteNode) || node.is_a?(IR::LocalReadNode)
|
|
793
|
-
end
|
|
794
|
-
|
|
795
|
-
private def extract_literal_type(ir_node)
|
|
796
|
-
case ir_node
|
|
797
|
-
when IR::LiteralNode
|
|
798
|
-
ir_node.type
|
|
799
|
-
else
|
|
800
|
-
Types::Unknown.instance
|
|
801
|
-
end
|
|
802
|
-
end
|
|
803
|
-
|
|
804
|
-
private def widen_to_hash_type(original_type, key_arg, value_type)
|
|
805
|
-
# When mixing key types, widen to generic HashType
|
|
806
|
-
new_key_type = infer_key_type(key_arg)
|
|
807
|
-
|
|
808
|
-
case original_type
|
|
809
|
-
when Types::HashShape
|
|
810
|
-
# HashShape with symbol keys + non-symbol key -> widen to Hash[Symbol | NewKeyType, ValueUnion]
|
|
811
|
-
original_key_type = Types::ClassInstance.for("Symbol")
|
|
812
|
-
original_value_types = original_type.fields.values.uniq
|
|
813
|
-
all_value_types = (original_value_types + [value_type]).uniq
|
|
814
|
-
|
|
815
|
-
combined_key_type = Types::Union.new([original_key_type, new_key_type].uniq)
|
|
816
|
-
combined_value_type = all_value_types.size == 1 ? all_value_types.first : Types::Union.new(all_value_types)
|
|
817
|
-
|
|
818
|
-
Types::HashType.new(combined_key_type, combined_value_type)
|
|
819
|
-
when Types::HashType
|
|
820
|
-
# Already a HashType, just union the key and value types
|
|
821
|
-
combined_key_type = union_types(original_type.key_type, new_key_type)
|
|
822
|
-
combined_value_type = union_types(original_type.value_type, value_type)
|
|
823
|
-
Types::HashType.new(combined_key_type, combined_value_type)
|
|
824
|
-
else
|
|
825
|
-
Types::HashType.new(new_key_type, value_type)
|
|
826
|
-
end
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
private def union_types(type1, type2)
|
|
830
|
-
return type2 if type1.nil? || type1.is_a?(Types::Unknown)
|
|
831
|
-
return type1 if type2.nil? || type2.is_a?(Types::Unknown)
|
|
832
|
-
return type1 if type1 == type2
|
|
833
|
-
|
|
834
|
-
types = []
|
|
835
|
-
types += type1.is_a?(Types::Union) ? type1.types : [type1]
|
|
836
|
-
types += type2.is_a?(Types::Union) ? type2.types : [type2]
|
|
837
|
-
Types::Union.new(types.uniq)
|
|
838
|
-
end
|
|
839
|
-
|
|
840
|
-
private def infer_key_type(key_arg)
|
|
841
|
-
case key_arg
|
|
842
|
-
when Prism::SymbolNode
|
|
843
|
-
Types::ClassInstance.for("Symbol")
|
|
844
|
-
when Prism::StringNode
|
|
845
|
-
Types::ClassInstance.for("String")
|
|
846
|
-
when Prism::IntegerNode
|
|
847
|
-
Types::ClassInstance.for("Integer")
|
|
848
|
-
else
|
|
849
|
-
Types::Unknown.instance
|
|
850
|
-
end
|
|
851
|
-
end
|
|
852
|
-
|
|
853
|
-
# Check if method is a container mutating method
|
|
854
|
-
private def container_mutating_method?(method, receiver_node)
|
|
855
|
-
return false unless local_variable_node?(receiver_node)
|
|
856
|
-
|
|
857
|
-
receiver_type = get_receiver_type(receiver_node)
|
|
858
|
-
return false unless receiver_type
|
|
859
|
-
|
|
860
|
-
case method
|
|
861
|
-
when :[]= then hash_like?(receiver_type) || array_like?(receiver_type)
|
|
862
|
-
when :<< then array_like?(receiver_type)
|
|
863
|
-
else false
|
|
864
|
-
end
|
|
865
|
-
end
|
|
866
|
-
|
|
867
|
-
# Get receiver's current type
|
|
868
|
-
private def get_receiver_type(receiver_node)
|
|
869
|
-
return nil unless receiver_node.respond_to?(:write_node)
|
|
870
|
-
|
|
871
|
-
write_node = receiver_node.write_node
|
|
872
|
-
return nil unless write_node
|
|
873
|
-
return nil unless write_node.respond_to?(:value)
|
|
874
|
-
|
|
875
|
-
value = write_node.value
|
|
876
|
-
return nil unless value.respond_to?(:type)
|
|
877
|
-
|
|
878
|
-
value.type
|
|
879
|
-
end
|
|
880
|
-
|
|
881
|
-
# Check if type is hash-like
|
|
882
|
-
private def hash_like?(type)
|
|
883
|
-
type.is_a?(Types::HashShape) || type.is_a?(Types::HashType)
|
|
884
|
-
end
|
|
885
|
-
|
|
886
|
-
# Check if type is array-like
|
|
887
|
-
private def array_like?(type)
|
|
888
|
-
type.is_a?(Types::ArrayType) || type.is_a?(Types::TupleType)
|
|
889
|
-
end
|
|
890
|
-
|
|
891
|
-
# Handle container mutation by creating new LocalWriteNode with merged type
|
|
892
|
-
private def handle_container_mutation(prism_node, receiver_node, args, context)
|
|
893
|
-
merged_type = compute_merged_type(receiver_node, prism_node.name, args, prism_node)
|
|
894
|
-
return receiver_node unless merged_type
|
|
895
|
-
|
|
896
|
-
# Block scope + outer variable → widen TupleType to ArrayType
|
|
897
|
-
is_outer_var = context.scope_type == :block && !context.owns_variable?(receiver_node.name)
|
|
898
|
-
merged_type = widen_tuple_to_array(merged_type) if is_outer_var
|
|
899
|
-
|
|
900
|
-
# Create new LiteralNode with merged type
|
|
901
|
-
value_node = IR::LiteralNode.new(merged_type, nil, nil, [], receiver_node.loc)
|
|
902
|
-
|
|
903
|
-
# Create new LocalWriteNode with merged type
|
|
904
|
-
new_write = IR::LocalWriteNode.new(
|
|
905
|
-
receiver_node.name, value_node, receiver_node.called_methods, convert_loc(prism_node.location)
|
|
906
|
-
)
|
|
907
|
-
|
|
908
|
-
# Register for next line references
|
|
909
|
-
context.register_variable(receiver_node.name, new_write)
|
|
910
|
-
|
|
911
|
-
# Propagate widened type to parent context (so it's visible after block)
|
|
912
|
-
context.register_variable_in_parent(receiver_node.name, new_write) if is_outer_var
|
|
913
|
-
|
|
914
|
-
# Create new LocalReadNode pointing to new write_node
|
|
915
|
-
new_read = IR::LocalReadNode.new(
|
|
916
|
-
receiver_node.name, new_write, receiver_node.called_methods, receiver_node.loc
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
# Register the newly created nodes in location_index
|
|
920
|
-
if context.location_index
|
|
921
|
-
context.location_index.add(context.file_path, value_node, context.scope_id)
|
|
922
|
-
context.location_index.add(context.file_path, new_write, context.scope_id)
|
|
923
|
-
context.location_index.add(context.file_path, new_read, context.scope_id)
|
|
924
|
-
end
|
|
925
|
-
|
|
926
|
-
new_read
|
|
927
|
-
end
|
|
928
|
-
|
|
929
|
-
# Compute merged type for container mutation
|
|
930
|
-
private def compute_merged_type(receiver_node, method, args, prism_node)
|
|
931
|
-
original_type = get_receiver_type(receiver_node)
|
|
932
|
-
return nil unless original_type
|
|
933
|
-
|
|
934
|
-
case method
|
|
935
|
-
when :[]=
|
|
936
|
-
if hash_like?(original_type)
|
|
937
|
-
compute_hash_assignment_type(original_type, args, prism_node)
|
|
938
|
-
elsif array_like?(original_type)
|
|
939
|
-
compute_array_assignment_type(original_type, args)
|
|
940
|
-
end
|
|
941
|
-
when :<<
|
|
942
|
-
compute_array_append_type(original_type, args) if array_like?(original_type)
|
|
943
|
-
end
|
|
944
|
-
end
|
|
945
|
-
|
|
946
|
-
# Compute Hash type after indexed assignment
|
|
947
|
-
private def compute_hash_assignment_type(original_type, args, prism_node)
|
|
948
|
-
return nil unless args.size == 2
|
|
949
|
-
|
|
950
|
-
key_arg = prism_node.arguments.arguments[0]
|
|
951
|
-
value_type = extract_literal_type(args[1])
|
|
952
|
-
|
|
953
|
-
case original_type
|
|
954
|
-
when Types::HashShape
|
|
955
|
-
if key_arg.is_a?(Prism::SymbolNode)
|
|
956
|
-
# Symbol key → keep HashShape, add field
|
|
957
|
-
key_name = key_arg.value.to_sym
|
|
958
|
-
Types::HashShape.new(original_type.fields.merge(key_name => value_type))
|
|
959
|
-
else
|
|
960
|
-
# Non-symbol key → widen to HashType
|
|
961
|
-
widen_to_hash_type(original_type, key_arg, value_type)
|
|
962
|
-
end
|
|
963
|
-
when Types::HashType
|
|
964
|
-
# Empty hash (Unknown types) + symbol key → becomes HashShape with one field
|
|
965
|
-
if empty_hash_type?(original_type) && key_arg.is_a?(Prism::SymbolNode)
|
|
966
|
-
key_name = key_arg.value.to_sym
|
|
967
|
-
Types::HashShape.new({ key_name => value_type })
|
|
968
|
-
else
|
|
969
|
-
key_type = infer_key_type(key_arg)
|
|
970
|
-
Types::HashType.new(
|
|
971
|
-
union_types(original_type.key_type, key_type),
|
|
972
|
-
union_types(original_type.value_type, value_type)
|
|
973
|
-
)
|
|
974
|
-
end
|
|
975
|
-
end
|
|
976
|
-
end
|
|
977
|
-
|
|
978
|
-
# Check if HashType is empty (has Unknown types)
|
|
979
|
-
private def empty_hash_type?(hash_type)
|
|
980
|
-
(hash_type.key_type.nil? || hash_type.key_type.is_a?(Types::Unknown)) &&
|
|
981
|
-
(hash_type.value_type.nil? || hash_type.value_type.is_a?(Types::Unknown))
|
|
982
|
-
end
|
|
983
|
-
|
|
984
|
-
# Compute Array type after indexed assignment
|
|
985
|
-
private def compute_array_assignment_type(original_type, args)
|
|
986
|
-
return nil unless args.size == 2
|
|
987
|
-
|
|
988
|
-
value_type = extract_literal_type(args[1])
|
|
989
|
-
case original_type
|
|
990
|
-
when Types::TupleType
|
|
991
|
-
Types::TupleType.new(original_type.element_types + [value_type])
|
|
992
|
-
else
|
|
993
|
-
combined = union_types(original_type.element_type, value_type)
|
|
994
|
-
Types::ArrayType.new(combined)
|
|
995
|
-
end
|
|
996
|
-
end
|
|
997
|
-
|
|
998
|
-
# Compute Array type after << operator
|
|
999
|
-
private def compute_array_append_type(original_type, args)
|
|
1000
|
-
return nil unless args.size == 1
|
|
1001
|
-
|
|
1002
|
-
value_type = extract_literal_type(args[0])
|
|
1003
|
-
case original_type
|
|
1004
|
-
when Types::TupleType
|
|
1005
|
-
Types::TupleType.new(original_type.element_types + [value_type])
|
|
1006
|
-
else
|
|
1007
|
-
combined = union_types(original_type.element_type, value_type)
|
|
1008
|
-
Types::ArrayType.new(combined)
|
|
1009
|
-
end
|
|
1010
|
-
end
|
|
1011
|
-
|
|
1012
|
-
# Widen TupleType to ArrayType (for block mutations where position info is meaningless)
|
|
1013
|
-
private def widen_tuple_to_array(type)
|
|
1014
|
-
return type unless type.is_a?(Types::TupleType)
|
|
1015
|
-
|
|
1016
|
-
unique = type.element_types.uniq
|
|
1017
|
-
elem = unique.size == 1 ? unique.first : Types::Union.new(unique)
|
|
1018
|
-
Types::ArrayType.new(elem)
|
|
1019
|
-
end
|
|
1020
|
-
|
|
1021
|
-
# Extract IR param nodes from a Prism parameter node
|
|
1022
|
-
# Handles destructuring (MultiTargetNode) by flattening nested params
|
|
1023
|
-
private def extract_param_nodes(param, kind, context, default_value: nil)
|
|
1024
|
-
case param
|
|
1025
|
-
when Prism::MultiTargetNode
|
|
1026
|
-
# Destructuring parameter like (a, b) - extract all nested params
|
|
1027
|
-
param.lefts.flat_map { |p| extract_param_nodes(p, kind, context) } +
|
|
1028
|
-
param.rights.flat_map { |p| extract_param_nodes(p, kind, context) }
|
|
1029
|
-
when Prism::RequiredParameterNode, Prism::OptionalParameterNode
|
|
1030
|
-
param_node = IR::ParamNode.new(param.name, kind, default_value, [], convert_loc(param.location))
|
|
1031
|
-
context.register_variable(param.name, param_node)
|
|
1032
|
-
[param_node]
|
|
1033
|
-
else
|
|
1034
|
-
[]
|
|
1035
|
-
end
|
|
1036
|
-
end
|
|
1037
|
-
|
|
1038
|
-
private def convert_block(block_node, call_node, context)
|
|
1039
|
-
# Create block parameter slots and register them in context
|
|
1040
|
-
block_context = context.fork(:block)
|
|
1041
|
-
|
|
1042
|
-
if block_node.parameters.is_a?(Prism::BlockParametersNode)
|
|
1043
|
-
parameters_node = block_node.parameters.parameters
|
|
1044
|
-
if parameters_node
|
|
1045
|
-
# Collect all parameters in order
|
|
1046
|
-
params = []
|
|
1047
|
-
params.concat(parameters_node.requireds) if parameters_node.requireds
|
|
1048
|
-
params.concat(parameters_node.optionals) if parameters_node.optionals
|
|
1049
|
-
|
|
1050
|
-
params.each_with_index do |param, index|
|
|
1051
|
-
param_name, param_loc = case param
|
|
1052
|
-
when Prism::RequiredParameterNode
|
|
1053
|
-
[param.name, param.location]
|
|
1054
|
-
when Prism::OptionalParameterNode
|
|
1055
|
-
[param.name, param.location]
|
|
1056
|
-
when Prism::MultiTargetNode
|
|
1057
|
-
# Destructuring parameters like |a, (b, c)|
|
|
1058
|
-
# For now, skip complex cases
|
|
1059
|
-
next
|
|
1060
|
-
else
|
|
1061
|
-
next
|
|
1062
|
-
end
|
|
1063
|
-
|
|
1064
|
-
slot = IR::BlockParamSlot.new(index, call_node, [], convert_loc(param_loc))
|
|
1065
|
-
call_node.block_params << slot
|
|
1066
|
-
block_context.register_variable(param_name, slot)
|
|
1067
|
-
end
|
|
1068
|
-
end
|
|
1069
|
-
end
|
|
1070
|
-
|
|
1071
|
-
# Convert block body and return it for block return type inference
|
|
1072
|
-
block_node.body ? convert(block_node.body, block_context) : nil
|
|
1073
|
-
end
|
|
1074
|
-
|
|
1075
|
-
private def convert_if(prism_node, context)
|
|
1076
|
-
# Convert then branch
|
|
1077
|
-
then_context = context.fork(:then)
|
|
1078
|
-
then_node = convert(prism_node.statements, then_context) if prism_node.statements
|
|
1079
|
-
|
|
1080
|
-
# Convert else branch (could be IfNode, ElseNode, or nil)
|
|
1081
|
-
else_context = context.fork(:else)
|
|
1082
|
-
else_node = if prism_node.subsequent
|
|
1083
|
-
case prism_node.subsequent
|
|
1084
|
-
when Prism::IfNode
|
|
1085
|
-
convert_if(prism_node.subsequent, else_context)
|
|
1086
|
-
when Prism::ElseNode
|
|
1087
|
-
convert(prism_node.subsequent.statements, else_context)
|
|
1088
|
-
end
|
|
1089
|
-
end
|
|
1090
|
-
|
|
1091
|
-
# Create merge nodes for variables modified in branches
|
|
1092
|
-
merge_modified_variables(context, then_context, else_context, then_node, else_node, prism_node.location)
|
|
1093
|
-
end
|
|
1094
|
-
|
|
1095
|
-
private def convert_unless(prism_node, context)
|
|
1096
|
-
# Unless is like if with inverted condition
|
|
1097
|
-
# We treat the unless body as the "else" branch and the consequent as "then"
|
|
1098
|
-
|
|
1099
|
-
unless_context = context.fork(:unless)
|
|
1100
|
-
unless_node = convert(prism_node.statements, unless_context) if prism_node.statements
|
|
1101
|
-
|
|
1102
|
-
else_context = context.fork(:else)
|
|
1103
|
-
else_node = (convert(prism_node.else_clause.statements, else_context) if prism_node.else_clause)
|
|
1104
|
-
|
|
1105
|
-
result = merge_modified_variables(context, unless_context, else_context, unless_node, else_node, prism_node.location)
|
|
1106
|
-
|
|
1107
|
-
# Guard clause narrowing: `return/raise unless x` → x is truthy after
|
|
1108
|
-
narrow_guard_variable(prism_node.predicate, :truthy, context, prism_node.location) if guard_clause?(unless_node)
|
|
1109
|
-
|
|
1110
|
-
result
|
|
1111
|
-
end
|
|
1112
|
-
|
|
1113
|
-
private def convert_case(prism_node, context)
|
|
1114
|
-
branches = []
|
|
1115
|
-
branch_contexts = []
|
|
1116
|
-
|
|
1117
|
-
# Convert each when clause
|
|
1118
|
-
prism_node.conditions&.each do |when_node|
|
|
1119
|
-
when_context = context.fork(:when)
|
|
1120
|
-
if when_node.statements
|
|
1121
|
-
when_result = convert(when_node.statements, when_context)
|
|
1122
|
-
# Skip non-returning branches (raise, fail, etc.)
|
|
1123
|
-
unless non_returning?(when_result)
|
|
1124
|
-
branches << (when_result || create_nil_literal(prism_node.location))
|
|
1125
|
-
branch_contexts << when_context
|
|
1126
|
-
end
|
|
1127
|
-
else
|
|
1128
|
-
# Empty when clause → nil
|
|
1129
|
-
branches << create_nil_literal(prism_node.location)
|
|
1130
|
-
branch_contexts << when_context
|
|
1131
|
-
end
|
|
1132
|
-
end
|
|
1133
|
-
|
|
1134
|
-
# Convert else clause
|
|
1135
|
-
if prism_node.else_clause
|
|
1136
|
-
else_context = context.fork(:else)
|
|
1137
|
-
else_result = convert(prism_node.else_clause.statements, else_context)
|
|
1138
|
-
# Skip non-returning else clause (raise, fail, etc.)
|
|
1139
|
-
unless non_returning?(else_result)
|
|
1140
|
-
branches << (else_result || create_nil_literal(prism_node.location))
|
|
1141
|
-
branch_contexts << else_context
|
|
1142
|
-
end
|
|
1143
|
-
else
|
|
1144
|
-
# If no else clause, nil is possible
|
|
1145
|
-
branches << create_nil_literal(prism_node.location)
|
|
1146
|
-
end
|
|
1147
|
-
|
|
1148
|
-
# Merge modified variables across all branches
|
|
1149
|
-
merge_case_variables(context, branch_contexts, branches, prism_node.location)
|
|
1150
|
-
end
|
|
1151
|
-
|
|
1152
|
-
private def convert_case_match(prism_node, context)
|
|
1153
|
-
# Pattern matching case (Ruby 3.0+)
|
|
1154
|
-
# For now, treat it similarly to regular case
|
|
1155
|
-
convert_case(prism_node, context)
|
|
1156
|
-
end
|
|
1157
|
-
|
|
1158
|
-
private def convert_statements(prism_node, context)
|
|
1159
|
-
last_node = nil
|
|
1160
|
-
prism_node.body.each do |stmt|
|
|
1161
|
-
last_node = convert(stmt, context)
|
|
1162
|
-
end
|
|
1163
|
-
last_node
|
|
1164
|
-
end
|
|
1165
|
-
|
|
1166
|
-
# Helper to convert an array of statement bodies
|
|
1167
|
-
# @param body [Array<Prism::Node>, nil] Array of statement nodes
|
|
1168
|
-
# @param context [Context] Conversion context
|
|
1169
|
-
# @return [Array<IR::Node>] Array of converted IR nodes
|
|
1170
|
-
private def convert_statements_body(body, context)
|
|
1171
|
-
return [] unless body
|
|
1172
|
-
|
|
1173
|
-
nodes = []
|
|
1174
|
-
body.each do |stmt|
|
|
1175
|
-
node = convert(stmt, context)
|
|
1176
|
-
nodes << node if node
|
|
1177
|
-
end
|
|
1178
|
-
nodes
|
|
1179
|
-
end
|
|
1180
|
-
|
|
1181
|
-
# Convert begin/rescue/ensure block
|
|
1182
|
-
private def convert_begin(prism_node, context)
|
|
1183
|
-
body_nodes = extract_begin_body_nodes(prism_node, context)
|
|
1184
|
-
# Return the last node (represents the value of the begin block)
|
|
1185
|
-
body_nodes.last
|
|
1186
|
-
end
|
|
1187
|
-
|
|
1188
|
-
# Convert || (or) operator to OrNode
|
|
1189
|
-
# a || b → LHS evaluated first, RHS only if LHS is falsy
|
|
1190
|
-
private def convert_or_node(prism_node, context)
|
|
1191
|
-
left_node = convert(prism_node.left, context)
|
|
1192
|
-
right_node = convert(prism_node.right, context)
|
|
1193
|
-
|
|
1194
|
-
return nil if left_node.nil? && right_node.nil?
|
|
1195
|
-
return left_node if right_node.nil?
|
|
1196
|
-
return right_node if left_node.nil?
|
|
1197
|
-
|
|
1198
|
-
IR::OrNode.new(left_node, right_node, [], convert_loc(prism_node.location))
|
|
1199
|
-
end
|
|
1200
|
-
|
|
1201
|
-
# Convert && (and) operator to MergeNode
|
|
1202
|
-
# a && b → result is either a or b (short-circuit evaluation)
|
|
1203
|
-
private def convert_and_node(prism_node, context)
|
|
1204
|
-
left_node = convert(prism_node.left, context)
|
|
1205
|
-
right_node = convert(prism_node.right, context)
|
|
1206
|
-
|
|
1207
|
-
branches = [left_node, right_node].compact
|
|
1208
|
-
return nil if branches.empty?
|
|
1209
|
-
return branches.first if branches.size == 1
|
|
1210
|
-
|
|
1211
|
-
IR::MergeNode.new(branches, [], convert_loc(prism_node.location))
|
|
1212
|
-
end
|
|
1213
|
-
|
|
1214
|
-
# Convert h[:key] ||= value → OrNode(h.[](:key), value)
|
|
1215
|
-
private def convert_index_or_write(prism_node, context)
|
|
1216
|
-
receiver_node = convert(prism_node.receiver, context)
|
|
1217
|
-
args = prism_node.arguments&.arguments&.map { |arg| convert(arg, context) } || []
|
|
1218
|
-
value_node = convert(prism_node.value, context)
|
|
1219
|
-
|
|
1220
|
-
read_call = IR::CallNode.new(:[], receiver_node, args, [], nil, false, [], convert_loc(prism_node.opening_loc))
|
|
1221
|
-
|
|
1222
|
-
IR::OrNode.new(read_call, value_node, [], convert_loc(prism_node.location))
|
|
1223
|
-
end
|
|
1224
|
-
|
|
1225
|
-
# Convert multiple assignment (a, b, c = expr)
|
|
1226
|
-
# Creates synthetic value[index] calls for each target variable
|
|
1227
|
-
private def convert_multi_write(prism_node, context)
|
|
1228
|
-
value_node = convert(prism_node.value, context)
|
|
1229
|
-
|
|
1230
|
-
# lefts: variables before splat → value[0], value[1], ...
|
|
1231
|
-
prism_node.lefts.each_with_index do |target, index|
|
|
1232
|
-
assign_multi_write_target(target, value_node, index, context)
|
|
1233
|
-
end
|
|
1234
|
-
|
|
1235
|
-
# rest: splat variable → ArrayType(Unknown)
|
|
1236
|
-
if prism_node.rest.is_a?(Prism::SplatNode) && prism_node.rest.expression
|
|
1237
|
-
splat_target = prism_node.rest.expression
|
|
1238
|
-
splat_value = IR::LiteralNode.new(
|
|
1239
|
-
Types::ArrayType.new, nil, nil, [], convert_loc(splat_target.location)
|
|
1240
|
-
)
|
|
1241
|
-
register_multi_write_variable(splat_target, splat_value, context)
|
|
1242
|
-
end
|
|
1243
|
-
|
|
1244
|
-
# rights: variables after splat → value[-n], value[-(n-1)], ...
|
|
1245
|
-
prism_node.rights.each_with_index do |target, index|
|
|
1246
|
-
negative_index = -(prism_node.rights.size - index)
|
|
1247
|
-
assign_multi_write_target(target, value_node, negative_index, context)
|
|
1248
|
-
end
|
|
1249
|
-
|
|
1250
|
-
value_node
|
|
1251
|
-
end
|
|
1252
|
-
|
|
1253
|
-
# Create synthetic value[index] call and register the target variable
|
|
1254
|
-
private def assign_multi_write_target(target, value_node, index, context)
|
|
1255
|
-
loc = convert_loc(target.location)
|
|
1256
|
-
index_literal = IR::LiteralNode.new(
|
|
1257
|
-
Types::ClassInstance.for("Integer"), index, nil, [], loc
|
|
1258
|
-
)
|
|
1259
|
-
call_node = IR::CallNode.new(:[], value_node, [index_literal], [], nil, false, [], loc)
|
|
1260
|
-
register_multi_write_variable(target, call_node, context)
|
|
1261
|
-
end
|
|
1262
|
-
|
|
1263
|
-
# Register a multi-write target variable (local or instance variable)
|
|
1264
|
-
private def register_multi_write_variable(target, value_node, context)
|
|
1265
|
-
loc = convert_loc(target.location)
|
|
1266
|
-
case target
|
|
1267
|
-
when Prism::LocalVariableTargetNode
|
|
1268
|
-
write_node = IR::LocalWriteNode.new(target.name, value_node, [], loc)
|
|
1269
|
-
context.register_variable(target.name, write_node)
|
|
1270
|
-
when Prism::InstanceVariableTargetNode
|
|
1271
|
-
write_node = IR::InstanceVariableWriteNode.new(
|
|
1272
|
-
target.name, context.current_class_name, value_node, [], loc
|
|
1273
|
-
)
|
|
1274
|
-
context.register_instance_variable(target.name, write_node)
|
|
1275
|
-
end
|
|
1276
|
-
end
|
|
1277
|
-
|
|
1278
|
-
# Extract all body nodes from a BeginNode (for DefNode bodies with rescue/ensure)
|
|
1279
|
-
# @param begin_node [Prism::BeginNode] The begin node
|
|
1280
|
-
# @param context [Context] Conversion context
|
|
1281
|
-
# @return [Array<IR::Node>] Array of all body nodes
|
|
1282
|
-
private def extract_begin_body_nodes(begin_node, context)
|
|
1283
|
-
body_nodes = []
|
|
1284
|
-
|
|
1285
|
-
# Convert main body statements
|
|
1286
|
-
body_nodes.concat(convert_statements_body(begin_node.statements.body, context)) if begin_node.statements
|
|
1287
|
-
|
|
1288
|
-
# Convert rescue clause(s)
|
|
1289
|
-
rescue_clause = begin_node.rescue_clause
|
|
1290
|
-
while rescue_clause
|
|
1291
|
-
# Register exception variable (=> e) if present
|
|
1292
|
-
register_rescue_variable(rescue_clause, context) if rescue_clause.reference.is_a?(Prism::LocalVariableTargetNode)
|
|
1293
|
-
|
|
1294
|
-
rescue_nodes = convert_statements_body(rescue_clause.statements&.body, context)
|
|
1295
|
-
body_nodes.concat(rescue_nodes)
|
|
1296
|
-
rescue_clause = rescue_clause.subsequent
|
|
1297
|
-
end
|
|
1298
|
-
|
|
1299
|
-
# Convert else clause
|
|
1300
|
-
if begin_node.else_clause
|
|
1301
|
-
else_nodes = convert_statements_body(begin_node.else_clause.statements&.body, context)
|
|
1302
|
-
body_nodes.concat(else_nodes)
|
|
1303
|
-
end
|
|
1304
|
-
|
|
1305
|
-
# Convert ensure clause
|
|
1306
|
-
if begin_node.ensure_clause
|
|
1307
|
-
ensure_nodes = convert_statements_body(begin_node.ensure_clause.statements&.body, context)
|
|
1308
|
-
body_nodes.concat(ensure_nodes)
|
|
1309
|
-
end
|
|
1310
|
-
|
|
1311
|
-
body_nodes
|
|
1312
|
-
end
|
|
1313
|
-
|
|
1314
|
-
private def convert_def(prism_node, context, module_function: false)
|
|
1315
|
-
def_context = context.fork(:method)
|
|
1316
|
-
def_context.current_method = prism_node.name.to_s
|
|
1317
|
-
def_context.in_singleton_method = prism_node.receiver.is_a?(Prism::SelfNode)
|
|
1318
|
-
|
|
1319
|
-
# Convert parameters
|
|
1320
|
-
params = []
|
|
1321
|
-
if prism_node.parameters
|
|
1322
|
-
parameters_node = prism_node.parameters
|
|
1323
|
-
|
|
1324
|
-
# Required parameters
|
|
1325
|
-
parameters_node.requireds&.each do |param|
|
|
1326
|
-
extract_param_nodes(param, :required, def_context).each do |param_node|
|
|
1327
|
-
params << param_node
|
|
1328
|
-
end
|
|
1329
|
-
end
|
|
1330
|
-
|
|
1331
|
-
# Optional parameters
|
|
1332
|
-
parameters_node.optionals&.each do |param|
|
|
1333
|
-
default_node = convert(param.value, def_context)
|
|
1334
|
-
param_node = IR::ParamNode.new(param.name, :optional, default_node, [], convert_loc(param.location))
|
|
1335
|
-
params << param_node
|
|
1336
|
-
def_context.register_variable(param.name, param_node)
|
|
1337
|
-
end
|
|
1338
|
-
|
|
1339
|
-
# Rest parameter (*args)
|
|
1340
|
-
if parameters_node.rest.is_a?(Prism::RestParameterNode)
|
|
1341
|
-
rest = parameters_node.rest
|
|
1342
|
-
param_node = IR::ParamNode.new(rest.name || :*, :rest, nil, [], convert_loc(rest.location))
|
|
1343
|
-
params << param_node
|
|
1344
|
-
def_context.register_variable(rest.name, param_node) if rest.name
|
|
1345
|
-
end
|
|
1346
|
-
|
|
1347
|
-
# Required keyword parameters (name:)
|
|
1348
|
-
parameters_node.keywords&.each do |kw|
|
|
1349
|
-
case kw
|
|
1350
|
-
when Prism::RequiredKeywordParameterNode
|
|
1351
|
-
param_node = IR::ParamNode.new(kw.name, :keyword_required, nil, [], convert_loc(kw.location))
|
|
1352
|
-
params << param_node
|
|
1353
|
-
def_context.register_variable(kw.name, param_node)
|
|
1354
|
-
when Prism::OptionalKeywordParameterNode
|
|
1355
|
-
default_node = convert(kw.value, def_context)
|
|
1356
|
-
param_node = IR::ParamNode.new(kw.name, :keyword_optional, default_node, [], convert_loc(kw.location))
|
|
1357
|
-
params << param_node
|
|
1358
|
-
def_context.register_variable(kw.name, param_node)
|
|
1359
|
-
end
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
|
-
# Keyword rest parameter (**kwargs)
|
|
1363
|
-
if parameters_node.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
|
|
1364
|
-
kwrest = parameters_node.keyword_rest
|
|
1365
|
-
param_node = IR::ParamNode.new(kwrest.name || :**, :keyword_rest, nil, [], convert_loc(kwrest.location))
|
|
1366
|
-
params << param_node
|
|
1367
|
-
def_context.register_variable(kwrest.name, param_node) if kwrest.name
|
|
1368
|
-
elsif parameters_node.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
1369
|
-
# Forwarding parameter (...)
|
|
1370
|
-
fwd = parameters_node.keyword_rest
|
|
1371
|
-
param_node = IR::ParamNode.new(:"...", :forwarding, nil, [], convert_loc(fwd.location))
|
|
1372
|
-
params << param_node
|
|
1373
|
-
end
|
|
1374
|
-
|
|
1375
|
-
# Block parameter (&block)
|
|
1376
|
-
if parameters_node.block
|
|
1377
|
-
block = parameters_node.block
|
|
1378
|
-
param_node = IR::ParamNode.new(block.name || :&, :block, nil, [], convert_loc(block.location))
|
|
1379
|
-
params << param_node
|
|
1380
|
-
def_context.register_variable(block.name, param_node) if block.name
|
|
1381
|
-
end
|
|
1382
|
-
end
|
|
1383
|
-
|
|
1384
|
-
# Convert method body - collect all body nodes
|
|
1385
|
-
body_nodes = []
|
|
1386
|
-
|
|
1387
|
-
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
1388
|
-
prism_node.body.body.each do |stmt|
|
|
1389
|
-
node = convert(stmt, def_context)
|
|
1390
|
-
body_nodes << node if node
|
|
1391
|
-
end
|
|
1392
|
-
elsif prism_node.body.is_a?(Prism::BeginNode)
|
|
1393
|
-
# Method with rescue/ensure block
|
|
1394
|
-
begin_node = prism_node.body
|
|
1395
|
-
body_nodes = extract_begin_body_nodes(begin_node, def_context)
|
|
1396
|
-
elsif prism_node.body
|
|
1397
|
-
node = convert(prism_node.body, def_context)
|
|
1398
|
-
body_nodes << node if node
|
|
1399
|
-
end
|
|
1400
|
-
|
|
1401
|
-
# Collect all return points: explicit returns + implicit last expression
|
|
1402
|
-
return_node = compute_return_node(body_nodes, prism_node.name_loc)
|
|
1403
|
-
|
|
1404
|
-
IR::DefNode.new(
|
|
1405
|
-
prism_node.name,
|
|
1406
|
-
def_context.current_class_name,
|
|
1407
|
-
params,
|
|
1408
|
-
return_node,
|
|
1409
|
-
body_nodes,
|
|
1410
|
-
[],
|
|
1411
|
-
convert_loc(prism_node.name_loc),
|
|
1412
|
-
prism_node.receiver.is_a?(Prism::SelfNode),
|
|
1413
|
-
module_function: module_function
|
|
1414
|
-
)
|
|
1415
|
-
end
|
|
1416
|
-
|
|
1417
|
-
# Compute the return node for a method by collecting all return points
|
|
1418
|
-
# @param body_nodes [Array<IR::Node>] All nodes in the method body
|
|
1419
|
-
# @param loc [Prism::Location] Location for the MergeNode if needed
|
|
1420
|
-
# @return [IR::Node, nil] The return node (MergeNode if multiple returns)
|
|
1421
|
-
private def compute_return_node(body_nodes, loc)
|
|
1422
|
-
return nil if body_nodes.empty?
|
|
1423
|
-
|
|
1424
|
-
# Collect all explicit returns from the body
|
|
1425
|
-
explicit_returns = collect_returns(body_nodes)
|
|
1426
|
-
|
|
1427
|
-
# The implicit return is the last non-ReturnNode in body
|
|
1428
|
-
implicit_return = body_nodes.reject { |n| n.is_a?(IR::ReturnNode) }.last
|
|
1429
|
-
|
|
1430
|
-
# Determine all return points
|
|
1431
|
-
return_points = explicit_returns.dup
|
|
1432
|
-
return_points << implicit_return if implicit_return && !last_node_returns?(body_nodes)
|
|
1433
|
-
|
|
1434
|
-
case return_points.size
|
|
1435
|
-
when 0
|
|
1436
|
-
nil
|
|
1437
|
-
when 1
|
|
1438
|
-
return_points.first
|
|
1439
|
-
else
|
|
1440
|
-
IR::MergeNode.new(return_points, [], convert_loc(loc))
|
|
1441
|
-
end
|
|
1442
|
-
end
|
|
1443
|
-
|
|
1444
|
-
# Collect all ReturnNode instances from body nodes (recursive)
|
|
1445
|
-
# Searches inside MergeNode branches to find nested returns from if/case
|
|
1446
|
-
# @param nodes [Array<IR::Node>] Nodes to search
|
|
1447
|
-
# @return [Array<IR::ReturnNode>] All explicit return nodes
|
|
1448
|
-
private def collect_returns(nodes)
|
|
1449
|
-
returns = []
|
|
1450
|
-
nodes.each do |node|
|
|
1451
|
-
case node
|
|
1452
|
-
when IR::ReturnNode
|
|
1453
|
-
returns << node
|
|
1454
|
-
when IR::MergeNode
|
|
1455
|
-
returns.concat(collect_returns(node.branches))
|
|
1456
|
-
when IR::OrNode
|
|
1457
|
-
returns.concat(collect_returns([node.lhs, node.rhs]))
|
|
1458
|
-
end
|
|
1459
|
-
end
|
|
1460
|
-
returns
|
|
1461
|
-
end
|
|
1462
|
-
|
|
1463
|
-
# Check if the last node in body is a ReturnNode
|
|
1464
|
-
# @param body_nodes [Array<IR::Node>] Body nodes
|
|
1465
|
-
# @return [Boolean]
|
|
1466
|
-
private def last_node_returns?(body_nodes)
|
|
1467
|
-
body_nodes.last.is_a?(IR::ReturnNode)
|
|
1468
|
-
end
|
|
1469
|
-
|
|
1470
|
-
private def convert_constant_read(prism_node, context)
|
|
1471
|
-
name = case prism_node
|
|
1472
|
-
when Prism::ConstantReadNode
|
|
1473
|
-
prism_node.name.to_s
|
|
1474
|
-
when Prism::ConstantPathNode
|
|
1475
|
-
prism_node.slice
|
|
1476
|
-
else
|
|
1477
|
-
prism_node.to_s
|
|
1478
|
-
end
|
|
1479
|
-
|
|
1480
|
-
IR::ConstantNode.new(name, context.lookup_constant(name), [], convert_loc(prism_node.location))
|
|
1481
|
-
end
|
|
1482
|
-
|
|
1483
|
-
private def convert_constant_write(prism_node, context)
|
|
1484
|
-
value_node = convert(prism_node.value, context)
|
|
1485
|
-
context.register_constant(prism_node.name.to_s, value_node)
|
|
1486
|
-
IR::ConstantNode.new(prism_node.name.to_s, value_node, [], convert_loc(prism_node.location))
|
|
1487
|
-
end
|
|
1488
|
-
|
|
1489
|
-
private def merge_modified_variables(parent_context, then_context, else_context, then_node, else_node, location)
|
|
1490
|
-
# Skip non-returning branches (raise, fail, etc.)
|
|
1491
|
-
then_node = nil if non_returning?(then_node)
|
|
1492
|
-
else_node = nil if non_returning?(else_node)
|
|
1493
|
-
|
|
1494
|
-
# Track which variables were modified in each branch
|
|
1495
|
-
then_vars = then_context&.local_variables || []
|
|
1496
|
-
else_vars = else_context&.local_variables || []
|
|
1497
|
-
|
|
1498
|
-
# All variables modified in either branch
|
|
1499
|
-
modified_vars = (then_vars + else_vars).uniq
|
|
1500
|
-
|
|
1501
|
-
# Create MergeNode for each modified variable
|
|
1502
|
-
modified_vars.each do |var_name|
|
|
1503
|
-
then_val = then_context&.variables&.[](var_name)
|
|
1504
|
-
else_val = else_context&.variables&.[](var_name)
|
|
1505
|
-
|
|
1506
|
-
# Get the original value from parent context (before if statement)
|
|
1507
|
-
original_val = parent_context.lookup_variable(var_name)
|
|
1508
|
-
|
|
1509
|
-
# Determine branches for merge
|
|
1510
|
-
branches = []
|
|
1511
|
-
if then_val
|
|
1512
|
-
branches << then_val
|
|
1513
|
-
elsif original_val
|
|
1514
|
-
# Variable not modified in then branch, use original
|
|
1515
|
-
branches << original_val
|
|
1516
|
-
end
|
|
1517
|
-
|
|
1518
|
-
if else_val
|
|
1519
|
-
branches << else_val
|
|
1520
|
-
elsif original_val
|
|
1521
|
-
# Variable not modified in else branch, use original
|
|
1522
|
-
branches << original_val
|
|
1523
|
-
elsif then_val
|
|
1524
|
-
# Inline if/unless: no else branch and no original value
|
|
1525
|
-
# Add nil to represent "variable may not be assigned"
|
|
1526
|
-
nil_node = IR::LiteralNode.new(
|
|
1527
|
-
Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location)
|
|
1528
|
-
)
|
|
1529
|
-
branches << nil_node
|
|
1530
|
-
end
|
|
1531
|
-
|
|
1532
|
-
# Create MergeNode only if we have multiple branches
|
|
1533
|
-
if branches.size > 1
|
|
1534
|
-
merge_node = IR::MergeNode.new(branches.uniq, [], convert_loc(location))
|
|
1535
|
-
parent_context.register_variable(var_name, merge_node)
|
|
1536
|
-
elsif branches.size == 1
|
|
1537
|
-
# Only one branch has a value, use it directly
|
|
1538
|
-
parent_context.register_variable(var_name, branches.first)
|
|
1539
|
-
end
|
|
1540
|
-
end
|
|
1541
|
-
|
|
1542
|
-
# Return MergeNode for the if expression value
|
|
1543
|
-
if then_node && else_node
|
|
1544
|
-
IR::MergeNode.new([then_node, else_node].compact, [], convert_loc(location))
|
|
1545
|
-
elsif then_node || else_node
|
|
1546
|
-
# Modifier form: one branch only → value or nil
|
|
1547
|
-
branch_node = then_node || else_node
|
|
1548
|
-
nil_node = IR::LiteralNode.new(
|
|
1549
|
-
Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location)
|
|
1550
|
-
)
|
|
1551
|
-
IR::MergeNode.new([branch_node, nil_node], [], convert_loc(location))
|
|
1552
|
-
end
|
|
1553
|
-
end
|
|
1554
|
-
|
|
1555
|
-
private def merge_case_variables(parent_context, branch_contexts, branches, location)
|
|
1556
|
-
# Collect all variables modified in any branch
|
|
1557
|
-
all_modified_vars = branch_contexts.flat_map { |ctx| ctx&.local_variables || [] }.uniq
|
|
1558
|
-
|
|
1559
|
-
# Create MergeNode for each modified variable
|
|
1560
|
-
all_modified_vars.each do |var_name|
|
|
1561
|
-
# Collect values from all branches
|
|
1562
|
-
branch_contexts.map { |ctx| ctx&.variables&.[](var_name) }
|
|
1563
|
-
|
|
1564
|
-
# Get original value from parent context
|
|
1565
|
-
original_val = parent_context.lookup_variable(var_name)
|
|
1566
|
-
|
|
1567
|
-
# Build branches array
|
|
1568
|
-
merge_branches = branch_contexts.map.with_index do |ctx, _idx|
|
|
1569
|
-
ctx&.variables&.[](var_name) || original_val
|
|
1570
|
-
end.compact.uniq
|
|
1571
|
-
|
|
1572
|
-
# Create MergeNode if we have multiple different values
|
|
1573
|
-
if merge_branches.size > 1
|
|
1574
|
-
merge_node = IR::MergeNode.new(merge_branches, [], convert_loc(location))
|
|
1575
|
-
parent_context.register_variable(var_name, merge_node)
|
|
1576
|
-
elsif merge_branches.size == 1
|
|
1577
|
-
parent_context.register_variable(var_name, merge_branches.first)
|
|
1578
|
-
end
|
|
1579
|
-
end
|
|
1580
|
-
|
|
1581
|
-
# Return MergeNode for the case expression value
|
|
1582
|
-
if branches.size > 1
|
|
1583
|
-
IR::MergeNode.new(branches.compact.uniq, [], convert_loc(location))
|
|
1584
|
-
elsif branches.size == 1
|
|
1585
|
-
branches.first
|
|
1586
|
-
end
|
|
1587
|
-
end
|
|
1588
|
-
|
|
1589
|
-
private def create_nil_literal(location)
|
|
1590
|
-
IR::LiteralNode.new(Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location))
|
|
1591
|
-
end
|
|
1592
|
-
|
|
1593
|
-
# Check if a node represents a non-returning expression (raise, fail, exit, abort)
|
|
1594
|
-
# These should be excluded from branch type inference
|
|
1595
|
-
private def non_returning?(node)
|
|
1596
|
-
return false unless node.is_a?(IR::CallNode)
|
|
1597
|
-
|
|
1598
|
-
node.receiver.nil? && %i[raise fail exit abort].include?(node.method)
|
|
1599
|
-
end
|
|
1600
|
-
|
|
1601
|
-
# Check if a node represents a guard clause body (exits the method)
|
|
1602
|
-
# Includes both non-returning expressions (raise/fail) and explicit returns
|
|
1603
|
-
private def guard_clause?(node)
|
|
1604
|
-
node.is_a?(IR::ReturnNode) || non_returning?(node)
|
|
1605
|
-
end
|
|
1606
|
-
|
|
1607
|
-
# After a guard clause (`return/raise unless x`), narrow the guarded variable
|
|
1608
|
-
# to remove falsy types (NilClass, FalseClass)
|
|
1609
|
-
private def narrow_guard_variable(predicate, kind, context, location)
|
|
1610
|
-
case predicate
|
|
1611
|
-
when Prism::LocalVariableReadNode
|
|
1612
|
-
write_node = context.lookup_variable(predicate.name)
|
|
1613
|
-
return unless write_node
|
|
1614
|
-
|
|
1615
|
-
narrow = IR::NarrowNode.new(write_node, kind, write_node.called_methods, convert_loc(location))
|
|
1616
|
-
context.register_variable(predicate.name, narrow)
|
|
1617
|
-
when Prism::InstanceVariableReadNode
|
|
1618
|
-
write_node = context.lookup_instance_variable(predicate.name)
|
|
1619
|
-
return unless write_node
|
|
1620
|
-
|
|
1621
|
-
narrow = IR::NarrowNode.new(write_node, kind, write_node.called_methods, convert_loc(location))
|
|
1622
|
-
context.narrow_instance_variable(predicate.name, narrow)
|
|
1623
|
-
end
|
|
1624
|
-
end
|
|
1625
|
-
|
|
1626
|
-
private def convert_class_or_module(prism_node, context)
|
|
1627
|
-
# Get class/module name first
|
|
1628
|
-
name = case prism_node.constant_path
|
|
1629
|
-
when Prism::ConstantReadNode
|
|
1630
|
-
prism_node.constant_path.name.to_s
|
|
1631
|
-
when Prism::ConstantPathNode
|
|
1632
|
-
prism_node.constant_path.slice
|
|
1633
|
-
else
|
|
1634
|
-
"Anonymous"
|
|
1635
|
-
end
|
|
1636
|
-
|
|
1637
|
-
# Create a new context for class/module scope with the full class path
|
|
1638
|
-
class_context = context.fork(:class)
|
|
1639
|
-
parent_path = context.current_class_name
|
|
1640
|
-
full_name = parent_path ? "#{parent_path}::#{name}" : name
|
|
1641
|
-
class_context.current_class = full_name
|
|
1642
|
-
|
|
1643
|
-
# Collect all method definitions and nested classes from the body
|
|
1644
|
-
methods = []
|
|
1645
|
-
nested_classes = []
|
|
1646
|
-
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
1647
|
-
prism_node.body.body.each do |stmt|
|
|
1648
|
-
node = convert(stmt, class_context)
|
|
1649
|
-
if node.is_a?(IR::DefNode)
|
|
1650
|
-
methods << node
|
|
1651
|
-
elsif node.is_a?(IR::ClassModuleNode)
|
|
1652
|
-
# Store nested class/module for separate indexing with proper scope
|
|
1653
|
-
nested_classes << node
|
|
1654
|
-
end
|
|
1655
|
-
end
|
|
1656
|
-
end
|
|
1657
|
-
# Store nested classes in methods array (RuntimeAdapter handles both types)
|
|
1658
|
-
methods.concat(nested_classes)
|
|
1659
|
-
|
|
1660
|
-
IR::ClassModuleNode.new(name, methods, [], convert_loc(prism_node.constant_path&.location || prism_node.location))
|
|
1661
|
-
end
|
|
1662
|
-
|
|
1663
|
-
private def convert_singleton_class(prism_node, context)
|
|
1664
|
-
# Create a new context for singleton class scope
|
|
1665
|
-
singleton_context = context.fork(:class)
|
|
1666
|
-
|
|
1667
|
-
# Generate singleton class name in format: Parent::<Class:ParentName>
|
|
1668
|
-
# This matches the scope convention used by RuntimeAdapter and RubyIndexer
|
|
1669
|
-
parent_path = context.current_class_name || ""
|
|
1670
|
-
parent_name = IR.extract_last_name(parent_path) || "Object"
|
|
1671
|
-
singleton_suffix = "<Class:#{parent_name}>"
|
|
1672
|
-
singleton_name = parent_path.empty? ? singleton_suffix : "#{parent_path}::#{singleton_suffix}"
|
|
1673
|
-
singleton_context.current_class = singleton_name
|
|
1674
|
-
|
|
1675
|
-
# Collect all method definitions from the body
|
|
1676
|
-
methods = []
|
|
1677
|
-
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
1678
|
-
prism_node.body.body.each do |stmt|
|
|
1679
|
-
node = convert(stmt, singleton_context)
|
|
1680
|
-
methods << node if node.is_a?(IR::DefNode)
|
|
1681
|
-
end
|
|
1682
|
-
end
|
|
1683
|
-
|
|
1684
|
-
IR::ClassModuleNode.new(singleton_name, methods, [], convert_loc(prism_node.location))
|
|
1685
|
-
end
|
|
1686
|
-
|
|
1687
|
-
private def array_element_type_for(array_node)
|
|
1688
|
-
return Types::TupleType.new([]) if array_node.elements.empty?
|
|
1689
|
-
|
|
1690
|
-
element_types = array_node.elements.filter_map do |elem|
|
|
1691
|
-
literal_type_for(elem) unless elem.nil?
|
|
1692
|
-
end
|
|
1693
|
-
|
|
1694
|
-
return Types::ArrayType.new if element_types.empty?
|
|
1695
|
-
|
|
1696
|
-
if element_types.any? { |t| t.is_a?(Types::Unknown) }
|
|
1697
|
-
# Splat or unknown elements → widen to ArrayType(Union)
|
|
1698
|
-
unique_types = element_types.uniq
|
|
1699
|
-
Types::ArrayType.new(Types::Union.new(unique_types))
|
|
1700
|
-
else
|
|
1701
|
-
Types::TupleType.new(element_types)
|
|
1702
|
-
end
|
|
1703
|
-
end
|
|
1704
|
-
|
|
1705
|
-
private def hash_element_types_for(hash_node)
|
|
1706
|
-
return Types::HashShape.new({}) if hash_node.elements.empty?
|
|
1707
|
-
|
|
1708
|
-
# Check if all keys are symbols for HashShape
|
|
1709
|
-
all_symbol_keys = hash_node.elements.all? do |elem|
|
|
1710
|
-
elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
|
|
1711
|
-
end
|
|
1712
|
-
|
|
1713
|
-
if all_symbol_keys
|
|
1714
|
-
# Build HashShape with field types
|
|
1715
|
-
fields = {}
|
|
1716
|
-
hash_node.elements.each do |elem|
|
|
1717
|
-
next unless elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
|
|
1718
|
-
|
|
1719
|
-
field_name = elem.key.value.to_sym
|
|
1720
|
-
field_type = literal_type_for(elem.value)
|
|
1721
|
-
fields[field_name] = field_type
|
|
1722
|
-
end
|
|
1723
|
-
Types::HashShape.new(fields)
|
|
1724
|
-
else
|
|
1725
|
-
# Non-symbol keys or mixed keys - return HashType
|
|
1726
|
-
key_types = []
|
|
1727
|
-
value_types = []
|
|
1728
|
-
|
|
1729
|
-
hash_node.elements.each do |elem|
|
|
1730
|
-
case elem
|
|
1731
|
-
when Prism::AssocNode
|
|
1732
|
-
key_types << literal_type_for(elem.key) if elem.key
|
|
1733
|
-
value_types << literal_type_for(elem.value) if elem.value
|
|
1734
|
-
end
|
|
1735
|
-
end
|
|
1736
|
-
|
|
1737
|
-
return Types::HashType.new if key_types.empty? && value_types.empty?
|
|
1738
|
-
|
|
1739
|
-
# Deduplicate types
|
|
1740
|
-
unique_key_types = key_types.uniq
|
|
1741
|
-
unique_value_types = value_types.uniq
|
|
1742
|
-
|
|
1743
|
-
key_type = if unique_key_types.size == 1
|
|
1744
|
-
unique_key_types.first
|
|
1745
|
-
elsif unique_key_types.empty?
|
|
1746
|
-
Types::Unknown.instance
|
|
1747
|
-
else
|
|
1748
|
-
Types::Union.new(unique_key_types)
|
|
1749
|
-
end
|
|
1750
|
-
|
|
1751
|
-
value_type = if unique_value_types.size == 1
|
|
1752
|
-
unique_value_types.first
|
|
1753
|
-
elsif unique_value_types.empty?
|
|
1754
|
-
Types::Unknown.instance
|
|
1755
|
-
else
|
|
1756
|
-
Types::Union.new(unique_value_types)
|
|
1757
|
-
end
|
|
1758
|
-
|
|
1759
|
-
Types::HashType.new(key_type, value_type)
|
|
1760
|
-
end
|
|
1761
|
-
end
|
|
1762
|
-
|
|
1763
|
-
private def convert_loc(prism_location)
|
|
1764
|
-
prism_location.start_offset
|
|
1765
|
-
end
|
|
1766
|
-
|
|
1767
|
-
# Register node in location_index and registries during conversion
|
|
1768
|
-
# This eliminates the need for a separate tree traversal after conversion
|
|
1769
|
-
private def register_node(node, context)
|
|
1770
|
-
return unless context.location_index
|
|
1771
|
-
|
|
1772
|
-
case node
|
|
1773
|
-
when IR::DefNode
|
|
1774
|
-
# DefNode uses singleton-adjusted method_scope for registration
|
|
1775
|
-
method_scope = singleton_scope_for(context.current_class_name || "", singleton: node.singleton)
|
|
1776
|
-
context.location_index.add(context.file_path, node, method_scope)
|
|
1777
|
-
register_method(node, context)
|
|
1778
|
-
|
|
1779
|
-
# Register params (created directly, not via convert)
|
|
1780
|
-
# Use method scope with method name for params
|
|
1781
|
-
param_scope = method_scope.empty? ? "##{node.name}" : "#{method_scope}##{node.name}"
|
|
1782
|
-
node.params&.each do |param|
|
|
1783
|
-
context.location_index.add(context.file_path, param, param_scope)
|
|
1784
|
-
end
|
|
1785
|
-
when IR::ClassModuleNode
|
|
1786
|
-
# ClassModuleNode uses parent scope for registration
|
|
1787
|
-
context.location_index.add(context.file_path, node, context.scope_id)
|
|
1788
|
-
register_class_module(node, context)
|
|
1789
|
-
when IR::CallNode
|
|
1790
|
-
context.location_index.add(context.file_path, node, context.scope_id)
|
|
1791
|
-
# Register block params (created directly, not via convert)
|
|
1792
|
-
node.block_params&.each do |param|
|
|
1793
|
-
context.location_index.add(context.file_path, param, context.scope_id)
|
|
1794
|
-
end
|
|
1795
|
-
when IR::InstanceVariableWriteNode
|
|
1796
|
-
context.location_index.add(context.file_path, node, context.scope_id)
|
|
1797
|
-
context.ivar_registry&.register(node.class_name, node.name, node, file_path: context.file_path)
|
|
1798
|
-
when IR::ClassVariableWriteNode
|
|
1799
|
-
context.location_index.add(context.file_path, node, context.scope_id)
|
|
1800
|
-
context.cvar_registry&.register(node.class_name, node.name, node, file_path: context.file_path)
|
|
1801
|
-
else
|
|
1802
|
-
# All other nodes (MergeNode, LiteralNode, etc.)
|
|
1803
|
-
context.location_index.add(context.file_path, node, context.scope_id)
|
|
1804
|
-
end
|
|
1805
|
-
end
|
|
1806
|
-
|
|
1807
|
-
# Register method in method_registry
|
|
1808
|
-
# Only registers top-level methods; class methods are handled by register_class_module
|
|
1809
|
-
private def register_method(node, context)
|
|
1810
|
-
return unless context.method_registry
|
|
1811
|
-
|
|
1812
|
-
# Only register top-level methods (no class context)
|
|
1813
|
-
return unless (context.current_class_name || "").empty?
|
|
1814
|
-
|
|
1815
|
-
context.method_registry.register("", node.name.to_s, node, file_path: context.file_path)
|
|
1816
|
-
end
|
|
1817
|
-
|
|
1818
|
-
# Register methods from a class/module in method_registry
|
|
1819
|
-
private def register_class_module(node, context)
|
|
1820
|
-
return unless context.method_registry
|
|
1821
|
-
|
|
1822
|
-
# Build the full class path from parent context + node name
|
|
1823
|
-
parent_path = context.current_class_name || ""
|
|
1824
|
-
class_path = parent_path.empty? ? node.name : "#{parent_path}::#{node.name}"
|
|
1825
|
-
|
|
1826
|
-
# Register each method in the class (nested classes are handled recursively via convert)
|
|
1827
|
-
node.methods&.each do |method|
|
|
1828
|
-
next if method.is_a?(IR::ClassModuleNode)
|
|
1829
|
-
|
|
1830
|
-
method_scope = singleton_scope_for(class_path, singleton: method.singleton)
|
|
1831
|
-
context.method_registry.register(method_scope, method.name.to_s, method, file_path: context.file_path)
|
|
1832
|
-
|
|
1833
|
-
# module_function: also register as singleton method
|
|
1834
|
-
if method.module_function
|
|
1835
|
-
singleton_scope = singleton_scope_for(class_path, singleton: true)
|
|
1836
|
-
context.method_registry.register(singleton_scope, method.name.to_s, method, file_path: context.file_path)
|
|
1837
|
-
end
|
|
1838
|
-
end
|
|
1839
|
-
end
|
|
1840
|
-
|
|
1841
|
-
# Build singleton class scope for method registration/lookup
|
|
1842
|
-
# Singleton methods use "<Class:ClassName>" suffix to match RubyIndexer convention
|
|
1843
|
-
# @param scope [String] Base scope (e.g., "RBS::Environment")
|
|
1844
|
-
# @param singleton [Boolean] Whether the method is a singleton method
|
|
1845
|
-
# @return [String] Scope with singleton class suffix if applicable
|
|
1846
|
-
private def singleton_scope_for(scope, singleton:)
|
|
1847
|
-
return scope unless singleton
|
|
1848
|
-
|
|
1849
|
-
parent_name = IR.extract_last_name(scope) || "Object"
|
|
1850
|
-
scope.empty? ? "<Class:Object>" : "#{scope}::<Class:#{parent_name}>"
|
|
1851
|
-
end
|
|
1852
|
-
|
|
1853
180
|
# Check if a CallNode is a visibility modifier wrapping a def (e.g., `private def foo`)
|
|
1854
181
|
private def visibility_modifier_with_def?(prism_node)
|
|
1855
182
|
%i[private protected public module_function].include?(prism_node.name) &&
|