type-guessr 0.0.1 → 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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -0
  3. data/exe/type-guessr +30 -0
  4. data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
  5. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
  6. data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
  7. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
  8. data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
  9. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  10. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  11. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  12. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  13. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  14. data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
  15. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  16. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
  17. data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
  18. data/lib/type-guessr.rb +3 -11
  19. data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
  20. data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
  21. data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
  22. data/lib/type_guessr/core/cache.rb +5 -0
  23. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
  24. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  25. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  26. data/lib/type_guessr/core/converter/context.rb +144 -0
  27. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  28. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  29. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  30. data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
  31. data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
  32. data/lib/type_guessr/core/converter/registration.rb +100 -0
  33. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  34. data/lib/type_guessr/core/converter.rb +4 -0
  35. data/lib/type_guessr/core/index/location_index.rb +32 -0
  36. data/lib/type_guessr/core/index.rb +3 -0
  37. data/lib/type_guessr/core/inference/resolver.rb +516 -349
  38. data/lib/type_guessr/core/inference.rb +4 -0
  39. data/lib/type_guessr/core/ir/nodes.rb +362 -103
  40. data/lib/type_guessr/core/ir.rb +3 -0
  41. data/lib/type_guessr/core/logger.rb +6 -13
  42. data/lib/type_guessr/core/node_context_helper.rb +126 -0
  43. data/lib/type_guessr/core/node_key_generator.rb +31 -0
  44. data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
  45. data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
  46. data/lib/type_guessr/core/registry/method_registry.rb +65 -38
  47. data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
  48. data/lib/type_guessr/core/registry.rb +6 -0
  49. data/lib/type_guessr/core/signature_builder.rb +39 -0
  50. data/lib/type_guessr/core/type_serializer.rb +96 -0
  51. data/lib/type_guessr/core/type_simplifier.rb +15 -12
  52. data/lib/type_guessr/core/types.rb +250 -32
  53. data/lib/type_guessr/core.rb +29 -0
  54. data/lib/type_guessr/mcp/file_watcher.rb +87 -0
  55. data/lib/type_guessr/mcp/server.rb +463 -0
  56. data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
  57. data/lib/type_guessr/version.rb +1 -1
  58. metadata +57 -8
  59. data/lib/type_guessr/core/rbs_provider.rb +0 -304
  60. data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
  61. data/lib/type_guessr/core/signature_provider.rb +0 -101
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../ir/nodes"
3
+ require_relative "../ir"
4
4
  require_relative "../types"
5
5
  require_relative "../type_simplifier"
6
- require_relative "../registry/method_registry"
7
- require_relative "../registry/variable_registry"
6
+ require_relative "../registry"
8
7
  require_relative "result"
9
8
 
10
9
  module TypeGuessr
@@ -13,60 +12,70 @@ module TypeGuessr
13
12
  # Resolves types by traversing the IR dependency graph
14
13
  # Each node points to nodes it depends on (reverse dependency graph)
15
14
  class Resolver
16
- # Callback for resolving method lists to class instances
17
- # @return [Proc, nil] A proc that takes Array<Symbol> and returns resolved type or nil
18
- attr_accessor :method_list_resolver
15
+ # Sentinel value to detect circular references during inference
16
+ INFERRING = Object.new.freeze
19
17
 
20
- # Callback for getting class ancestors
21
- # @return [Proc, nil] A proc that takes class_name and returns array of ancestor names
22
- attr_accessor :ancestry_provider
23
-
24
- # Callback for checking if a constant is a class or module
25
- # @return [Proc, nil] A proc that takes constant_name and returns :class, :module, or nil
26
- attr_accessor :constant_kind_provider
27
-
28
- # Callback for looking up class methods via RubyIndexer
29
- # @return [Proc, nil] A proc that takes (class_name, method_name) and returns owner_name or nil
30
- attr_accessor :class_method_lookup_provider
31
-
32
- # Type simplifier for normalizing union types
33
- # @return [TypeSimplifier, nil]
34
- attr_accessor :type_simplifier
18
+ # Maximum recursion depth for type inference.
19
+ # Measured max across project+gem code: 43 (rubygems default_gem_spec chain).
20
+ # Set to 50 to cover all observed cases with margin.
21
+ MAX_DEPTH = 50
35
22
 
36
23
  # Method registry for storing and looking up project method definitions
37
24
  # @return [Registry::MethodRegistry]
38
25
  attr_reader :method_registry
39
26
 
40
- # Variable registry for storing and looking up instance/class variables
41
- # @return [Registry::VariableRegistry]
42
- attr_reader :variable_registry
43
-
44
- # @param signature_provider [SignatureProvider] Provider for RBS method signatures
45
- # @param method_registry [Registry::MethodRegistry, nil] Registry for project methods
46
- # @param variable_registry [Registry::VariableRegistry, nil] Registry for variables
47
- def initialize(signature_provider, method_registry: nil, variable_registry: nil)
48
- @signature_provider = signature_provider
49
- @method_registry = method_registry || Registry::MethodRegistry.new
50
- @variable_registry = variable_registry || Registry::VariableRegistry.new
27
+ # Instance variable registry for storing and looking up instance variables
28
+ # @return [Registry::InstanceVariableRegistry]
29
+ attr_reader :ivar_registry
30
+
31
+ # Class variable registry for storing and looking up class variables
32
+ # @return [Registry::ClassVariableRegistry]
33
+ attr_reader :cvar_registry
34
+
35
+ # @param signature_registry [Registry::SignatureRegistry] Registry for stdlib RBS signatures
36
+ # @param type_simplifier [TypeSimplifier] Type simplifier for normalizing union types
37
+ # @param code_index [#find_classes_defining_methods, #ancestors_of, #constant_kind, #class_method_owner]
38
+ # Adapter wrapping RubyIndexer
39
+ # @param method_registry [Registry::MethodRegistry] Registry for project methods
40
+ # @param ivar_registry [Registry::InstanceVariableRegistry] Registry for instance variables
41
+ # @param cvar_registry [Registry::ClassVariableRegistry] Registry for class variables
42
+ def initialize(signature_registry, type_simplifier:, code_index:, method_registry:, ivar_registry:, cvar_registry:)
43
+ @signature_registry = signature_registry
44
+ @code_index = code_index
45
+ @method_registry = method_registry
46
+ @ivar_registry = ivar_registry
47
+ @cvar_registry = cvar_registry
51
48
  @cache = {}.compare_by_identity
52
- @method_list_resolver = nil
53
- @ancestry_provider = nil
54
- @constant_kind_provider = nil
55
- @class_method_lookup_provider = nil
56
- @type_simplifier = nil
49
+ @type_simplifier = type_simplifier
50
+ @depth = 0
57
51
  end
58
52
 
59
53
  # Infer the type of an IR node
