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
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
- require_relative "../ir/nodes"
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) &&