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
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
require_relative "../types"
|
|
5
|
+
require_relative "../logger"
|
|
6
|
+
require_relative "../converter"
|
|
7
|
+
require_relative "../type_serializer"
|
|
8
|
+
|
|
9
|
+
module TypeGuessr
|
|
10
|
+
module Core
|
|
11
|
+
module Registry
|
|
12
|
+
# Preloads stdlib RBS signatures and provides O(1) hash lookup
|
|
13
|
+
# Singleton to ensure RBS is loaded only once across all usages
|
|
14
|
+
class SignatureRegistry
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :instance
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Represents a method signature entry from RBS
|
|
20
|
+
# Handles overload resolution and type conversion
|
|
21
|
+
class MethodEntry
|
|
22
|
+
# @param method_types [Array<RBS::MethodType>] RBS method type definitions
|
|
23
|
+
# @param converter [Converter::RBSConverter] RBS to internal type converter
|
|
24
|
+
def initialize(method_types, converter)
|
|
25
|
+
@method_types = method_types
|
|
26
|
+
@converter = converter
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get the return type with overload resolution
|
|
30
|
+
# @param arg_types [Array<Types::Type>] argument types for overload matching
|
|
31
|
+
# @return [Types::Type] the return type
|
|
32
|
+
def return_type(arg_types = [])
|
|
33
|
+
best_match = find_best_overload(arg_types)
|
|
34
|
+
@converter.convert(best_match.type.return_type)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get block parameter types for this method
|
|
38
|
+
# @return [Array<Types::Type>] array of block parameter types (empty if no block)
|
|
39
|
+
def block_param_types
|
|
40
|
+
return @block_param_types if defined?(@block_param_types)
|
|
41
|
+
|
|
42
|
+
@block_param_types = compute_block_param_types
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get method-level type parameter names from the best matching overload
|
|
46
|
+
# @param arg_types [Array<Types::Type>] argument types for overload matching
|
|
47
|
+
# @return [Array<Symbol>] type parameter names (e.g., [:U], [:X], [])
|
|
48
|
+
def type_params(arg_types = [])
|
|
49
|
+
find_best_overload(arg_types).type_params.map(&:name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the type variable used as the block return type
|
|
53
|
+
# Returns the method-level type variable that appears in the block's return type,
|
|
54
|
+
# allowing Resolver to substitute it with the actual block body type.
|
|
55
|
+
# @param arg_types [Array<Types::Type>] argument types for overload matching
|
|
56
|
+
# @return [Symbol, nil] type variable name (e.g., :U, :X) or nil if no block/no type var
|
|
57
|
+
def block_return_type_var(arg_types = [])
|
|
58
|
+
best = find_best_overload(arg_types)
|
|
59
|
+
return nil unless best.block
|
|
60
|
+
|
|
61
|
+
method_type_param_names = best.type_params.to_set(&:name)
|
|
62
|
+
return nil if method_type_param_names.empty?
|
|
63
|
+
|
|
64
|
+
extract_type_var_from_return(best.block.type.return_type, method_type_param_names)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get raw method signatures for display
|
|
68
|
+
# @return [Array<RBS::MethodType>] raw RBS method types
|
|
69
|
+
def signatures
|
|
70
|
+
@method_types
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get formatted signature strings for display
|
|
74
|
+
# @return [Array<String>] human-readable method signatures
|
|
75
|
+
def signature_strings
|
|
76
|
+
@method_types.map(&:to_s)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private def compute_block_param_types
|
|
80
|
+
# Find the signature with a block
|
|
81
|
+
sig_with_block = @method_types.find(&:block)
|
|
82
|
+
return [] unless sig_with_block
|
|
83
|
+
|
|
84
|
+
extract_block_param_types(sig_with_block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private def extract_block_param_types(method_type)
|
|
88
|
+
return [] unless method_type.block
|
|
89
|
+
|
|
90
|
+
block_func = method_type.block.type
|
|
91
|
+
return [] unless block_func.is_a?(RBS::Types::Function)
|
|
92
|
+
|
|
93
|
+
block_func.required_positionals.flat_map do |param|
|
|
94
|
+
# Handle Tuple types (e.g., [K, V] in Hash#each) by flattening
|
|
95
|
+
if param.type.is_a?(RBS::Types::Tuple)
|
|
96
|
+
param.type.types.map { |t| @converter.convert(t) }
|
|
97
|
+
else
|
|
98
|
+
[@converter.convert(param.type)]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Extract a method-level type variable from the block return type
|
|
104
|
+
# Walks the RBS type tree to find a Variable whose name is in the method's type_params.
|
|
105
|
+
# Handles Union types like `nil | false | U` (filter_map).
|
|
106
|
+
# @param rbs_type [RBS::Types::t] the block return type from RBS
|
|
107
|
+
# @param method_type_param_names [Set<Symbol>] method-level type param names
|
|
108
|
+
# @return [Symbol, nil] the type variable name, or nil if not found
|
|
109
|
+
private def extract_type_var_from_return(rbs_type, method_type_param_names)
|
|
110
|
+
case rbs_type
|
|
111
|
+
when RBS::Types::Variable
|
|
112
|
+
method_type_param_names.include?(rbs_type.name) ? rbs_type.name : nil
|
|
113
|
+
when RBS::Types::Union
|
|
114
|
+
# Search union members for a type variable (e.g., nil | false | U)
|
|
115
|
+
rbs_type.types.each do |t|
|
|
116
|
+
found = extract_type_var_from_return(t, method_type_param_names)
|
|
117
|
+
return found if found
|
|
118
|
+
end
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Find the best matching overload for given argument types
|
|
124
|
+
# @param arg_types [Array<Types::Type>] argument types
|
|
125
|
+
# @return [RBS::MethodType] best matching method type (first if no match)
|
|
126
|
+
private def find_best_overload(arg_types)
|
|
127
|
+
return @method_types.first if arg_types.empty?
|
|
128
|
+
|
|
129
|
+
# Score each overload
|
|
130
|
+
scored = @method_types.map do |method_type|
|
|
131
|
+
score = calculate_overload_score(method_type, arg_types)
|
|
132
|
+
[method_type, score]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Return best scoring overload, or first if all scores are 0
|
|
136
|
+
best = scored.max_by { |_mt, score| score }
|
|
137
|
+
best[1].positive? ? best[0] : @method_types.first
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Calculate match score for an overload
|
|
141
|
+
private def calculate_overload_score(method_type, arg_types)
|
|
142
|
+
func = method_type.type
|
|
143
|
+
return 0 unless func.is_a?(RBS::Types::Function)
|
|
144
|
+
|
|
145
|
+
required = func.required_positionals
|
|
146
|
+
optional = func.optional_positionals
|
|
147
|
+
rest = func.rest_positionals
|
|
148
|
+
|
|
149
|
+
# Check argument count
|
|
150
|
+
min_args = required.size
|
|
151
|
+
max_args = rest ? Float::INFINITY : required.size + optional.size
|
|
152
|
+
return 0 unless arg_types.size.between?(min_args, max_args)
|
|
153
|
+
|
|
154
|
+
# Score each argument match
|
|
155
|
+
score = 0
|
|
156
|
+
arg_types.each_with_index do |arg_type, i|
|
|
157
|
+
param = if i < required.size
|
|
158
|
+
required[i]
|
|
159
|
+
elsif i < required.size + optional.size
|
|
160
|
+
optional[i - required.size]
|
|
161
|
+
else
|
|
162
|
+
rest
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
break unless param
|
|
166
|
+
|
|
167
|
+
score += type_match_score(arg_type, param.type)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
score
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Calculate match score between our type and RBS parameter type
|
|
174
|
+
private def type_match_score(our_type, rbs_type)
|
|
175
|
+
case rbs_type
|
|
176
|
+
when RBS::Types::ClassInstance
|
|
177
|
+
class_name = rbs_type.name.to_s.delete_prefix("::")
|
|
178
|
+
return 2 if types_match_class?(our_type, class_name)
|
|
179
|
+
|
|
180
|
+
0
|
|
181
|
+
when RBS::Types::Union
|
|
182
|
+
max_score = rbs_type.types.map { |t| type_match_score(our_type, t) }.max || 0
|
|
183
|
+
max_score.positive? ? 1 : 0
|
|
184
|
+
else
|
|
185
|
+
# Unknown RBS type - give weak match to avoid penalizing
|
|
186
|
+
1
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private def types_match_class?(our_type, class_name)
|
|
191
|
+
case our_type
|
|
192
|
+
when Types::ClassInstance
|
|
193
|
+
our_type.name == class_name
|
|
194
|
+
when Types::ArrayType, Types::TupleType
|
|
195
|
+
class_name == "Array"
|
|
196
|
+
when Types::HashShape
|
|
197
|
+
class_name == "Hash"
|
|
198
|
+
else
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Represents a cached gem method entry (pre-inferred, no RBS)
|
|
205
|
+
# Implements the same interface as MethodEntry for transparent lookup
|
|
206
|
+
class GemMethodEntry
|
|
207
|
+
attr_reader :params
|
|
208
|
+
|
|
209
|
+
def initialize(return_type, params = [], skip_stdlib_rbs: false)
|
|
210
|
+
@return_type = return_type
|
|
211
|
+
@params = params
|
|
212
|
+
@skip_stdlib_rbs = skip_stdlib_rbs
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def skip_stdlib_rbs? = @skip_stdlib_rbs
|
|
216
|
+
|
|
217
|
+
def return_type(_arg_types = [])
|
|
218
|
+
@return_type
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def block_param_types
|
|
222
|
+
[]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def type_params(_arg_types = [])
|
|
226
|
+
[]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def block_return_type_var(_arg_types = [])
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def signatures
|
|
234
|
+
[]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Get formatted signature strings for display
|
|
238
|
+
# @return [Array<String>] human-readable method signatures
|
|
239
|
+
def signature_strings
|
|
240
|
+
param_str = @params.join(", ")
|
|
241
|
+
["(#{param_str}) -> #{@return_type}"]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
attr_accessor :code_index
|
|
246
|
+
attr_writer :on_demand_inferrer # ->(class_name, method_name, kind) { ... }
|
|
247
|
+
|
|
248
|
+
def initialize(code_index: nil)
|
|
249
|
+
@code_index = code_index
|
|
250
|
+
@instance_methods = {} # { "String" => { "upcase" => MethodEntry } }
|
|
251
|
+
@class_methods = {} # { "File" => { "read" => MethodEntry } }
|
|
252
|
+
@converter = Converter::RBSConverter.new
|
|
253
|
+
@preloaded = false
|
|
254
|
+
@on_demand_inferrer = nil
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Preload stdlib RBS signatures
|
|
258
|
+
# @return [self]
|
|
259
|
+
def preload
|
|
260
|
+
return self if @preloaded
|
|
261
|
+
|
|
262
|
+
load_stdlib_rbs
|
|
263
|
+
@preloaded = true
|
|
264
|
+
self
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Check if preloading is complete
|
|
268
|
+
# @return [Boolean]
|
|
269
|
+
def preloaded?
|
|
270
|
+
@preloaded
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Look up instance method entry
|
|
274
|
+
# Falls back to ancestor chain traversal when code_index is available
|
|
275
|
+
# @param class_name [String] the class name
|
|
276
|
+
# @param method_name [String] the method name
|
|
277
|
+
# @return [MethodEntry, nil] method entry or nil if not found
|
|
278
|
+
def lookup(class_name, method_name)
|
|
279
|
+
result = @instance_methods.dig(class_name, method_name)
|
|
280
|
+
return result if result
|
|
281
|
+
|
|
282
|
+
return nil unless @code_index
|
|
283
|
+
|
|
284
|
+
@code_index.ancestors_of(class_name).each do |ancestor|
|
|
285
|
+
next if ancestor == class_name
|
|
286
|
+
|
|
287
|
+
result = @instance_methods.dig(ancestor, method_name)
|
|
288
|
+
return result if result
|
|
289
|
+
end
|
|
290
|
+
nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Look up class method entry
|
|
294
|
+
# Falls back to ancestor chain traversal when code_index is available
|
|
295
|
+
# @param class_name [String] the class name
|
|
296
|
+
# @param method_name [String] the method name
|
|
297
|
+
# @return [MethodEntry, nil] method entry or nil if not found
|
|
298
|
+
def lookup_class_method(class_name, method_name)
|
|
299
|
+
result = @class_methods.dig(class_name, method_name)
|
|
300
|
+
return result if result
|
|
301
|
+
return nil unless @code_index
|
|
302
|
+
|
|
303
|
+
@code_index.ancestors_of(class_name).each do |ancestor|
|
|
304
|
+
next if ancestor == class_name
|
|
305
|
+
|
|
306
|
+
result = @class_methods.dig(ancestor, method_name)
|
|
307
|
+
return result if result
|
|
308
|
+
end
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Get method return type (convenience method matching old SignatureProvider API)
|
|
313
|
+
# Triggers on-demand inference if the entry has Unguessed return type.
|
|
314
|
+
# @param class_name [String] the class name
|
|
315
|
+
# @param method_name [String] the method name
|
|
316
|
+
# @param arg_types [Array<Types::Type>] argument types for overload matching
|
|
317
|
+
# @return [Types::Type] the return type (Unknown if not found)
|
|
318
|
+
def get_method_return_type(class_name, method_name, arg_types = [])
|
|
319
|
+
entry = lookup(class_name, method_name)
|
|
320
|
+
return Types::Unknown.instance unless entry
|
|
321
|
+
|
|
322
|
+
result = entry.return_type(arg_types)
|
|
323
|
+
if result.is_a?(Types::Unguessed)
|
|
324
|
+
try_on_demand_inference(class_name, method_name, :instance)
|
|
325
|
+
entry = lookup(class_name, method_name)
|
|
326
|
+
return Types::Unknown.instance unless entry
|
|
327
|
+
|
|
328
|
+
result = entry.return_type(arg_types)
|
|
329
|
+
return Types::Unknown.instance if result.is_a?(Types::Unguessed)
|
|
330
|
+
end
|
|
331
|
+
result
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Get class method return type (convenience method matching old SignatureProvider API)
|
|
335
|
+
# Triggers on-demand inference if the entry has Unguessed return type.
|
|
336
|
+
# @param class_name [String] the class name
|
|
337
|
+
# @param method_name [String] the method name
|
|
338
|
+
# @param arg_types [Array<Types::Type>] argument types for overload matching
|
|
339
|
+
# @return [Types::Type] the return type (Unknown if not found)
|
|
340
|
+
def get_class_method_return_type(class_name, method_name, arg_types = [])
|
|
341
|
+
entry = lookup_class_method(class_name, method_name)
|
|
342
|
+
return Types::Unknown.instance unless entry
|
|
343
|
+
|
|
344
|
+
result = entry.return_type(arg_types)
|
|
345
|
+
if result.is_a?(Types::Unguessed)
|
|
346
|
+
try_on_demand_inference(class_name, method_name, :class)
|
|
347
|
+
entry = lookup_class_method(class_name, method_name)
|
|
348
|
+
return Types::Unknown.instance unless entry
|
|
349
|
+
|
|
350
|
+
result = entry.return_type(arg_types)
|
|
351
|
+
return Types::Unknown.instance if result.is_a?(Types::Unguessed)
|
|
352
|
+
end
|
|
353
|
+
result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Get block parameter types for a method
|
|
357
|
+
# @param class_name [String] the receiver class name
|
|
358
|
+
# @param method_name [String] the method name
|
|
359
|
+
# @return [Array<Types::Type>] array of block parameter types (empty if no block)
|
|
360
|
+
def get_block_param_types(class_name, method_name)
|
|
361
|
+
entry = lookup(class_name, method_name)
|
|
362
|
+
return [] unless entry
|
|
363
|
+
|
|
364
|
+
entry.block_param_types
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Get method signatures for hover display
|
|
368
|
+
# @param class_name [String] the class name
|
|
369
|
+
# @param method_name [String] the method name
|
|
370
|
+
# @return [Array<Signature>] wrapped signatures for compatibility
|
|
371
|
+
def get_method_signatures(class_name, method_name)
|
|
372
|
+
entry = lookup(class_name, method_name)
|
|
373
|
+
return [] unless entry
|
|
374
|
+
|
|
375
|
+
entry.signatures.map { |mt| Signature.new(mt) }
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Get class method signatures for hover display
|
|
379
|
+
# @param class_name [String] the class name
|
|
380
|
+
# @param method_name [String] the method name
|
|
381
|
+
# @return [Array<Signature>] wrapped signatures for compatibility
|
|
382
|
+
def get_class_method_signatures(class_name, method_name)
|
|
383
|
+
entry = lookup_class_method(class_name, method_name)
|
|
384
|
+
return [] unless entry
|
|
385
|
+
|
|
386
|
+
entry.signatures.map { |mt| Signature.new(mt) }
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Register a single gem instance method
|
|
390
|
+
# Skips if RBS entry already exists (RBS takes priority).
|
|
391
|
+
# force: true overwrites existing GemMethodEntry (but never MethodEntry/RBS).
|
|
392
|
+
def register_gem_method(class_name, method_name, return_type, params = [], force: false)
|
|
393
|
+
@instance_methods[class_name] ||= {}
|
|
394
|
+
|
|
395
|
+
existing = @instance_methods[class_name][method_name]
|
|
396
|
+
if existing
|
|
397
|
+
return if existing.is_a?(MethodEntry)
|
|
398
|
+
return unless force
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
@instance_methods[class_name][method_name] = GemMethodEntry.new(return_type, params, skip_stdlib_rbs: force)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Register a single gem class method
|
|
405
|
+
# Skips if RBS entry already exists (RBS takes priority).
|
|
406
|
+
# force: true overwrites existing GemMethodEntry (but never MethodEntry/RBS).
|
|
407
|
+
def register_gem_class_method(class_name, method_name, return_type, params = [], force: false)
|
|
408
|
+
@class_methods[class_name] ||= {}
|
|
409
|
+
|
|
410
|
+
existing = @class_methods[class_name][method_name]
|
|
411
|
+
if existing
|
|
412
|
+
return if existing.is_a?(MethodEntry)
|
|
413
|
+
return unless force
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
@class_methods[class_name][method_name] = GemMethodEntry.new(return_type, params, skip_stdlib_rbs: force)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Bulk load gem cache data into the registry
|
|
420
|
+
# @param cache_data [Hash] { class_name => { method_name => { "return_type" => ..., "params" => [...] } } }
|
|
421
|
+
# @param kind [Symbol] :instance or :class
|
|
422
|
+
def load_gem_cache(cache_data, kind: :instance)
|
|
423
|
+
target = kind == :class ? @class_methods : @instance_methods
|
|
424
|
+
|
|
425
|
+
cache_data.each do |class_name, methods|
|
|
426
|
+
target[class_name] ||= {}
|
|
427
|
+
methods.each do |method_name, entry_data|
|
|
428
|
+
next if target[class_name].key?(method_name) # RBS priority
|
|
429
|
+
|
|
430
|
+
return_type = TypeSerializer.deserialize(entry_data["return_type"])
|
|
431
|
+
params = (entry_data["params"] || []).map do |p|
|
|
432
|
+
Types::ParamSignature.new(
|
|
433
|
+
name: p["name"].to_sym,
|
|
434
|
+
kind: p["kind"].to_sym,
|
|
435
|
+
type: TypeSerializer.deserialize(p["type"])
|
|
436
|
+
)
|
|
437
|
+
end
|
|
438
|
+
target[class_name][method_name] = GemMethodEntry.new(return_type, params)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Replace Unguessed GemMethodEntry entries with inferred results.
|
|
444
|
+
# Only replaces entries that are GemMethodEntry with Unguessed return type.
|
|
445
|
+
# @param cache_data [Hash] { class_name => { method_name => { "return_type" => ..., "params" => [...] } } }
|
|
446
|
+
# @param kind [Symbol] :instance or :class
|
|
447
|
+
def replace_unguessed_entries(cache_data, kind: :instance)
|
|
448
|
+
target = kind == :class ? @class_methods : @instance_methods
|
|
449
|
+
|
|
450
|
+
cache_data.each do |class_name, methods|
|
|
451
|
+
next unless target[class_name]
|
|
452
|
+
|
|
453
|
+
methods.each do |method_name, entry_data|
|
|
454
|
+
existing = target[class_name][method_name]
|
|
455
|
+
next unless existing.is_a?(GemMethodEntry)
|
|
456
|
+
next unless existing.return_type.is_a?(Types::Unguessed)
|
|
457
|
+
|
|
458
|
+
return_type = TypeSerializer.deserialize(entry_data["return_type"])
|
|
459
|
+
params = (entry_data["params"] || []).map do |p|
|
|
460
|
+
Types::ParamSignature.new(
|
|
461
|
+
name: p["name"].to_sym,
|
|
462
|
+
kind: p["kind"].to_sym,
|
|
463
|
+
type: TypeSerializer.deserialize(p["type"])
|
|
464
|
+
)
|
|
465
|
+
end
|
|
466
|
+
target[class_name][method_name] = GemMethodEntry.new(return_type, params)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Wrapper for RBS method type (for compatibility with existing code)
|
|
472
|
+
class Signature
|
|
473
|
+
attr_reader :method_type
|
|
474
|
+
|
|
475
|
+
def initialize(method_type)
|
|
476
|
+
@method_type = method_type
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
private def try_on_demand_inference(class_name, method_name, kind)
|
|
481
|
+
return unless @on_demand_inferrer
|
|
482
|
+
|
|
483
|
+
@on_demand_inferrer.call(class_name, method_name, kind)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
private def load_stdlib_rbs
|
|
487
|
+
loader = RBS::EnvironmentLoader.new
|
|
488
|
+
env = RBS::Environment.from_loader(loader).resolve_type_names
|
|
489
|
+
builder = RBS::DefinitionBuilder.new(env: env)
|
|
490
|
+
|
|
491
|
+
# Collect type parameter names for generic classes (e.g., "Set" => [:A])
|
|
492
|
+
# This lets RBSConverter build ClassInstance with named type_params
|
|
493
|
+
class_type_params = {}
|
|
494
|
+
env.class_decls.each do |type_name, entry|
|
|
495
|
+
params = entry.type_params.map(&:name)
|
|
496
|
+
class_type_params[type_name.to_s.delete_prefix("::")] = params if params.any?
|
|
497
|
+
end
|
|
498
|
+
@converter = Converter::RBSConverter.new(class_type_params)
|
|
499
|
+
|
|
500
|
+
env.class_decls.each_key do |type_name|
|
|
501
|
+
load_class_definitions(type_name, builder)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
env.class_alias_decls.each_value do |entry|
|
|
505
|
+
alias_name = entry.decl.new_name.to_s.delete_prefix("::")
|
|
506
|
+
original_name = entry.decl.old_name.to_s.delete_prefix("::")
|
|
507
|
+
@instance_methods[alias_name] ||= @instance_methods[original_name] if @instance_methods[original_name]
|
|
508
|
+
@class_methods[alias_name] ||= @class_methods[original_name] if @class_methods[original_name]
|
|
509
|
+
end
|
|
510
|
+
rescue StandardError => e
|
|
511
|
+
Logger.error("Failed to preload RBS environment", e)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
private def load_class_definitions(type_name, builder)
|
|
515
|
+
class_name = type_name.to_s.delete_prefix("::")
|
|
516
|
+
|
|
517
|
+
# Load instance methods
|
|
518
|
+
begin
|
|
519
|
+
definition = builder.build_instance(type_name)
|
|
520
|
+
@instance_methods[class_name] = build_method_entries(definition)
|
|
521
|
+
rescue RBS::NoTypeFoundError, RBS::NoSuperclassFoundError, RBS::NoMixinFoundError
|
|
522
|
+
# Skip classes with missing dependencies
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Load class methods (singleton)
|
|
526
|
+
begin
|
|
527
|
+
definition = builder.build_singleton(type_name)
|
|
528
|
+
@class_methods[class_name] = build_method_entries(definition)
|
|
529
|
+
rescue RBS::NoTypeFoundError, RBS::NoSuperclassFoundError, RBS::NoMixinFoundError
|
|
530
|
+
# Skip classes with missing dependencies
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
private def build_method_entries(definition)
|
|
535
|
+
# RBS methods hash uses Symbol keys, but we use String keys for lookup
|
|
536
|
+
definition.methods.to_h do |method_name, method_def|
|
|
537
|
+
[method_name.to_s, MethodEntry.new(method_def.method_types, @converter)]
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "types"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
# Builds MethodSignature from DefNode using Resolver for type inference.
|
|
8
|
+
# Extracts the param formatting + type inference logic that was previously
|
|
9
|
+
# embedded in the LSP hover layer, making it reusable across contexts.
|
|
10
|
+
class SignatureBuilder
|
|
11
|
+
def initialize(resolver)
|
|
12
|
+
@resolver = resolver
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param def_node [IR::DefNode] Method definition node
|
|
16
|
+
# @return [Types::MethodSignature] Structured method signature
|
|
17
|
+
def build_from_def_node(def_node)
|
|
18
|
+
params = build_param_signatures(def_node.params)
|
|
19
|
+
return_type = @resolver.infer(def_node).type
|
|
20
|
+
Types::MethodSignature.new(params, return_type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private def build_param_signatures(param_nodes)
|
|
24
|
+
return [] if param_nodes.nil? || param_nodes.empty?
|
|
25
|
+
|
|
26
|
+
param_nodes.map { |p| build_param_signature(p) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private def build_param_signature(param_node)
|
|
30
|
+
type = @resolver.infer(param_node).type
|
|
31
|
+
Types::ParamSignature.new(
|
|
32
|
+
name: param_node.name,
|
|
33
|
+
kind: param_node.kind,
|
|
34
|
+
type: type
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "types"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
# Converts Type objects to/from JSON-compatible Hashes.
|
|
8
|
+
# Used by GemSignatureCache for persisting inferred method signatures.
|
|
9
|
+
module TypeSerializer
|
|
10
|
+
module_function def serialize(type)
|
|
11
|
+
case type
|
|
12
|
+
when Types::Unguessed
|
|
13
|
+
{ "_type" => "Unguessed" }
|
|
14
|
+
when Types::Unknown
|
|
15
|
+
{ "_type" => "Unknown" }
|
|
16
|
+
when Types::ClassInstance
|
|
17
|
+
h = { "_type" => "ClassInstance", "name" => type.name }
|
|
18
|
+
h["type_params"] = type.type_params.to_h { |k, v| [k.to_s, serialize(v)] } if type.type_params&.any?
|
|
19
|
+
h
|
|
20
|
+
when Types::SingletonType
|
|
21
|
+
{ "_type" => "SingletonType", "name" => type.name }
|
|
22
|
+
when Types::TupleType
|
|
23
|
+
{ "_type" => "TupleType", "element_types" => type.element_types.map { |t| serialize(t) } }
|
|
24
|
+
when Types::ArrayType
|
|
25
|
+
{ "_type" => "ArrayType", "element_type" => serialize(type.element_type) }
|
|
26
|
+
when Types::HashShape
|
|
27
|
+
{ "_type" => "HashShape", "fields" => type.fields.transform_keys(&:to_s).transform_values { |v| serialize(v) } }
|
|
28
|
+
when Types::HashType
|
|
29
|
+
{ "_type" => "HashType", "key_type" => serialize(type.key_type), "value_type" => serialize(type.value_type) }
|
|
30
|
+
when Types::RangeType
|
|
31
|
+
{ "_type" => "RangeType", "element_type" => serialize(type.element_type) }
|
|
32
|
+
when Types::Union
|
|
33
|
+
{ "_type" => "Union", "types" => type.types.map { |t| serialize(t) } }
|
|
34
|
+
when Types::TypeVariable
|
|
35
|
+
{ "_type" => "TypeVariable", "name" => type.name.to_s }
|
|
36
|
+
when Types::SelfType
|
|
37
|
+
{ "_type" => "SelfType" }
|
|
38
|
+
when Types::ForwardingArgs
|
|
39
|
+
{ "_type" => "ForwardingArgs" }
|
|
40
|
+
when Types::MethodSignature
|
|
41
|
+
{ "_type" => "MethodSignature",
|
|
42
|
+
"return_type" => serialize(type.return_type),
|
|
43
|
+
"params" => type.params.map { |p| serialize_param(p) } }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Deserialize a Hash back to a Type object
|
|
48
|
+
# @param hash [Hash] JSON-compatible hash with "_type" discriminator
|
|
49
|
+
# @return [Types::Type] The deserialized type
|
|
50
|
+
# @raise [ArgumentError] if "_type" is unknown
|
|
51
|
+
module_function def deserialize(hash)
|
|
52
|
+
case hash["_type"]
|
|
53
|
+
when "Unguessed" then Types::Unguessed.instance
|
|
54
|
+
when "Unknown" then Types::Unknown.instance
|
|
55
|
+
when "ClassInstance"
|
|
56
|
+
type_params = hash["type_params"]&.to_h { |k, v| [k.to_sym, deserialize(v)] }
|
|
57
|
+
Types::ClassInstance.for(hash["name"], type_params)
|
|
58
|
+
when "SingletonType" then Types::SingletonType.new(hash["name"])
|
|
59
|
+
when "ArrayType" then Types::ArrayType.new(deserialize(hash["element_type"]))
|
|
60
|
+
when "TupleType" then Types::TupleType.new(hash["element_types"].map { |t| deserialize(t) })
|
|
61
|
+
when "HashType" then Types::HashType.new(deserialize(hash["key_type"]), deserialize(hash["value_type"]))
|
|
62
|
+
when "RangeType" then Types::RangeType.new(deserialize(hash["element_type"]))
|
|
63
|
+
when "HashShape" then Types::HashShape.new(hash["fields"].to_h { |k, v| [k.to_sym, deserialize(v)] })
|
|
64
|
+
when "Union" then Types::Union.new(hash["types"].map { |t| deserialize(t) })
|
|
65
|
+
when "TypeVariable" then Types::TypeVariable.new(hash["name"].to_sym)
|
|
66
|
+
when "SelfType" then Types::SelfType.instance
|
|
67
|
+
when "ForwardingArgs" then Types::ForwardingArgs.instance
|
|
68
|
+
when "MethodSignature" then deserialize_method_signature(hash)
|
|
69
|
+
else
|
|
70
|
+
raise ArgumentError, "Unknown type: #{hash["_type"]}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param param [Types::ParamSignature]
|
|
75
|
+
# @return [Hash]
|
|
76
|
+
module_function def serialize_param(param)
|
|
77
|
+
{ "name" => param.name.to_s, "kind" => param.kind.to_s, "type" => serialize(param.type) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param hash [Hash]
|
|
81
|
+
# @return [Types::MethodSignature]
|
|
82
|
+
module_function def deserialize_method_signature(hash)
|
|
83
|
+
params = hash["params"].map do |p|
|
|
84
|
+
Types::ParamSignature.new(
|
|
85
|
+
name: p["name"].to_sym,
|
|
86
|
+
kind: p["kind"].to_sym,
|
|
87
|
+
type: deserialize(p["type"])
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
Types::MethodSignature.new(params, deserialize(hash["return_type"]))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private_class_method :serialize_param, :deserialize_method_signature
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|