60
54
  # @param node [IR::Node] IR node to infer type for
61
55
  # @return [Result] Inference result with type and reason
62
56
  def infer(node)
57
+ # Early return: nil node passed (defensive, shouldn't happen in normal flow)
63
58
  return Result.new(Types::Unknown.instance, "no node", :unknown) unless node
64
59
 
65
60
  # Use cache to avoid redundant inference
66
61
  cached = @cache[node]
62
+
63
+ # Early return: circular reference detected (A→B→A pattern)
64
+ # INFERRING sentinel means we're already processing this node up the call stack
65
+ return Result.new(Types::Unknown.instance, "circular reference", :unknown) if cached.equal?(INFERRING)
66
+
67
+ # Early return: cache hit - return previously computed result
67
68
  return cached if cached
68
69
 
70
+ # Early return: depth limit exceeded
71
+ return Result.new(Types::Unknown.instance, "max depth exceeded", :unknown) if @depth >= MAX_DEPTH
72
+
73
+ # Mark as in-progress to detect cycles
74
+ @cache[node] = INFERRING
75
+
76
+ @depth += 1
69
77
  result = infer_node(node)
78
+ @depth -= 1
70
79
 
71
80
  # Apply type simplification if available
72
81
  result = simplify_result(result) if @type_simplifier
@@ -82,36 +91,28 @@ module TypeGuessr
82
91
 
83
92
  # Convert a list of matching class names to a type
84
93
  # @param classes [Array<String>] List of class names
85
- # @return [Type, nil] ClassInstance (1 match), Union (2-3 matches), or nil
94
+ # @return [Type] The resulting type
86
95
  def classes_to_type(classes)
87
- case classes.size
88
- when 0
89
- nil
90
- when 1
91
- Types::ClassInstance.new(classes.first)
92
- when 2, 3
93
- types = classes.map { |c| Types::ClassInstance.new(c) }
94
- Types::Union.new(types)
95
- end
96
- # 4+ matches → nil (too ambiguous)
96
+ # Early return: no classes matched the called_methods query
97
+ return Types::Unknown.instance if classes.empty?
98
+ # Early return: single class match - no Union needed
99
+ return Types::ClassInstance.for(classes.first) if classes.size == 1
100
+
101
+ types = classes.map { |c| Types::ClassInstance.for(c) }
102
+ union = Types::Union.new(types)
103
+ @type_simplifier.simplify(union)
97
104
  end
98
105
 
99
- private
100
-
101
- def infer_node(node)
106
+ private def infer_node(node)
102
107
  case node
103
108
  when IR::LiteralNode
104
109
  infer_literal(node)
105
- when IR::LocalWriteNode
106
- infer_local_write(node)
110
+ when IR::LocalWriteNode, IR::InstanceVariableWriteNode, IR::ClassVariableWriteNode
111
+ infer_variable_write(node)
107
112
  when IR::LocalReadNode
108
113
  infer_local_read(node)
109
- when IR::InstanceVariableWriteNode
110
- infer_instance_variable_write(node)
111
114
  when IR::InstanceVariableReadNode
112
115
  infer_instance_variable_read(node)
113
- when IR::ClassVariableWriteNode
114
- infer_class_variable_write(node)
115
116
  when IR::ClassVariableReadNode
116
117
  infer_class_variable_read(node)
117
118
  when IR::ParamNode
@@ -122,12 +123,16 @@ module TypeGuessr
122
123
  infer_call(node)
123
124
  when IR::BlockParamSlot
124
125
  infer_block_param_slot(node)
126
+ when IR::OrNode
127
+ infer_or(node)
125
128
  when IR::MergeNode
126
129
  infer_merge(node)
127
130
  when IR::DefNode
128
131
  infer_def(node)
129
132
  when IR::SelfNode
130
133
  infer_self(node)
134
+ when IR::NarrowNode
135
+ infer_narrow(node)
131
136
  when IR::ReturnNode
132
137
  infer_return(node)
133
138
  else
@@ -135,30 +140,29 @@ module TypeGuessr
135
140
  end
136
141
  end
137
142
 
138
- def infer_literal(node)
143
+ private def infer_literal(node)
139
144
  Result.new(node.type, "literal", :literal)
140
145
  end
141
146
 
142
- def infer_local_write(node)
147
+ private def infer_variable_write(node)
143
148
  return Result.new(Types::Unknown.instance, "unassigned variable", :unknown) unless node.value
144
149
 
145
150
  dep_result = infer(node.value)
146
151
  Result.new(dep_result.type, "assigned from #{dep_result.reason}", dep_result.source)
147
152
  end
148
153
 
149
- def infer_local_read(node)
154
+ private def infer_local_read(node)
155
+ # Early return: variable read without corresponding write (undefined variable)
150
156
  return Result.new(Types::Unknown.instance, "unassigned variable", :unknown) unless node.write_node
151
157
 
152
158
  write_result = infer(node.write_node)
153
159
 
154
- # If type is Unknown (or Union of only Unknown), try to resolve from called_methods
155
- type_is_unknown = write_result.type.is_a?(Types::Unknown) ||
156
- (write_result.type.is_a?(Types::Union) &&
157
- write_result.type.types.all? { |t| t.is_a?(Types::Unknown) })
158
-
159
- if type_is_unknown && node.called_methods.any?
160
+ # Fallback: write type is Unknown, try called_methods inference
161
+ if write_result.type.is_a?(Types::Unknown) && node.called_methods.any?
160
162
  resolved_type = resolve_called_methods(node.called_methods)
