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.
- checksums.yaml +4 -4
- data/README.md +41 -0
- data/exe/type-guessr +30 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
- data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
- data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
- data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
- data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
- data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
- data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
- data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
- data/lib/type-guessr.rb +3 -11
- data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
- data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
- data/lib/type_guessr/core/cache.rb +5 -0
- data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
- data/lib/type_guessr/core/converter/call_converter.rb +161 -0
- data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
- data/lib/type_guessr/core/converter/context.rb +144 -0
- data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
- data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
- data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
- data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
- data/lib/type_guessr/core/converter/registration.rb +100 -0
- data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
- data/lib/type_guessr/core/converter.rb +4 -0
- data/lib/type_guessr/core/index/location_index.rb +32 -0
- data/lib/type_guessr/core/index.rb +3 -0
- data/lib/type_guessr/core/inference/resolver.rb +516 -349
- data/lib/type_guessr/core/inference.rb +4 -0
- data/lib/type_guessr/core/ir/nodes.rb +362 -103
- data/lib/type_guessr/core/ir.rb +3 -0
- data/lib/type_guessr/core/logger.rb +6 -13
- data/lib/type_guessr/core/node_context_helper.rb +126 -0
- data/lib/type_guessr/core/node_key_generator.rb +31 -0
- data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
- data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
- data/lib/type_guessr/core/registry/method_registry.rb +65 -38
- data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
- data/lib/type_guessr/core/registry.rb +6 -0
- data/lib/type_guessr/core/signature_builder.rb +39 -0
- data/lib/type_guessr/core/type_serializer.rb +96 -0
- data/lib/type_guessr/core/type_simplifier.rb +15 -12
- data/lib/type_guessr/core/types.rb +250 -32
- data/lib/type_guessr/core.rb +29 -0
- data/lib/type_guessr/mcp/file_watcher.rb +87 -0
- data/lib/type_guessr/mcp/server.rb +463 -0
- data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
- data/lib/type_guessr/version.rb +1 -1
- metadata +57 -8
- data/lib/type_guessr/core/rbs_provider.rb +0 -304
- data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
- 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
|
|
3
|
+
require_relative "../ir"
|
|
4
4
|
require_relative "../types"
|
|
5
5
|
require_relative "../type_simplifier"
|
|
6
|
-
require_relative "../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
|
-
#
|
|
17
|
-
|
|
18
|
-
attr_accessor :method_list_resolver
|
|
15
|
+
# Sentinel value to detect circular references during inference
|
|
16
|
+
INFERRING = Object.new.freeze
|
|
19
17
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
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
|
-
#
|
|
41
|
-
# @return [Registry::
|
|
42
|
-
attr_reader :
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
# @
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
@
|
|
53
|
-
@
|
|
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
|
|
94
|
+
# @return [Type] The resulting type
|
|
86
95
|
def classes_to_type(classes)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
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 = @
|
|
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
|
|
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 = @
|
|
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
|
-
#
|
|
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
|
-
#
|
|
205
|
+
# Early return: *args is always Array
|
|
214
206
|
return Result.new(Types::ArrayType.new, "rest parameter", :inference)
|
|
215
207
|
when :keyword_rest
|
|
216
|
-
#
|
|
217
|
-
return Result.new(
|
|
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
|
-
#
|
|
220
|
-
return Result.new(Types::ClassInstance.
|
|
215
|
+
# Early return: &block is always Proc
|
|
216
|
+
return Result.new(Types::ClassInstance.for("Proc"), "block parameter", :inference)
|
|
221
217
|
when :forwarding
|
|
222
|
-
#
|
|
218
|
+
# Early return: ... forwards all arguments
|
|
223
219
|
return Result.new(Types::ForwardingArgs.instance, "forwarding parameter", :inference)
|
|
224
220
|
end
|
|
225
221
|
|
|
226
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
"#{
|
|
334
|
-
:
|
|
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
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
#
|
|
408
|
+
# Object RBS for common methods (==, to_s, etc.)
|
|
431
409
|
arg_types = node.args.map { |arg| infer(arg).type }
|
|
432
|
-
return_type = @
|
|
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
|
|
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
|
-
#
|
|
448
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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(
|
|
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
|
|
480
|
-
|
|
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
|
-
#
|
|
612
|
+
# Early return: empty method body returns nil (Ruby semantics)
|
|
490
613
|
unless node.return_node
|
|
491
614
|
return Result.new(
|
|
492
|
-
Types::ClassInstance.
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
)
|
|
550
|
-
|
|
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
|
|
689
|
+
# Fall back to SignatureRegistry (DSL or RBS)
|
|
555
690
|
arg_types = node.args.map { |arg| infer(arg).type }
|
|
556
|
-
|
|
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}
|
|
566
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
#
|
|
608
|
-
|
|
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
|
-
#
|
|
612
|
-
#
|
|
613
|
-
# @param
|
|
614
|
-
# @return [
|
|
615
|
-
def
|
|
616
|
-
return nil
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
624
|
-
|
|
749
|
+
element_type = tuple_type.element_types[index]
|
|
750
|
+
return Result.new(element_type, "Tuple[#{index}]", :inference) if element_type
|
|
625
751
|
|
|
626
|
-
|
|
752
|
+
Result.new(Types::ClassInstance.for("NilClass"), "Tuple[#{index}] (out of range)", :inference)
|
|
627
753
|
end
|
|
628
754
|
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
# @
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return
|
|
635
|
-
|
|
636
|
-
classes
|
|
637
|
-
|
|
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.
|
|
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
|