type-guessr 0.0.1

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.
@@ -0,0 +1,664 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ir/nodes"
4
+ require_relative "../types"
5
+ require_relative "../type_simplifier"
6
+ require_relative "../registry/method_registry"
7
+ require_relative "../registry/variable_registry"
8
+ require_relative "result"
9
+
10
+ module TypeGuessr
11
+ module Core
12
+ module Inference
13
+ # Resolves types by traversing the IR dependency graph
14
+ # Each node points to nodes it depends on (reverse dependency graph)
15
+ 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
19
+
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
35
+
36
+ # Method registry for storing and looking up project method definitions
37
+ # @return [Registry::MethodRegistry]
38
+ attr_reader :method_registry
39
+
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
51
+ @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
57
+ end
58
+
59
+ # Infer the type of an IR node
60
+ # @param node [IR::Node] IR node to infer type for
61
+ # @return [Result] Inference result with type and reason
62
+ def infer(node)
63
+ return Result.new(Types::Unknown.instance, "no node", :unknown) unless node
64
+
65
+ # Use cache to avoid redundant inference
66
+ cached = @cache[node]
67
+ return cached if cached
68
+
69
+ result = infer_node(node)
70
+
71
+ # Apply type simplification if available
72
+ result = simplify_result(result) if @type_simplifier
73
+
74
+ @cache[node] = result
75
+ result
76
+ end
77
+
78
+ # Clear the inference cache
79
+ def clear_cache
80
+ @cache.clear
81
+ end
82
+
83
+ # Convert a list of matching class names to a type
84
+ # @param classes [Array<String>] List of class names
85
+ # @return [Type, nil] ClassInstance (1 match), Union (2-3 matches), or nil
86
+ 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)
97
+ end
98
+
99
+ private
100
+
101
+ def infer_node(node)
102
+ case node
103
+ when IR::LiteralNode
104
+ infer_literal(node)
105
+ when IR::LocalWriteNode
106
+ infer_local_write(node)
107
+ when IR::LocalReadNode
108
+ infer_local_read(node)
109
+ when IR::InstanceVariableWriteNode
110
+ infer_instance_variable_write(node)
111
+ when IR::InstanceVariableReadNode
112
+ infer_instance_variable_read(node)
113
+ when IR::ClassVariableWriteNode
114
+ infer_class_variable_write(node)
115
+ when IR::ClassVariableReadNode
116
+ infer_class_variable_read(node)
117
+ when IR::ParamNode
118
+ infer_param(node)
119
+ when IR::ConstantNode
120
+ infer_constant(node)
121
+ when IR::CallNode
122
+ infer_call(node)
123
+ when IR::BlockParamSlot
124
+ infer_block_param_slot(node)
125
+ when IR::MergeNode
126
+ infer_merge(node)
127
+ when IR::DefNode
128
+ infer_def(node)
129
+ when IR::SelfNode
130
+ infer_self(node)
131
+ when IR::ReturnNode
132
+ infer_return(node)
133
+ else
134
+ Result.new(Types::Unknown.instance, "unknown node type", :unknown)
135
+ end
136
+ end
137
+
138
+ def infer_literal(node)
139
+ Result.new(node.type, "literal", :literal)
140
+ end
141
+
142
+ def infer_local_write(node)
143
+ return Result.new(Types::Unknown.instance, "unassigned variable", :unknown) unless node.value
144
+
145
+ dep_result = infer(node.value)
146
+ Result.new(dep_result.type, "assigned from #{dep_result.reason}", dep_result.source)
147
+ end
148
+
149
+ def infer_local_read(node)
150
+ return Result.new(Types::Unknown.instance, "unassigned variable", :unknown) unless node.write_node
151
+
152
+ write_result = infer(node.write_node)
153
+
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
+ resolved_type = resolve_called_methods(node.called_methods)
161
+ if resolved_type
162
+ return Result.new(
163
+ resolved_type,
164
+ "variable inferred from #{node.called_methods.join(", ")}",
165
+ :inference
166
+ )
167
+ end
168
+ end
169
+
170
+ write_result
171
+ end
172
+
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)
181
+ write_node = node.write_node
182
+
183
+ # 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
185
+
186
+ return Result.new(Types::Unknown.instance, "unassigned instance variable", :unknown) unless write_node
187
+
188
+ infer(write_node)
189
+ end
190
+
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)
199
+ write_node = node.write_node
200
+
201
+ # 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
203
+
204
+ return Result.new(Types::Unknown.instance, "unassigned class variable", :unknown) unless write_node
205
+
206
+ infer(write_node)
207
+ end
208
+
209
+ def infer_param(node)
210
+ # Handle special parameter kinds first
211
+ case node.kind
212
+ when :rest
213
+ # Rest parameter (*args) is always Array
214
+ return Result.new(Types::ArrayType.new, "rest parameter", :inference)
215
+ when :keyword_rest
216
+ # Keyword rest parameter (**kwargs) is always Hash
217
+ return Result.new(Types::ClassInstance.new("Hash"), "keyword rest parameter", :inference)
218
+ when :block
219
+ # Block parameter (&block) is always Proc
220
+ return Result.new(Types::ClassInstance.new("Proc"), "block parameter", :inference)
221
+ when :forwarding
222
+ # Forwarding parameter (...) forwards all arguments
223
+ return Result.new(Types::ForwardingArgs.instance, "forwarding parameter", :inference)
224
+ end
225
+
226
+ # Try default value for optional parameters
227
+ if node.default_value
228
+ dep_result = infer(node.default_value)
229
+ return Result.new(dep_result.type, "parameter default: #{dep_result.reason}", dep_result.source)
230
+ end
231
+
232
+ # Try to resolve type from called methods
233
+ if node.called_methods.any?
234
+ resolved_type = resolve_called_methods(node.called_methods)
235
+ if resolved_type
236
+ return Result.new(
237
+ resolved_type,
238
+ "parameter inferred from #{node.called_methods.join(", ")}",
239
+ :project
240
+ )
241
+ end
242
+
243
+ return Result.new(
244
+ Types::Unknown.instance,
245
+ "parameter with unresolved methods: #{node.called_methods.join(", ")}",
246
+ :unknown
247
+ )
248
+ end
249
+
250
+ Result.new(Types::Unknown.instance, "parameter without type info", :unknown)
251
+ end
252
+
253
+ def infer_constant(node)
254
+ # If there's a dependency (e.g., constant write), infer from it
255
+ if node.dependency
256
+ dep_result = infer(node.dependency)
257
+ return Result.new(dep_result.type, "constant #{node.name}: #{dep_result.reason}", dep_result.source)
258
+ end
259
+
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
270
+ end
271
+
272
+ Result.new(Types::Unknown.instance, "undefined constant", :unknown)
273
+ end
274
+
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
284
+
285
+ result = infer_class_method_call(class_name, node)
286
+ return result if result
287
+ end
288
+
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
+ )
318
+
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
327
+
328
+ # Substitute self with receiver type
329
+ return_type = return_type.substitute({ self: receiver_type })
330
+
331
+ 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
384
+ )
385
+ end
386
+
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
420
+ end
421
+
422
+ # Method call without receiver or unknown receiver type
423
+ # First, try to lookup top-level method
424
+ def_node = @method_registry.lookup("", node.method.to_s)
425
+ if def_node
426
+ return_type = infer(def_node.return_node)
427
+ return Result.new(return_type.type, "top-level method #{node.method}", :project)
428
+ end
429
+
430
+ # Fallback to Object to query RBS for common methods (==, to_s, etc.)
431
+ 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)
434
+ return_type = return_type.substitute({ self: receiver_type }) if receiver_type
435
+ return Result.new(return_type, "Object##{node.method}", :stdlib) unless return_type.is_a?(Types::Unknown)
436
+
437
+ Result.new(Types::Unknown.instance, "call #{node.method} on unknown receiver", :unknown)
438
+ end
439
+
440
+ def infer_block_param_slot(node)
441
+ return Result.new(Types::Unknown.instance, "block param without type info", :unknown) unless node.call_node.receiver
442
+
443
+ receiver_type = infer(node.call_node.receiver).type
444
+ class_name = receiver_type.rbs_class_name
445
+ return Result.new(Types::Unknown.instance, "block param without type info", :unknown) unless class_name
446
+
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)
449
+
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)
453
+ end
454
+
455
+ return Result.new(Types::Unknown.instance, "block param without type info", :unknown) unless raw_block_param_types.size > node.index
456
+
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))
460
+
461
+ Result.new(resolved_type, "block param from #{class_name}##{node.call_node.method}", :stdlib)
462
+ end
463
+
464
+ def infer_merge(node)
465
+ # Infer types from all branches and create union
466
+ branch_results = node.branches.map { |branch| infer(branch) }
467
+ branch_types = branch_results.map(&:type)
468
+
469
+ union_type = if branch_types.size == 1
470
+ branch_types.first
471
+ else
472
+ Types::Union.new(branch_types)
473
+ end
474
+
475
+ reasons = branch_results.map(&:reason).uniq.join(" | ")
476
+ Result.new(union_type, "branch merge: #{reasons}", :unknown)
477
+ end
478
+
479
+ def infer_def(node)
480
+ # initialize always returns self (the class instance)
481
+ if node.name == :initialize && node.class_name
482
+ return Result.new(
483
+ Types::SelfType.instance,
484
+ "def #{node.name} returns self",
485
+ :project
486
+ )
487
+ end
488
+
489
+ # Empty method body returns nil
490
+ unless node.return_node
491
+ return Result.new(
492
+ Types::ClassInstance.new("NilClass"),
493
+ "def #{node.name} returns nil (empty body)",
494
+ :project
495
+ )
496
+ end
497
+
498
+ return_result = infer(node.return_node)
499
+ Result.new(
500
+ return_result.type,
501
+ "def #{node.name} returns #{return_result.reason}",
502
+ :project
503
+ )
504
+ end
505
+
506
+ def infer_self(node)
507
+ type = if node.singleton
508
+ Types::SingletonType.new(node.class_name)
509
+ else
510
+ Types::ClassInstance.new(node.class_name)
511
+ end
512
+ Result.new(type, "self in #{node.class_name}", :inference)
513
+ end
514
+
515
+ def infer_return(node)
516
+ if node.value
517
+ 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)
521
+ end
522
+ end
523
+
524
+ # Infer class method call (ClassName.method or self.method in singleton context)
525
+ # @param class_name [String] The class name
526
+ # @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
530
+ if node.method == :new
531
+ return Result.new(
532
+ Types::ClassInstance.new(class_name),
533
+ "#{class_name}.new",
534
+ :inference
535
+ )
536
+ end
537
+
538
+ # 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
551
+ end
552
+ end
553
+
554
+ # Fall back to RBS signature provider
555
+ arg_types = node.args.map { |arg| infer(arg).type }
556
+ return_type = @signature_provider.get_class_method_return_type(
557
+ class_name,
558
+ node.method.to_s,
559
+ arg_types
560
+ )
561
+
562
+ unless return_type.is_a?(Types::Unknown)
563
+ return Result.new(
564
+ return_type,
565
+ "#{class_name}.#{node.method} (RBS)",
566
+ :rbs
567
+ )
568
+ end
569
+
570
+ nil
571
+ end
572
+
573
+ # Infer type for HashShape field access (hash[:key])
574
+ # @param hash_shape [Types::HashShape] The hash shape type
575
+ # @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
579
+ return nil unless key_node.is_a?(IR::LiteralNode)
580
+ return nil unless key_node.type.is_a?(Types::ClassInstance) && key_node.type.name == "Symbol"
581
+ return nil unless key_node.literal_value.is_a?(Symbol)
582
+
583
+ key = key_node.literal_value
584
+ field_type = hash_shape.fields[key]
585
+
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
606
+
607
+ # Then try project methods
608
+ resolve_called_methods_from_project(called_methods.map(&:to_s))
609
+ end
610
+
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?
617
+
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
622
+
623
+ # Filter out subclasses when parent is also matched (prefer most general type)
624
+ matching_classes = filter_to_most_general_types(matching_classes)
625
+
626
+ classes_to_type(matching_classes)
627
+ end
628
+
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
641
+ end
642
+
643
+ # Apply type simplification to a result
644
+ # @param result [Result] The inference result
645
+ # @return [Result] Result with simplified type
646
+ def simplify_result(result)
647
+ simplified_type = @type_simplifier.simplify(result.type)
648
+ return result if simplified_type.equal?(result.type)
649
+
650
+ Result.new(simplified_type, result.reason, result.source)
651
+ end
652
+
653
+ # Build substitutions hash with type variables and self
654
+ # @param receiver_type [Type] The receiver type
655
+ # @return [Hash{Symbol => Type}] Substitutions including :self
656
+ def build_substitutions(receiver_type)
657
+ substitutions = receiver_type.type_variable_substitutions.dup
658
+ substitutions[:self] = receiver_type
659
+ substitutions
660
+ end
661
+ end
662
+ end
663
+ end
664
+ end