161
- if resolved_type
163
+
164
+ # Early return: called_methods successfully resolved to a type
165
+ if !resolved_type.is_a?(Types::Unknown)
162
166
  return Result.new(
163
167
  resolved_type,
164
168
  "variable inferred from #{node.called_methods.join(", ")}",
@@ -170,298 +174,365 @@ module TypeGuessr
170
174
  write_result
171
175
  end
172
176
 
173
- def infer_instance_variable_write(node)
174
- return Result.new(Types::Unknown.instance, "unassigned instance variable", :unknown) unless node.value
175
-
176
- dep_result = infer(node.value)
177
- Result.new(dep_result.type, "assigned from #{dep_result.reason}", dep_result.source)
178
- end
179
-
180
- def infer_instance_variable_read(node)
177
+ private def infer_instance_variable_read(node)
181
178
  write_node = node.write_node
182
179
 
183
180
  # Deferred lookup: if write_node is nil at conversion time, try registry
184
- write_node = @variable_registry.lookup_instance_variable(node.class_name, node.name) if write_node.nil? && node.class_name
181
+ write_node = @ivar_registry.lookup(node.class_name, node.name) if write_node.nil? && node.class_name
185
182
 
183
+ # Early return: @var write not found (conversion + registry both failed)
186
184
  return Result.new(Types::Unknown.instance, "unassigned instance variable", :unknown) unless write_node
187
185
 
188
186
  infer(write_node)
189
187
  end
190
188
 
191
- def infer_class_variable_write(node)
192
- return Result.new(Types::Unknown.instance, "unassigned class variable", :unknown) unless node.value
193
-
194
- dep_result = infer(node.value)
195
- Result.new(dep_result.type, "assigned from #{dep_result.reason}", dep_result.source)
196
- end
197
-
198
- def infer_class_variable_read(node)
189
+ private def infer_class_variable_read(node)
199
190
  write_node = node.write_node
200
191
 
201
192
  # Deferred lookup: if write_node is nil at conversion time, try registry
202
- write_node = @variable_registry.lookup_class_variable(node.class_name, node.name) if write_node.nil? && node.class_name
193
+ write_node = @cvar_registry.lookup(node.class_name, node.name) if write_node.nil? && node.class_name
203
194
 
195
+ # Early return: @@var write not found (conversion + registry both failed)
204
196
  return Result.new(Types::Unknown.instance, "unassigned class variable", :unknown) unless write_node
205
197
 
206
198
  infer(write_node)
207
199
  end
208
200
 
209
- def infer_param(node)
210
- # Handle special parameter kinds first
201
+ private def infer_param(node)
202
+ # Early returns for special parameter kinds (type is known structurally)
211
203
  case node.kind
212
204
  when :rest
213
- # Rest parameter (*args) is always Array
205
+ # Early return: *args is always Array
214
206
  return Result.new(Types::ArrayType.new, "rest parameter", :inference)
215
207
  when :keyword_rest
216
- # Keyword rest parameter (**kwargs) is always Hash
217
- return Result.new(Types::ClassInstance.new("Hash"), "keyword rest parameter", :inference)
208
+ # Early return: **kwargs is always Hash[Symbol, untyped]
209
+ return Result.new(
210
+ Types::HashType.new(Types::ClassInstance.for("Symbol"), Types::Unknown.instance),
211
+ "keyword rest parameter",
212
+ :inference
213
+ )
218
214
  when :block
219
- # Block parameter (&block) is always Proc
220
- return Result.new(Types::ClassInstance.new("Proc"), "block parameter", :inference)
215
+ # Early return: &block is always Proc
216
+ return Result.new(Types::ClassInstance.for("Proc"), "block parameter", :inference)
221
217
  when :forwarding
222
- # Forwarding parameter (...) forwards all arguments
218
+ # Early return: ... forwards all arguments
223
219
  return Result.new(Types::ForwardingArgs.instance, "forwarding parameter", :inference)
224
220
  end
225
221
 
226
- # Try default value for optional parameters
222
+ # Early return: optional parameter with default value - infer from default
227
223
  if node.default_value
228
224
  dep_result = infer(node.default_value)
229
225
  return Result.new(dep_result.type, "parameter default: #{dep_result.reason}", dep_result.source)
230
226
  end
231
227
 
232
- # Try to resolve type from called methods
228
+ # Fallback: try to resolve type from called methods
233
229
  if node.called_methods.any?
234
230
  resolved_type = resolve_called_methods(node.called_methods)
235
- if resolved_type
231
+
232
+ # Early return: called_methods query found no matching classes
233
+ if resolved_type.is_a?(Types::Unknown)
234
+ return Result.new(
235
+ Types::Unknown.instance,
236
+ "parameter with unresolved methods: #{node.called_methods.join(", ")}",
237
+ :unknown
238
+ )
239
+ # Early return: called_methods successfully resolved to a type
240
+ else
236
241
  return Result.new(
237
242
  resolved_type,
238
243
  "parameter inferred from #{node.called_methods.join(", ")}",
239
244
  :project
240
245
  )
241
246
  end
242
-
243
- return Result.new(
244
- Types::Unknown.instance,
245
- "parameter with unresolved methods: #{node.called_methods.join(", ")}",
246
- :unknown
247
- )
248
247
  end
249
248
 
250
249
  Result.new(Types::Unknown.instance, "parameter without type info", :unknown)
251
250
  end
252
251
 
253
- def infer_constant(node)
254
- # If there's a dependency (e.g., constant write), infer from it
252
+ private def infer_constant(node)
253
+ # Early return: constant assignment (Foo = ...) - infer from assigned value
255
254
  if node.dependency
256
255
  dep_result = infer(node.dependency)
257
256
  return Result.new(dep_result.type, "constant #{node.name}: #{dep_result.reason}", dep_result.source)
258
257
  end
259
258
 
260
- # Check if constant is a class or module using RubyIndexer
261
- if @constant_kind_provider
262
- kind = @constant_kind_provider.call(node.name)
263
- if %i[class module].include?(kind)
264
- return Result.new(
265
- Types::SingletonType.new(node.name),
266
- "class constant #{node.name}",
267
- :inference
268
- )
269
- end
259
+ # Check if constant is a class or module using code_index adapter
260
+ kind = @code_index&.constant_kind(node.name)
261
+
262
+ # Early return: class/module constant - return singleton type
263
+ if %i[class module].include?(kind)
264
+ return Result.new(
265
+ Types::SingletonType.new(node.name),
266
+ "class constant #{node.name}",
267
+ :inference
268
+ )
270
269
  end
271
270
 
272
271
  Result.new(Types::Unknown.instance, "undefined constant", :unknown)
273
272
  end
274
273
 
275
- def infer_call(node)
276
- # Special case: Class method calls (ClassName.method)
277
- if node.receiver.is_a?(IR::ConstantNode)
278
- # Resolve constant first (handles aliases like RecipeAlias = Recipe)
279
- receiver_result = infer(node.receiver)
280
- class_name = case receiver_result.type
281
- when Types::SingletonType then receiver_result.type.name
282
- else node.receiver.name
283
- end
274
+ private def infer_call(node)
275
+ return infer_constant_receiver_call(node) if node.receiver.is_a?(IR::ConstantNode)
276
+ return infer_no_receiver_call(node) unless node.receiver
277
+
278
+ receiver_type = infer(node.receiver).type
284
279
 
285
- result = infer_class_method_call(class_name, node)
280
+ case receiver_type
281
+ when Types::SingletonType
282
+ result = infer_class_method_call(receiver_type.name, node)
286
283
  return result if result
284
+ when Types::ClassInstance then return infer_class_instance_call(node, receiver_type)
285
+ when Types::ArrayType then return infer_array_call(node, receiver_type)
286
+ when Types::TupleType then return infer_tuple_call(node, receiver_type)
287
+ when Types::HashShape then return infer_hash_shape_call(node, receiver_type)
288
+ when Types::RangeType then return infer_range_call(node, receiver_type)
289
+ when Types::HashType then return infer_hash_type_call(node, receiver_type)
290
+ when Types::Unknown then return infer_unknown_receiver_call(node, receiver_type)
287
291
  end
288
292
 
289
- # Infer receiver type first
290
- if node.receiver
291
- receiver_result = infer(node.receiver)
292
- receiver_type = receiver_result.type
293
-
294
- # Query for method return type: project first, then RBS
295
- case receiver_type
296
- when Types::SingletonType
297
- result = infer_class_method_call(receiver_type.name, node)
298
- return result if result
299
- when Types::ClassInstance
300
- # 1. Try project methods first
301
- def_node = @method_registry.lookup(receiver_type.name, node.method.to_s)
302
- if def_node
303
- return_result = infer(def_node)
304
- return Result.new(
305
- return_result.type,
306
- "#{receiver_type.name}##{node.method} (project)",
307
- :project
308
- )
309
- end
310
-
311
- # 2. Fall back to RBS signature provider
312
- arg_types = node.args.map { |arg| infer(arg).type }
313
- return_type = @signature_provider.get_method_return_type(
314
- receiver_type.name,
315
- node.method.to_s,
316
- arg_types
317
- )
293
+ infer_fallback_call(node, receiver_type)
294
+ end
318
295
 
319
- # Fall back to Object if class-specific lookup returns Unknown
320
- if return_type.is_a?(Types::Unknown) && receiver_type.name != "Object"
321
- return_type = @signature_provider.get_method_return_type(
322
- "Object",
323
- node.method.to_s,
324
- arg_types
325
- )
326
- end
296
+ # ClassName.method resolve constant alias, then class method lookup
297
+ private def infer_constant_receiver_call(node)
298
+ receiver_result = infer(node.receiver)
299
+ class_name = case receiver_result.type
300
+ when Types::SingletonType then receiver_result.type.name
301
+ else node.receiver.name
302
+ end
327
303
 
328
- # Substitute self with receiver type
329
- return_type = return_type.substitute({ self: receiver_type })
304
+ result = infer_class_method_call(class_name, node)
305
+ return result if result
330
306
 
307
+ # Constant receiver but class method not found — fall through to no-receiver fallback
308
+ infer_no_receiver_call(node)
309
+ end
310
+
311
+ # receiver.method where receiver is a ClassInstance — project method → DSL/RBS → Object fallback
312
+ private def infer_class_instance_call(node, receiver_type)
313
+ # 1. Try project methods first
314
+ def_node = @method_registry.lookup(receiver_type.name, node.method.to_s)
315
+ if def_node
316
+ return_result = infer(def_node)
317
+ return Result.new(
318
+ return_result.type,
319
+ "#{receiver_type.name}##{node.method} (project)",
320
+ :project
321
+ )
322
+ end
323
+
324
+ # 2. Fall back to SignatureRegistry (DSL or RBS)
325
+ arg_types = node.args.map { |arg| infer(arg).type }
326
+ entry = @signature_registry.lookup(receiver_type.name, node.method.to_s)
327
+ source = entry.is_a?(Registry::SignatureRegistry::GemMethodEntry) && entry.skip_stdlib_rbs? ? :dsl : :stdlib
328
+
329
+ return_type = @signature_registry.get_method_return_type(
330
+ receiver_type.name,
331
+ node.method.to_s,
332
+ arg_types
333
+ )
334
+
335
+ # Fall back to Object if class-specific lookup returns Unknown
336
+ if return_type.is_a?(Types::Unknown) && receiver_type.name != "Object"
337
+ return_type = @signature_registry.get_method_return_type(
338
+ "Object",
339
+ node.method.to_s,
340
+ arg_types
341
+ )
342
+ source = :stdlib
343
+ end
344
+
345
+ # Substitute class-level type vars, self, block return type, and remaining type variables
346
+ substitutions = build_substitutions(receiver_type)
347
+ add_method_type_var_substitutions(substitutions, node, receiver_type.name, node.method.to_s, arg_types)
348
+ return_type = return_type.substitute(substitutions)
349
+
350
+ Result.new(
351
+ return_type,
352
+ "#{receiver_type.name}##{node.method}",
353
+ source
354
+ )
355
+ end
356
+
357
+ # Unknown receiver — infer receiver type from method uniqueness, then retry
358
+ private def infer_unknown_receiver_call(node, receiver_type)
359
+ cm = IR::CalledMethod.new(name: node.method, positional_count: nil, keywords: [])
360
+ inferred_receiver = resolve_called_methods([cm])
361
+
362
+ if inferred_receiver.is_a?(Types::ClassInstance)
363
+ # Try project methods with inferred receiver type
364
+ def_node = @method_registry.lookup(inferred_receiver.name, node.method.to_s)
365
+ if def_node
366
+ return_result = infer(def_node)
331
367
  return Result.new(
332
- return_type,
333
- "#{receiver_type.name}##{node.method}",
334
- :stdlib
335
- )
336
- when Types::ArrayType
337
- # Handle Array methods with element type substitution
338
- substitutions = build_substitutions(receiver_type)
339
-
340
- # Check for block presence and infer its return type for U substitution
341
- if node.has_block
342
- if node.block_body
343
- block_result = infer(node.block_body)
344
- substitutions[:U] = block_result.type unless block_result.type.is_a?(Types::Unknown)
345
- else
346
- # Empty block returns nil
347
- substitutions[:U] = Types::ClassInstance.new("NilClass")
348
- end
349
- end
350
-
351
- # Get raw return type, then substitute type variables
352
- raw_return_type = @signature_provider.get_method_return_type("Array", node.method.to_s)
353
- return_type = raw_return_type.substitute(substitutions)
354
- return Result.new(
355
- return_type,
356
- "Array[#{receiver_type.element_type || "untyped"}]##{node.method}",
357
- :stdlib
358
- )
359
- when Types::HashShape
360
- # Handle HashShape field access with [] method
361
- if node.method == :[] && node.args.size == 1
362
- key_result = infer_hash_shape_access(receiver_type, node.args.first)
363
- return key_result if key_result
364
- end
365
-
366
- # Fall back to Hash RBS for other methods
367
- substitutions = build_substitutions(receiver_type)
368
- raw_return_type = @signature_provider.get_method_return_type("Hash", node.method.to_s)
369
- return_type = raw_return_type.substitute(substitutions)
370
- return Result.new(
371
- return_type,
372
- "HashShape##{node.method}",
373
- :stdlib
374
- )
375
- when Types::HashType
376
- # Handle generic HashType
377
- substitutions = build_substitutions(receiver_type)
378
- raw_return_type = @signature_provider.get_method_return_type("Hash", node.method.to_s)
379
- return_type = raw_return_type.substitute(substitutions)
380
- return Result.new(
381
- return_type,
382
- "Hash[#{receiver_type.key_type}, #{receiver_type.value_type}]##{node.method}",
383
- :stdlib
368
+ return_result.type,
369
+ "#{inferred_receiver.name}##{node.method} (inferred receiver)",
370
+ :project
384
371
  )
385
372
  end
386
373
 
387
- # Try to infer Unknown receiver type from method uniqueness
388
- # Also handle Union types that are effectively Unknown (only contain Unknown)
389
- receiver_is_unknown = receiver_type.is_a?(Types::Unknown) ||
390
- (receiver_type.is_a?(Types::Union) &&
391
- receiver_type.types.all? { |t| t.is_a?(Types::Unknown) })
392
- if receiver_is_unknown
393
- inferred_receiver = resolve_called_methods([node.method])
394
- if inferred_receiver.is_a?(Types::ClassInstance)
395
- # Try project methods with inferred receiver type
396
- def_node = @method_registry.lookup(inferred_receiver.name, node.method.to_s)
397
- if def_node
398
- return_result = infer(def_node)
399
- return Result.new(
400
- return_result.type,
401
- "#{inferred_receiver.name}##{node.method} (inferred receiver)",
402
- :project
403
- )
404
- end
405
-
406
- # Fall back to RBS
407
- arg_types = node.args.map { |arg| infer(arg).type }
408
- return_type = @signature_provider.get_method_return_type(
409
- inferred_receiver.name,
410
- node.method.to_s,
411
- arg_types
412
- )
413
- return Result.new(
414
- return_type,
415
- "#{inferred_receiver.name}##{node.method} (inferred receiver)",
416
- :stdlib
417
- )
418
- end
419
- end
374
+ # RBS lookup with inferred receiver
375
+ arg_types = node.args.map { |arg| infer(arg).type }
376
+ return_type = @signature_registry.get_method_return_type(
377
+ inferred_receiver.name,
378
+ node.method.to_s,
379
+ arg_types
380
+ )
381
+ substitutions = build_substitutions(inferred_receiver)
382
+ add_method_type_var_substitutions(substitutions, node, inferred_receiver.name, node.method.to_s, arg_types)
383
+ return_type = return_type.substitute(substitutions)
384
+ return Result.new(
385
+ return_type,
386
+ "#{inferred_receiver.name}##{node.method} (inferred receiver)",
387
+ :stdlib
388
+ )
420
389
  end
421
390
 
422
- # Method call without receiver or unknown receiver type
423
- # First, try to lookup top-level method
391
+ infer_fallback_call(node, receiver_type)
392
+ end
393
+
394
+ # No receiver — top-level method → Object RBS → Unknown
395
+ private def infer_no_receiver_call(node)
396
+ infer_fallback_call(node, nil)
397
+ end
398
+
399
+ # Shared fallback: top-level method → Object RBS → Unknown
400
+ private def infer_fallback_call(node, receiver_type)
401
+ # Try top-level method first
424
402
  def_node = @method_registry.lookup("", node.method.to_s)
425
403
  if def_node
426
404
  return_type = infer(def_node.return_node)
427
405
  return Result.new(return_type.type, "top-level method #{node.method}", :project)
428
406
  end
429
407
 
430
- # Fallback to Object to query RBS for common methods (==, to_s, etc.)
408
+ # Object RBS for common methods (==, to_s, etc.)
431
409
  arg_types = node.args.map { |arg| infer(arg).type }
432
- return_type = @signature_provider.get_method_return_type("Object", node.method.to_s, arg_types)
433
- # Substitute self with receiver type if available (e.g., Object#dup returns self)
410
+ return_type = @signature_registry.get_method_return_type("Object", node.method.to_s, arg_types)
434
411
  return_type = return_type.substitute({ self: receiver_type }) if receiver_type
435
412
  return Result.new(return_type, "Object##{node.method}", :stdlib) unless return_type.is_a?(Types::Unknown)
436
413
 
437
414
  Result.new(Types::Unknown.instance, "call #{node.method} on unknown receiver", :unknown)
438
415
  end
439
416
 
440
- def infer_block_param_slot(node)
417
+ private def infer_array_call(node, receiver_type)
418
+ substitutions = build_substitutions(receiver_type)
419
+ add_method_type_var_substitutions(substitutions, node, "Array", node.method.to_s)
420
+ raw_return_type = @signature_registry.get_method_return_type("Array", node.method.to_s)
421
+ return_type = raw_return_type.substitute(substitutions)
422
+ Result.new(
423
+ return_type,
424
+ "Array[#{receiver_type.element_type || "untyped"}]##{node.method}",
425
+ :stdlib
426
+ )
427
+ end
428
+
429
+ private def infer_tuple_call(node, receiver_type)
430
+ # Handle indexed access with integer literal
431
+ if node.method == :[] && node.args.size == 1
432
+ tuple_result = infer_tuple_access(receiver_type, node.args.first)
433
+ return tuple_result if tuple_result
434
+ end
435
+
436
+ # Fall back to Array RBS for other methods
437
+ substitutions = build_substitutions(receiver_type)
438
+ add_method_type_var_substitutions(substitutions, node, "Array", node.method.to_s)
439
+ raw_return_type = @signature_registry.get_method_return_type("Array", node.method.to_s)
440
+ return_type = raw_return_type.substitute(substitutions)
441
+ Result.new(
442
+ return_type,
443
+ "[#{receiver_type.element_types.join(", ")}]##{node.method}",
444
+ :stdlib
445
+ )
446
+ end
447
+
448
+ private def infer_hash_shape_call(node, receiver_type)
449
+ # Handle HashShape field access with [] method
450
+ if node.method == :[] && node.args.size == 1
451
+ key_result = infer_hash_shape_access(receiver_type, node.args.first)
452
+ return key_result if key_result
453
+ end
454
+
455
+ # Fall back to Hash RBS for other methods
456
+ substitutions = build_substitutions(receiver_type)
457
+ add_method_type_var_substitutions(substitutions, node, "Hash", node.method.to_s)
458
+ raw_return_type = @signature_registry.get_method_return_type("Hash", node.method.to_s)
459
+ return_type = raw_return_type.substitute(substitutions)
460
+ Result.new(
461
+ return_type,
462
+ "HashShape##{node.method}",
463
+ :stdlib
464
+ )
465
+ end
466
+
467
+ private def infer_range_call(node, receiver_type)
468
+ substitutions = build_substitutions(receiver_type)
469
+ add_method_type_var_substitutions(substitutions, node, "Range", node.method.to_s)
470
+ raw_return_type = @signature_registry.get_method_return_type("Range", node.method.to_s)
471
+ return_type = raw_return_type.substitute(substitutions)
472
+ Result.new(
473
+ return_type,
474
+ "Range[#{receiver_type.element_type}]##{node.method}",
475
+ :stdlib
476
+ )
477
+ end
478
+
479
+ private def infer_hash_type_call(node, receiver_type)
480
+ substitutions = build_substitutions(receiver_type)
481
+ add_method_type_var_substitutions(substitutions, node, "Hash", node.method.to_s)
482
+ raw_return_type = @signature_registry.get_method_return_type("Hash", node.method.to_s)
483
+ return_type = raw_return_type.substitute(substitutions)
484
+ Result.new(
485
+ return_type,
486
+ "Hash[#{receiver_type.key_type}, #{receiver_type.value_type}]##{node.method}",
487
+ :stdlib
488
+ )
489
+ end
490
+
491
+ private def infer_block_param_slot(node)
492
+ # Early return: top-level block (each { } without receiver class context)
441
493
  return Result.new(Types::Unknown.instance, "block param without type info", :unknown) unless node.call_node.receiver
442
494
 
443
495
  receiver_type = infer(node.call_node.receiver).type
444
496
  class_name = receiver_type.rbs_class_name
445
- return Result.new(Types::Unknown.instance, "block param without type info", :unknown) unless class_name
446
497
 
447
- # Get block parameter types (returns internal types with TypeVariables)
448
- raw_block_param_types = @signature_provider.get_block_param_types(class_name, node.call_node.method.to_s)
498
+ # Path 1: RBS-based inference (when receiver type has a class name)
499
+ if class_name
500
+ raw_block_param_types = @signature_registry.get_block_param_types(class_name, node.call_node.method.to_s)
501
+
502
+ if raw_block_param_types.empty? && class_name != "Object"
503
+ raw_block_param_types = @signature_registry.get_block_param_types("Object", node.call_node.method.to_s)
504
+ end
449
505
 
450
- # Fall back to Object if class-specific lookup returns empty
451
- if raw_block_param_types.empty? && class_name != "Object"
452
- raw_block_param_types = @signature_provider.get_block_param_types("Object", node.call_node.method.to_s)
506
+ if raw_block_param_types.size > node.index
507
+ raw_type = raw_block_param_types[node.index]
508
+ resolved_type = raw_type.substitute(build_substitutions(receiver_type))
509
+ return Result.new(resolved_type, "block param from #{class_name}##{node.call_node.method}", :stdlib)
510
+ end
453
511
  end
454
512
 
455
- return Result.new(Types::Unknown.instance, "block param without type info", :unknown) unless raw_block_param_types.size > node.index
513
+ # Path 2: Fallback to called_methods inference (like infer_param)
514
+ if node.called_methods.any?
515
+ resolved_type = resolve_called_methods(node.called_methods)
516
+
517
+ if resolved_type.is_a?(Types::Unknown)
518
+ return Result.new(
519
+ Types::Unknown.instance,
520
+ "block param with unresolved methods: #{node.called_methods.join(", ")}",
521
+ :unknown
522
+ )
523
+ end
456
524
 
457
- # Type#substitute applies type variable and self substitutions
458
- raw_type = raw_block_param_types[node.index]
459
- resolved_type = raw_type.substitute(build_substitutions(receiver_type))
525
+ return Result.new(
526
+ resolved_type,
527
+ "block param inferred from #{node.called_methods.join(", ")}",
528
+ :project
529
+ )
530
+ end
460
531
 
461
- Result.new(resolved_type, "block param from #{class_name}##{node.call_node.method}", :stdlib)
532
+ Result.new(Types::Unknown.instance, "block param without type info", :unknown)
462
533
  end
463
534
 
464
- def infer_merge(node)
535
+ private def infer_merge(node)
465
536
  # Infer types from all branches and create union
466
537
  branch_results = node.branches.map { |branch| infer(branch) }
467
538
  branch_types = branch_results.map(&:type)
@@ -476,8 +547,60 @@ module TypeGuessr
476
547
  Result.new(union_type, "branch merge: #{reasons}", :unknown)
477
548
  end
478
549
 
479
- def infer_def(node)
480
- # initialize always returns self (the class instance)
550
+ private def infer_or(node)
551
+ lhs_result = infer(node.lhs)
552
+ rhs_result = infer(node.rhs)
553
+ lhs_type = lhs_result.type
554
+
555
+ # Unknown LHS → use RHS (no truthiness info available for Unknown)
556
+ return Result.new(rhs_result.type, "or: #{rhs_result.reason} (lhs unknown)", rhs_result.source) if lhs_type.is_a?(Types::Unknown)
557
+
558
+ # Separate LHS into truthy and falsy parts
559
+ truthy_lhs = remove_falsy_types(lhs_type)
560
+ has_falsy = falsy_types?(lhs_type)
561
+
562
+ if !has_falsy
563
+ # LHS is always truthy → RHS unreachable
564
+ Result.new(lhs_type, "or: #{lhs_result.reason} (always truthy)", lhs_result.source)
565
+ elsif truthy_lhs.is_a?(Types::Unknown)
566
+ # LHS is entirely falsy → RHS only
567
+ Result.new(rhs_result.type, "or: #{rhs_result.reason} (lhs falsy)", rhs_result.source)
568
+ else
569
+ # Mixed: truthy part of LHS | RHS
570
+ union = Types::Union.new([truthy_lhs, rhs_result.type])
571
+ Result.new(union, "or: #{lhs_result.reason} | #{rhs_result.reason}", :unknown)
572
+ end
573
+ end
574
+
575
+ private def remove_falsy_types(type)
576
+ case type
577
+ when Types::Union
578
+ truthy = type.types.reject { |t| falsy_type?(t) }
579
+ case truthy.size
580
+ when 0 then Types::Unknown.instance
581
+ when 1 then truthy.first
582
+ else Types::Union.new(truthy)
583
+ end
584
+ else
585
+ falsy_type?(type) ? Types::Unknown.instance : type
586
+ end
587
+ end
588
+
589
+ private def falsy_types?(type)
590
+ case type
591
+ when Types::Union
592
+ type.types.any? { |t| falsy_type?(t) }
593
+ else
594
+ falsy_type?(type)
595
+ end
596
+ end
597
+
598
+ private def falsy_type?(type)
599
+ type.is_a?(Types::ClassInstance) && %w[NilClass FalseClass].include?(type.name)
600
+ end
601
+
602
+ private def infer_def(node)
603
+ # Early return: initialize always returns self (Ruby semantics)
481
604
  if node.name == :initialize && node.class_name
482
605
  return Result.new(
483
606
  Types::SelfType.instance,
@@ -486,10 +609,10 @@ module TypeGuessr
486
609
  )
487
610
  end
488
611
 
489
- # Empty method body returns nil
612
+ # Early return: empty method body returns nil (Ruby semantics)
490
613
  unless node.return_node
491
614
  return Result.new(
492
- Types::ClassInstance.new("NilClass"),
615
+ Types::ClassInstance.for("NilClass"),
493
616
  "def #{node.name} returns nil (empty body)",
494
617
  :project
495
618
  )
@@ -503,161 +626,205 @@ module TypeGuessr
503
626
  )
