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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +138 -0
- data/lib/ruby_lsp/type_guessr/config.rb +90 -0
- data/lib/ruby_lsp/type_guessr/debug_server.rb +861 -0
- data/lib/ruby_lsp/type_guessr/graph_builder.rb +349 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +565 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +506 -0
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +200 -0
- data/lib/type-guessr.rb +28 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +1649 -0
- data/lib/type_guessr/core/converter/rbs_converter.rb +88 -0
- data/lib/type_guessr/core/index/location_index.rb +72 -0
- data/lib/type_guessr/core/inference/resolver.rb +664 -0
- data/lib/type_guessr/core/inference/result.rb +41 -0
- data/lib/type_guessr/core/ir/nodes.rb +599 -0
- data/lib/type_guessr/core/logger.rb +43 -0
- data/lib/type_guessr/core/rbs_provider.rb +304 -0
- data/lib/type_guessr/core/registry/method_registry.rb +106 -0
- data/lib/type_guessr/core/registry/variable_registry.rb +87 -0
- data/lib/type_guessr/core/signature_provider.rb +101 -0
- data/lib/type_guessr/core/type_simplifier.rb +64 -0
- data/lib/type_guessr/core/types.rb +425 -0
- data/lib/type_guessr/version.rb +5 -0
- metadata +81 -0
|
@@ -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
|