504
627
  end
505
628
 
506
- def infer_self(node)
629
+ private def infer_self(node)
507
630
  type = if node.singleton
508
631
  Types::SingletonType.new(node.class_name)
509
632
  else
510
- Types::ClassInstance.new(node.class_name)
633
+ Types::ClassInstance.for(node.class_name)
511
634
  end
512
635
  Result.new(type, "self in #{node.class_name}", :inference)
513
636
  end
514
637
 
515
- def infer_return(node)
638
+ private def infer_narrow(node)
639
+ inner_result = infer(node.value)
640
+ narrowed_type = case node.kind
641
+ when :truthy then remove_falsy_types(inner_result.type)
642
+ else inner_result.type
643
+ end
644
+ Result.new(narrowed_type, "narrowed(#{inner_result.reason})", inner_result.source)
645
+ end
646
+
647
+ private def infer_return(node)
648
+ # Early return: `return expr` - infer from expression
516
649
  if node.value
517
650
  value_result = infer(node.value)
518
- Result.new(value_result.type, "explicit return: #{value_result.reason}", value_result.source)
519
- else
520
- Result.new(Types::ClassInstance.new("NilClass"), "explicit return nil", :inference)
651
+ return Result.new(value_result.type, "explicit return: #{value_result.reason}", value_result.source)
521
652
  end
653
+
654
+ # Default: bare `return` returns nil
655
+ Result.new(Types::ClassInstance.for("NilClass"), "explicit return nil", :inference)
522
656
  end
523
657
 
524
658
  # Infer class method call (ClassName.method or self.method in singleton context)
525
659
  # @param class_name [String] The class name
526
660
  # @param node [IR::CallNode] The call node
527
- # @return [Result, nil] The result if resolved, nil otherwise
528
- def infer_class_method_call(class_name, node)
529
- # ClassName.new returns instance of that class
661
+ # @return [Result, nil] The result if resolved, nil otherwise (caller handles fallback)
662
+ private def infer_class_method_call(class_name, node)
663
+ # Early return: Foo.new always returns Foo instance
530
664
  if node.method == :new
531
665
  return Result.new(
532
- Types::ClassInstance.new(class_name),
666
+ Types::ClassInstance.for(class_name),
533
667
  "#{class_name}.new",
534
668
  :inference
535
669
  )
536
670
  end
537
671
 
538
672
  # Try project class methods first (includes extended module methods)
539
- if @class_method_lookup_provider
540
- owner_name = @class_method_lookup_provider.call(class_name, node.method.to_s)
541
- if owner_name
542
- def_node = @method_registry.lookup(owner_name, node.method.to_s)
543
- if def_node
544
- return_result = infer(def_node)
545
- return Result.new(
546
- return_result.type,
547
- "#{class_name}.#{node.method} (project)",
548
- :project
549
- )
550
- end
673
+ # Use code_index adapter to find method owner
674
+ owner_name = @code_index&.class_method_owner(class_name, node.method.to_s)
675
+
676
+ # Early return: project class method found
677
+ if owner_name
678
+ def_node = @method_registry.lookup(owner_name, node.method.to_s)
679
+ if def_node
680
+ return_result = infer(def_node)
681
+ return Result.new(
682
+ return_result.type,
683
+ "#{class_name}.#{node.method} (project)",
684
+ :project
685
+ )
551
686
  end
552
687
  end
553
688
 
554
- # Fall back to RBS signature provider
689
+ # Fall back to SignatureRegistry (DSL or RBS)
555
690
  arg_types = node.args.map { |arg| infer(arg).type }
556
- return_type = @signature_provider.get_class_method_return_type(
691
+ class_entry = @signature_registry.lookup_class_method(class_name, node.method.to_s)
692
+ class_source = class_entry.is_a?(Registry::SignatureRegistry::GemMethodEntry) && class_entry.skip_stdlib_rbs? ? :dsl : :rbs
693
+
694
+ return_type = @signature_registry.get_class_method_return_type(
557
695
  class_name,
558
696
  node.method.to_s,
559
697
  arg_types
560
698
  )
561
699
 
700
+ # Early return: class method found — substitute SelfType with actual class
562
701
  unless return_type.is_a?(Types::Unknown)
702
+ return_type = return_type.substitute(self: Types::ClassInstance.for(class_name))
563
703
  return Result.new(
564
704
  return_type,
565
- "#{class_name}.#{node.method} (RBS)",
566
- :rbs
705
+ "#{class_name}.#{node.method}",
706
+ class_source
567
707
  )
568
708
  end
569
709
 
710
+ # Return nil to let caller try other fallback strategies
570
711
  nil
571
712
  end
572
713
 
573
714
  # Infer type for HashShape field access (hash[:key])
574
715
  # @param hash_shape [Types::HashShape] The hash shape type
575
716
  # @param key_node [IR::Node] The key argument node
576
- # @return [Result, nil] The field type result, or nil if not a known symbol key
577
- def infer_hash_shape_access(hash_shape, key_node)
578
- # Only handle symbol literal keys
717
+ # @return [Result, nil] The field type result, or nil if not a known symbol key (caller falls back to Hash RBS)
718
+ private def infer_hash_shape_access(hash_shape, key_node)
719
+ # Early return nil: dynamic key (hash[var]) - can't resolve at static analysis time
579
720
  return nil unless key_node.is_a?(IR::LiteralNode)
721
+ # Early return nil: non-symbol key (hash["key"]) - HashShape only tracks symbol keys
580
722
  return nil unless key_node.type.is_a?(Types::ClassInstance) && key_node.type.name == "Symbol"
723
+ # Early return nil: literal_value not Symbol (defensive)
581
724
  return nil unless key_node.literal_value.is_a?(Symbol)
582
725
 
583
726
  key = key_node.literal_value
584
727
  field_type = hash_shape.fields[key]
585
728
 
586
- if field_type
587
- Result.new(field_type, "HashShape[:#{key}]", :inference)
588
- else
589
- # Key not found in shape - return nil type (like Hash#[] for missing keys)
590
- Result.new(Types::ClassInstance.new("NilClass"), "HashShape[:#{key}] (missing)", :inference)
591
- end
592
- end
593
-
594
- # Resolve called methods to a type
595
- # First tries external resolver (RubyIndexer), then project methods
596
- # @param called_methods [Array<Symbol>] Methods called on the parameter
597
- # @return [Type, nil] Resolved type or nil
598
- def resolve_called_methods(called_methods)
599
- return nil if called_methods.empty?
600
-
601
- # First try external resolver (RubyIndexer)
602
- if @method_list_resolver
603
- resolved = @method_list_resolver.call(called_methods)
604
- return resolved if resolved && !resolved.is_a?(Types::Unknown)
605
- end
729
+ # Early return: known field found in shape
730
+ return Result.new(field_type, "HashShape[:#{key}]", :inference) if field_type
606
731
 
607
- # Then try project methods
608
- resolve_called_methods_from_project(called_methods.map(&:to_s))
732
+ # Default: key not found in shape - return nil type (like Hash#[] for missing keys)
733
+ Result.new(Types::ClassInstance.for("NilClass"), "HashShape[:#{key}] (missing)", :inference)
609
734
  end
610
735
 
611
- # Resolve called methods from project method registry
612
- # Returns ClassInstance if exactly one class matches, Union if 2-3 match, nil otherwise
613
- # @param methods [Array<String>] Method names
614
- # @return [Type, nil] Resolved type or nil
615
- def resolve_called_methods_from_project(methods)
616
- return nil if methods.empty?
736
+ # Infer type for TupleType indexed access (tuple[0])
737
+ # @param tuple_type [Types::TupleType] The tuple type
738
+ # @param index_node [IR::Node] The index argument node
739
+ # @return [Result, nil] The element type result, or nil if not an integer literal (caller falls back to Array RBS)
740
+ private def infer_tuple_access(tuple_type, index_node)
741
+ return nil unless index_node.is_a?(IR::LiteralNode)
742
+ return nil unless index_node.type.is_a?(Types::ClassInstance) && index_node.type.name == "Integer"
743
+ return nil unless index_node.literal_value.is_a?(Integer)
617
744
 
618
- # Find classes that define all the methods (including inherited ones)
619
- matching_classes = @method_registry.registered_classes.select do |class_name|
620
- @method_registry.all_methods_for_class(class_name).superset?(methods.to_set)
621
- end
745
+ index = index_node.literal_value
746
+ # Support negative indexing
747
+ index = tuple_type.element_types.size + index if index.negative?
622
748
 
623
- # Filter out subclasses when parent is also matched (prefer most general type)
624
- matching_classes = filter_to_most_general_types(matching_classes)
749
+ element_type = tuple_type.element_types[index]
750
+ return Result.new(element_type, "Tuple[#{index}]", :inference) if element_type
625
751
 
626
- classes_to_type(matching_classes)
752
+ Result.new(Types::ClassInstance.for("NilClass"), "Tuple[#{index}] (out of range)", :inference)
627
753
  end
628
754
 
629
- # Filter out classes whose ancestor is also in the list
630
- # This ensures we return the most general type that satisfies the constraint
631
- # @param classes [Array<String>] List of class names
632
- # @return [Array<String>] Filtered list with only the most general types
633
- def filter_to_most_general_types(classes)
634
- return classes unless @ancestry_provider
635
-
636
- classes.reject do |class_name|
637
- ancestors = @ancestry_provider.call(class_name)
638
- # Check if any ancestor (excluding self) is also in the matching list
639
- ancestors.any? { |ancestor| ancestor != class_name && classes.include?(ancestor) }
640
- end
755
+ # Resolve called methods to a type via code_index adapter
756
+ # @param called_methods [Array<CalledMethod>] List of called methods
757
+ # @return [Type] The resulting type
758
+ private def resolve_called_methods(called_methods)
759
+ # Early return: no method call info available for inference
760
+ return Types::Unknown.instance if called_methods.empty?
761
+
762
+ classes = @code_index.find_classes_defining_methods(called_methods)
763
+ classes_to_type(classes)
641
764
  end
642
765
 
643
766
  # Apply type simplification to a result
644
767
  # @param result [Result] The inference result
645
768
  # @return [Result] Result with simplified type
646
- def simplify_result(result)
769
+ private def simplify_result(result)
647
770
  simplified_type = @type_simplifier.simplify(result.type)
648
771
  return result if simplified_type.equal?(result.type)
649
772
 
650
773
  Result.new(simplified_type, result.reason, result.source)
651
774
  end
652
775
 
776
+ # Add block return type substitution to substitutions hash
777
+ # Uses the actual type variable name from the method signature instead of hardcoding :U.
778
+ # @param substitutions [Hash{Symbol => Type}] Substitutions hash to modify
779
+ # @param node [IR::CallNode] The call node with block info
780
+ # @param block_return_var [Symbol, nil] The type variable name for block return (e.g., :U, :X)
781
+ # @return [void]
782
+ private def add_block_return_substitution(substitutions, node, block_return_var)
783
+ return unless node.has_block && block_return_var
784
+
785
+ if node.block_body
786
+ block_result = infer(node.block_body)
787
+ substitutions[block_return_var] = block_result.type
788
+ else
789
+ # Empty block returns nil
790
+ substitutions[block_return_var] = Types::ClassInstance.for("NilClass")
791
+ end
792
+ end
793
+
794
+ # Substitute remaining method-level type variables with Unknown
795
+ # Prevents TypeVariable leakage in inferred types.
796
+ # @param substitutions [Hash{Symbol => Type}] Substitutions hash to modify
797
+ # @param type_params [Array<Symbol>] Method-level type parameter names
798
+ # @return [void]
799
+ private def substitute_remaining_type_vars(substitutions, type_params)
800
+ type_params.each { |name| substitutions[name] ||= Types::Unknown.instance }
801
+ end
802
+
653
803
  # Build substitutions hash with type variables and self
654
804
  # @param receiver_type [Type] The receiver type
655
805
  # @return [Hash{Symbol => Type}] Substitutions including :self
656
- def build_substitutions(receiver_type)
657
- substitutions = receiver_type.type_variable_substitutions.dup
806
+ private def build_substitutions(receiver_type)
807
+ substitutions = receiver_type.type_parameter_bindings.dup
658
808
  substitutions[:self] = receiver_type
659
809
  substitutions
660
810
  end
811
+
812
+ # Add method-level type variable substitutions from entry
813
+ # Looks up the MethodEntry and adds block_return_var + remaining type_params substitutions.
814
+ # @param substitutions [Hash{Symbol => Type}] Substitutions hash to modify
815
+ # @param node [IR::CallNode] The call node with block info
816
+ # @param class_name [String] The class to look up
817
+ # @param method_name [String] The method to look up
818
+ # @param arg_types [Array<Types::Type>] Argument types for overload matching
819
+ # @return [void]
820
+ private def add_method_type_var_substitutions(substitutions, node, class_name, method_name, arg_types = [])
821
+ entry = @signature_registry.lookup(class_name, method_name)
822
+ entry ||= @signature_registry.lookup("Object", method_name) if class_name != "Object"
823
+ return unless entry
824
+
825
+ add_block_return_substitution(substitutions, node, entry.block_return_type_var(arg_types))
826
+ substitute_remaining_type_vars(substitutions, entry.type_params(arg_types))
827
+ end
661
828
  end
662
829
  end
663
830
  end