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,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "rbs"
|
|
5
|
+
require_relative "types"
|
|
6
|
+
require_relative "logger"
|
|
7
|
+
require_relative "converter/rbs_converter"
|
|
8
|
+
|
|
9
|
+
module TypeGuessr
|
|
10
|
+
module Core
|
|
11
|
+
# RBSProvider loads and queries RBS signature information
|
|
12
|
+
# Provides lazy loading of RBS environment for method signatures
|
|
13
|
+
# Uses RBSConverter to convert RBS types to internal type system
|
|
14
|
+
class RBSProvider
|
|
15
|
+
include Singleton
|
|
16
|
+
|
|
17
|
+
# Represents a method signature from RBS
|
|
18
|
+
class Signature
|
|
19
|
+
attr_reader :method_type
|
|
20
|
+
|
|
21
|
+
def initialize(method_type)
|
|
22
|
+
@method_type = method_type
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@env = nil
|
|
28
|
+
@loader = nil
|
|
29
|
+
@converter = Converter::RBSConverter.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Preload RBS environment (for eager loading during addon activation)
|
|
33
|
+
# @return [self]
|
|
34
|
+
def preload
|
|
35
|
+
ensure_environment_loaded
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get method signatures for a class and method name
|
|
40
|
+
# @param class_name [String] the class name
|
|
41
|
+
# @param method_name [String] the method name
|
|
42
|
+
# @return [Array<Signature>] array of method signatures
|
|
43
|
+
def get_method_signatures(class_name, method_name)
|
|
44
|
+
ensure_environment_loaded
|
|
45
|
+
|
|
46
|
+
# Build the type name
|
|
47
|
+
type_name = build_type_name(class_name)
|
|
48
|
+
|
|
49
|
+
# Check if class exists in RBS before building (avoids RuntimeError for project classes)
|
|
50
|
+
return [] unless @env.class_decl?(type_name)
|
|
51
|
+
|
|
52
|
+
# Use RBS::DefinitionBuilder to get method definitions
|
|
53
|
+
builder = RBS::DefinitionBuilder.new(env: @env)
|
|
54
|
+
definition = builder.build_instance(type_name)
|
|
55
|
+
|
|
56
|
+
# Get the method definition
|
|
57
|
+
method_def = definition.methods[method_name.to_sym]
|
|
58
|
+
return [] unless method_def
|
|
59
|
+
|
|
60
|
+
# Return all method types (overloads)
|
|
61
|
+
method_def.method_types.map { |mt| Signature.new(mt) }
|
|
62
|
+
rescue RBS::NoTypeFoundError, RBS::NoSuperclassFoundError, RBS::NoMixinFoundError => _e
|
|
63
|
+
# Class not found in RBS
|
|
64
|
+
[]
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
# If anything goes wrong, return empty array
|
|
67
|
+
Logger.error("RBSProvider error", e)
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the return type of a method call with overload resolution
|
|
72
|
+
# @param class_name [String] the receiver class name
|
|
73
|
+
# @param method_name [String] the method name
|
|
74
|
+
# @param arg_types [Array<Types::Type>] argument types for overload matching
|
|
75
|
+
# @return [Types::Type] the return type (Unknown if not found)
|
|
76
|
+
def get_method_return_type(class_name, method_name, arg_types = [])
|
|
77
|
+
signatures = get_method_signatures(class_name, method_name)
|
|
78
|
+
return Types::Unknown.instance if signatures.empty?
|
|
79
|
+
|
|
80
|
+
# Find best matching overload based on argument types
|
|
81
|
+
best_match = find_best_overload(signatures, arg_types)
|
|
82
|
+
return_type = best_match.method_type.type.return_type
|
|
83
|
+
|
|
84
|
+
@converter.convert(return_type)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get block parameter types for a method
|
|
88
|
+
# @param class_name [String] the receiver class name
|
|
89
|
+
# @param method_name [String] the method name
|
|
90
|
+
# @return [Array<Types::Type>] array of block parameter types (empty if no block)
|
|
91
|
+
def get_block_param_types(class_name, method_name)
|
|
92
|
+
block_sig = find_block_signature(class_name, method_name)
|
|
93
|
+
return [] unless block_sig
|
|
94
|
+
|
|
95
|
+
extract_block_param_types(block_sig)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get class method signatures (singleton methods like File.read, Array.new)
|
|
99
|
+
# @param class_name [String] the class name
|
|
100
|
+
# @param method_name [String] the method name
|
|
101
|
+
# @return [Array<Signature>] array of method signatures
|
|
102
|
+
def get_class_method_signatures(class_name, method_name)
|
|
103
|
+
ensure_environment_loaded
|
|
104
|
+
|
|
105
|
+
# Build the type name
|
|
106
|
+
type_name = build_type_name(class_name)
|
|
107
|
+
|
|
108
|
+
# Check if class exists in RBS before building (avoids RuntimeError for project classes)
|
|
109
|
+
return [] unless @env.class_decl?(type_name)
|
|
110
|
+
|
|
111
|
+
# Use RBS::DefinitionBuilder to get singleton method definitions
|
|
112
|
+
builder = RBS::DefinitionBuilder.new(env: @env)
|
|
113
|
+
definition = builder.build_singleton(type_name)
|
|
114
|
+
|
|
115
|
+
# Get the method definition
|
|
116
|
+
method_def = definition.methods[method_name.to_sym]
|
|
117
|
+
return [] unless method_def
|
|
118
|
+
|
|
119
|
+
# Return all method types (overloads)
|
|
120
|
+
method_def.method_types.map { |mt| Signature.new(mt) }
|
|
121
|
+
rescue RBS::NoTypeFoundError, RBS::NoSuperclassFoundError, RBS::NoMixinFoundError => _e
|
|
122
|
+
# Class not found in RBS
|
|
123
|
+
[]
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
# If anything goes wrong, return empty array
|
|
126
|
+
Logger.error("RBSProvider class method error", e)
|
|
127
|
+
[]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get the return type of a class method call
|
|
131
|
+
# @param class_name [String] the class name
|
|
132
|
+
# @param method_name [String] the method name
|
|
133
|
+
# @param arg_types [Array<Types::Type>] the argument types
|
|
134
|
+
# @return [Types::Type] the return type (Unknown if not found)
|
|
135
|
+
def get_class_method_return_type(class_name, method_name, arg_types = [])
|
|
136
|
+
signatures = get_class_method_signatures(class_name, method_name)
|
|
137
|
+
return Types::Unknown.instance if signatures.empty?
|
|
138
|
+
|
|
139
|
+
# Find best matching overload based on argument types
|
|
140
|
+
best_match = find_best_overload(signatures, arg_types)
|
|
141
|
+
return_type = best_match.method_type.type.return_type
|
|
142
|
+
|
|
143
|
+
@converter.convert(return_type)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Find a method signature that has a block
|
|
149
|
+
# @param class_name [String] the receiver class name
|
|
150
|
+
# @param method_name [String] the method name
|
|
151
|
+
# @return [RBS::MethodType, nil] the method type with block, or nil
|
|
152
|
+
def find_block_signature(class_name, method_name)
|
|
153
|
+
signatures = get_method_signatures(class_name, method_name)
|
|
154
|
+
return nil if signatures.empty?
|
|
155
|
+
|
|
156
|
+
# Find the signature with a block
|
|
157
|
+
sig_with_block = signatures.find { |s| s.method_type.block }
|
|
158
|
+
sig_with_block&.method_type
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Extract block parameter types from a method type
|
|
162
|
+
# @param method_type [RBS::MethodType] the method type
|
|
163
|
+
# @param substitutions [Hash{Symbol => Types::Type}] type variable substitutions (e.g., { Elem: Integer })
|
|
164
|
+
# @return [Array<Types::Type>] array of parameter types
|
|
165
|
+
def extract_block_param_types(method_type, substitutions: {})
|
|
166
|
+
return [] unless method_type.block
|
|
167
|
+
|
|
168
|
+
block_func = method_type.block.type
|
|
169
|
+
block_func.required_positionals.flat_map do |param|
|
|
170
|
+
# Handle Tuple types (e.g., [K, V] in Hash#each) by flattening
|
|
171
|
+
raw_types = if param.type.is_a?(RBS::Types::Tuple)
|
|
172
|
+
param.type.types.map { |t| @converter.convert(t) }
|
|
173
|
+
else
|
|
174
|
+
[@converter.convert(param.type)]
|
|
175
|
+
end
|
|
176
|
+
# Apply substitutions after conversion
|
|
177
|
+
raw_types.map { |t| t.substitute(substitutions) }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def ensure_environment_loaded
|
|
182
|
+
return if @env
|
|
183
|
+
|
|
184
|
+
@loader = RBS::EnvironmentLoader.new
|
|
185
|
+
|
|
186
|
+
# Add optional library paths if they exist
|
|
187
|
+
# loader.add(path: Pathname("sig")) if Dir.exist?("sig")
|
|
188
|
+
|
|
189
|
+
# Load environment (this automatically loads core/stdlib)
|
|
190
|
+
@env = RBS::Environment.from_loader(@loader).resolve_type_names
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
Logger.error("Failed to load RBS environment", e)
|
|
193
|
+
# Fallback to empty environment
|
|
194
|
+
@env = RBS::Environment.new
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def build_type_name(class_name)
|
|
198
|
+
# Parse class name to handle namespaced classes like "Foo::Bar"
|
|
199
|
+
parts = class_name.split("::")
|
|
200
|
+
namespace_parts = parts[0...-1]
|
|
201
|
+
name = parts.last
|
|
202
|
+
|
|
203
|
+
namespace = if namespace_parts.empty?
|
|
204
|
+
RBS::Namespace.root
|
|
205
|
+
else
|
|
206
|
+
RBS::Namespace.parse(namespace_parts.join("::"))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
RBS::TypeName.new(name: name.to_sym, namespace: namespace)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Find the best matching overload for given argument types
|
|
213
|
+
# @param signatures [Array<Signature>] available overloads
|
|
214
|
+
# @param arg_types [Array<Types::Type>] argument types
|
|
215
|
+
# @return [Signature] best matching signature (first if no match)
|
|
216
|
+
def find_best_overload(signatures, arg_types)
|
|
217
|
+
return signatures.first if arg_types.empty?
|
|
218
|
+
|
|
219
|
+
# Score each overload
|
|
220
|
+
scored = signatures.map do |sig|
|
|
221
|
+
score = calculate_overload_score(sig.method_type, arg_types)
|
|
222
|
+
[sig, score]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Return best scoring overload, or first if all scores are 0
|
|
226
|
+
best = scored.max_by { |_sig, score| score }
|
|
227
|
+
best[1].positive? ? best[0] : signatures.first
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Calculate match score for an overload
|
|
231
|
+
# @param method_type [RBS::MethodType] the method type
|
|
232
|
+
# @param arg_types [Array<Types::Type>] argument types
|
|
233
|
+
# @return [Integer] score (higher = better match)
|
|
234
|
+
def calculate_overload_score(method_type, arg_types)
|
|
235
|
+
func = method_type.type
|
|
236
|
+
required = func.required_positionals
|
|
237
|
+
optional = func.optional_positionals
|
|
238
|
+
rest = func.rest_positionals
|
|
239
|
+
|
|
240
|
+
# Check argument count
|
|
241
|
+
min_args = required.size
|
|
242
|
+
max_args = rest ? Float::INFINITY : required.size + optional.size
|
|
243
|
+
return 0 unless arg_types.size.between?(min_args, max_args)
|
|
244
|
+
|
|
245
|
+
# Score each argument match
|
|
246
|
+
score = 0
|
|
247
|
+
arg_types.each_with_index do |arg_type, i|
|
|
248
|
+
param = if i < required.size
|
|
249
|
+
required[i]
|
|
250
|
+
elsif i < required.size + optional.size
|
|
251
|
+
optional[i - required.size]
|
|
252
|
+
else
|
|
253
|
+
rest
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
break unless param
|
|
257
|
+
|
|
258
|
+
score += type_match_score(arg_type, param.type)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
score
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Calculate match score between our type and RBS parameter type
|
|
265
|
+
# @param our_type [Types::Type] our type representation
|
|
266
|
+
# @param rbs_type [RBS::Types::t] RBS type
|
|
267
|
+
# @return [Integer] score (0 = no match, 1 = weak match, 2 = exact match)
|
|
268
|
+
def type_match_score(our_type, rbs_type)
|
|
269
|
+
case rbs_type
|
|
270
|
+
when RBS::Types::ClassInstance
|
|
271
|
+
# Exact class match
|
|
272
|
+
class_name = rbs_type.name.to_s.delete_prefix("::")
|
|
273
|
+
return 2 if types_match_class?(our_type, class_name)
|
|
274
|
+
|
|
275
|
+
0
|
|
276
|
+
when RBS::Types::Union
|
|
277
|
+
# Check if our type matches any member
|
|
278
|
+
max_score = rbs_type.types.map { |t| type_match_score(our_type, t) }.max || 0
|
|
279
|
+
max_score.positive? ? 1 : 0
|
|
280
|
+
else
|
|
281
|
+
# Unknown RBS type - give weak match to avoid penalizing
|
|
282
|
+
1
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Check if our type matches a class name
|
|
287
|
+
# @param our_type [Types::Type] our type
|
|
288
|
+
# @param class_name [String] class name to match
|
|
289
|
+
# @return [Boolean] true if types match
|
|
290
|
+
def types_match_class?(our_type, class_name)
|
|
291
|
+
case our_type
|
|
292
|
+
when Types::ClassInstance
|
|
293
|
+
our_type.name == class_name
|
|
294
|
+
when Types::ArrayType
|
|
295
|
+
class_name == "Array"
|
|
296
|
+
when Types::HashShape
|
|
297
|
+
class_name == "Hash"
|
|
298
|
+
else
|
|
299
|
+
false
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Registry
|
|
6
|
+
# Stores and retrieves project method definitions
|
|
7
|
+
# Supports inheritance chain traversal when ancestry_provider is set
|
|
8
|
+
class MethodRegistry
|
|
9
|
+
# Callback for getting class ancestors
|
|
10
|
+
# @return [Proc, nil] A proc that takes class_name and returns array of ancestor names
|
|
11
|
+
attr_accessor :ancestry_provider
|
|
12
|
+
|
|
13
|
+
# @param ancestry_provider [Proc, nil] Returns ancestors for inheritance lookup
|
|
14
|
+
def initialize(ancestry_provider: nil)
|
|
15
|
+
@methods = {} # { "ClassName" => { "method_name" => DefNode } }
|
|
16
|
+
@ancestry_provider = ancestry_provider
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Register a method definition
|
|
20
|
+
# @param class_name [String] Class name (empty string for top-level)
|
|
21
|
+
# @param method_name [String] Method name
|
|
22
|
+
# @param def_node [IR::DefNode] Method definition node
|
|
23
|
+
def register(class_name, method_name, def_node)
|
|
24
|
+
@methods[class_name] ||= {}
|
|
25
|
+
@methods[class_name][method_name] = def_node
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Look up a method definition (with inheritance chain traversal)
|
|
29
|
+
# @param class_name [String] Class name
|
|
30
|
+
# @param method_name [String] Method name
|
|
31
|
+
# @return [IR::DefNode, nil] Method definition node or nil
|
|
32
|
+
def lookup(class_name, method_name)
|
|
33
|
+
# Try current class first
|
|
34
|
+
result = @methods.dig(class_name, method_name)
|
|
35
|
+
return result if result
|
|
36
|
+
|
|
37
|
+
# Traverse ancestor chain if provider available
|
|
38
|
+
return nil unless @ancestry_provider
|
|
39
|
+
|
|
40
|
+
ancestors = @ancestry_provider.call(class_name)
|
|
41
|
+
ancestors.each do |ancestor_name|
|
|
42
|
+
next if ancestor_name == class_name # Skip self
|
|
43
|
+
|
|
44
|
+
result = @methods.dig(ancestor_name, method_name)
|
|
45
|
+
return result if result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get all registered class names
|
|
52
|
+
# @return [Array<String>] List of class names (frozen)
|
|
53
|
+
def registered_classes
|
|
54
|
+
@methods.keys.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get all methods for a specific class (direct methods only)
|
|
58
|
+
# @param class_name [String] Class name
|
|
59
|
+
# @return [Hash<String, IR::DefNode>] Methods hash (frozen)
|
|
60
|
+
def methods_for_class(class_name)
|
|
61
|
+
(@methods[class_name] || {}).freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Search for methods matching a pattern
|
|
65
|
+
# @param pattern [String] Search pattern (partial match on "ClassName#method_name")
|
|
66
|
+
# @return [Array<Array>] Array of [class_name, method_name, def_node]
|
|
67
|
+
def search(pattern)
|
|
68
|
+
results = []
|
|
69
|
+
@methods.each do |class_name, methods|
|
|
70
|
+
methods.each do |method_name, def_node|
|
|
71
|
+
full_name = "#{class_name}##{method_name}"
|
|
72
|
+
results << [class_name, method_name, def_node] if full_name.include?(pattern)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
results
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get all methods available on a class (including inherited)
|
|
79
|
+
# @param class_name [String]
|
|
80
|
+
# @return [Set<String>] Method names
|
|
81
|
+
def all_methods_for_class(class_name)
|
|
82
|
+
# Start with directly defined methods
|
|
83
|
+
class_methods = (@methods[class_name]&.keys || []).to_set
|
|
84
|
+
|
|
85
|
+
# Add inherited methods if ancestry_provider is available
|
|
86
|
+
return class_methods unless @ancestry_provider
|
|
87
|
+
|
|
88
|
+
ancestors = @ancestry_provider.call(class_name)
|
|
89
|
+
ancestors.each do |ancestor_name|
|
|
90
|
+
next if ancestor_name == class_name # Skip self
|
|
91
|
+
|
|
92
|
+
ancestor_methods = @methods[ancestor_name]&.keys || []
|
|
93
|
+
class_methods.merge(ancestor_methods)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class_methods
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Clear all registered methods
|
|
100
|
+
def clear
|
|
101
|
+
@methods.clear
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Registry
|
|
6
|
+
# Stores and retrieves instance/class variable write nodes
|
|
7
|
+
# Supports inheritance chain traversal for instance variables when ancestry_provider is set
|
|
8
|
+
class VariableRegistry
|
|
9
|
+
# Callback for getting class ancestors
|
|
10
|
+
# @return [Proc, nil] A proc that takes class_name and returns array of ancestor names
|
|
11
|
+
attr_accessor :ancestry_provider
|
|
12
|
+
|
|
13
|
+
# @param ancestry_provider [Proc, nil] Returns ancestors for inheritance lookup
|
|
14
|
+
def initialize(ancestry_provider: nil)
|
|
15
|
+
@instance_variables = {} # { "ClassName" => { :@name => WriteNode } }
|
|
16
|
+
@class_variables = {} # { "ClassName" => { :@@name => WriteNode } }
|
|
17
|
+
@ancestry_provider = ancestry_provider
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Register an instance variable write
|
|
21
|
+
# @param class_name [String] Class name
|
|
22
|
+
# @param name [Symbol] Variable name (e.g., :@recipe)
|
|
23
|
+
# @param write_node [IR::InstanceVariableWriteNode]
|
|
24
|
+
def register_instance_variable(class_name, name, write_node)
|
|
25
|
+
return unless class_name
|
|
26
|
+
|
|
27
|
+
@instance_variables[class_name] ||= {}
|
|
28
|
+
# First write wins (preserves consistent behavior)
|
|
29
|
+
@instance_variables[class_name][name] ||= write_node
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Look up an instance variable write (with inheritance chain traversal)
|
|
33
|
+
# @param class_name [String]
|
|
34
|
+
# @param name [Symbol]
|
|
35
|
+
# @return [IR::InstanceVariableWriteNode, nil]
|
|
36
|
+
def lookup_instance_variable(class_name, name)
|
|
37
|
+
return nil unless class_name
|
|
38
|
+
|
|
39
|
+
# Try current class first
|
|
40
|
+
result = @instance_variables.dig(class_name, name)
|
|
41
|
+
return result if result
|
|
42
|
+
|
|
43
|
+
# Traverse ancestor chain if provider available
|
|
44
|
+
return nil unless @ancestry_provider
|
|
45
|
+
|
|
46
|
+
ancestors = @ancestry_provider.call(class_name)
|
|
47
|
+
ancestors.each do |ancestor_name|
|
|
48
|
+
next if ancestor_name == class_name # Skip self
|
|
49
|
+
|
|
50
|
+
result = @instance_variables.dig(ancestor_name, name)
|
|
51
|
+
return result if result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Register a class variable write
|
|
58
|
+
# @param class_name [String]
|
|
59
|
+
# @param name [Symbol] (e.g., :@@count)
|
|
60
|
+
# @param write_node [IR::ClassVariableWriteNode]
|
|
61
|
+
def register_class_variable(class_name, name, write_node)
|
|
62
|
+
return unless class_name
|
|
63
|
+
|
|
64
|
+
@class_variables[class_name] ||= {}
|
|
65
|
+
# First write wins
|
|
66
|
+
@class_variables[class_name][name] ||= write_node
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Look up a class variable write
|
|
70
|
+
# @param class_name [String]
|
|
71
|
+
# @param name [Symbol]
|
|
72
|
+
# @return [IR::ClassVariableWriteNode, nil]
|
|
73
|
+
def lookup_class_variable(class_name, name)
|
|
74
|
+
return nil unless class_name
|
|
75
|
+
|
|
76
|
+
@class_variables.dig(class_name, name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Clear all registered variables
|
|
80
|
+
def clear
|
|
81
|
+
@instance_variables.clear
|
|
82
|
+
@class_variables.clear
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "types"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
# SignatureProvider aggregates multiple type sources for method signature lookups
|
|
8
|
+
# Uses priority-based resolution: first non-Unknown result wins
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# provider = SignatureProvider.new
|
|
12
|
+
# provider.add_provider(ProjectRBSProvider.new, priority: :high)
|
|
13
|
+
# provider.add_provider(RBSProvider.instance)
|
|
14
|
+
#
|
|
15
|
+
# return_type = provider.get_method_return_type("String", "upcase")
|
|
16
|
+
class SignatureProvider
|
|
17
|
+
# @param providers [Array<#get_method_return_type>] Providers in priority order (first = highest)
|
|
18
|
+
def initialize(providers = [])
|
|
19
|
+
@providers = providers.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add a provider to the chain
|
|
23
|
+
# @param provider [#get_method_return_type] Provider implementing the signature protocol
|
|
24
|
+
# @param priority [:high, :low] Priority level (:high = first, :low = last)
|
|
25
|
+
def add_provider(provider, priority: :low)
|
|
26
|
+
case priority
|
|
27
|
+
when :high
|
|
28
|
+
@providers.unshift(provider)
|
|
29
|
+
when :low
|
|
30
|
+
@providers.push(provider)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get instance method return type with overload resolution
|
|
35
|
+
# @param class_name [String] Class name (e.g., "String", "Array")
|
|
36
|
+
# @param method_name [String] Method name (e.g., "upcase", "map")
|
|
37
|
+
# @param arg_types [Array<Types::Type>] Argument types for overload matching
|
|
38
|
+
# @return [Types::Type] Return type (Unknown if not found in any provider)
|
|
39
|
+
def get_method_return_type(class_name, method_name, arg_types = [])
|
|
40
|
+
@providers.each do |provider|
|
|
41
|
+
result = provider.get_method_return_type(class_name, method_name, arg_types)
|
|
42
|
+
return result unless result.is_a?(Types::Unknown)
|
|
43
|
+
end
|
|
44
|
+
Types::Unknown.instance
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get class method return type (e.g., File.read, Array.new)
|
|
48
|
+
# @param class_name [String] Class name
|
|
49
|
+
# @param method_name [String] Method name
|
|
50
|
+
# @param arg_types [Array<Types::Type>] Argument types for overload matching
|
|
51
|
+
# @return [Types::Type] Return type (Unknown if not found in any provider)
|
|
52
|
+
def get_class_method_return_type(class_name, method_name, arg_types = [])
|
|
53
|
+
@providers.each do |provider|
|
|
54
|
+
result = provider.get_class_method_return_type(class_name, method_name, arg_types)
|
|
55
|
+
return result unless result.is_a?(Types::Unknown)
|
|
56
|
+
end
|
|
57
|
+
Types::Unknown.instance
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get block parameter types for a method
|
|
61
|
+
# @param class_name [String] Class name
|
|
62
|
+
# @param method_name [String] Method name
|
|
63
|
+
# @return [Array<Types::Type>] Block parameter types (empty if no block or not found)
|
|
64
|
+
def get_block_param_types(class_name, method_name)
|
|
65
|
+
@providers.each do |provider|
|
|
66
|
+
result = provider.get_block_param_types(class_name, method_name)
|
|
67
|
+
return result unless result.empty?
|
|
68
|
+
end
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get method signatures for hover display
|
|
73
|
+
# @param class_name [String] Class name
|
|
74
|
+
# @param method_name [String] Method name
|
|
75
|
+
# @return [Array<Signature>] Method signatures (empty if not found)
|
|
76
|
+
def get_method_signatures(class_name, method_name)
|
|
77
|
+
@providers.each do |provider|
|
|
78
|
+
next unless provider.respond_to?(:get_method_signatures)
|
|
79
|
+
|
|
80
|
+
result = provider.get_method_signatures(class_name, method_name)
|
|
81
|
+
return result unless result.empty?
|
|
82
|
+
end
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get class method signatures for hover display (e.g., File.exist?, Dir.pwd)
|
|
87
|
+
# @param class_name [String] Class name
|
|
88
|
+
# @param method_name [String] Method name
|
|
89
|
+
# @return [Array<Signature>] Method signatures (empty if not found)
|
|
90
|
+
def get_class_method_signatures(class_name, method_name)
|
|
91
|
+
@providers.each do |provider|
|
|
92
|
+
next unless provider.respond_to?(:get_class_method_signatures)
|
|
93
|
+
|
|
94
|
+
result = provider.get_class_method_signatures(class_name, method_name)
|
|
95
|
+
return result unless result.empty?
|
|
96
|
+
end
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "types"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
# Simplifies types by unwrapping single-element unions and
|
|
8
|
+
# unifying parent/child class relationships
|
|
9
|
+
class TypeSimplifier
|
|
10
|
+
# @param ancestry_provider [Proc, nil] A proc that takes class_name and returns array of ancestor names
|
|
11
|
+
def initialize(ancestry_provider: nil)
|
|
12
|
+
@ancestry_provider = ancestry_provider
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Simplify a type
|
|
16
|
+
# @param type [Types::Type] The type to simplify
|
|
17
|
+
# @return [Types::Type] The simplified type
|
|
18
|
+
def simplify(type)
|
|
19
|
+
case type
|
|
20
|
+
when Types::Union
|
|
21
|
+
simplify_union(type)
|
|
22
|
+
else
|
|
23
|
+
type
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def simplify_union(union)
|
|
30
|
+
types = union.types
|
|
31
|
+
|
|
32
|
+
# 1. Single element: unwrap
|
|
33
|
+
return types.first if types.size == 1
|
|
34
|
+
|
|
35
|
+
# 2. Filter to most general types (remove children when parent is present)
|
|
36
|
+
types = filter_to_most_general_types(types) if @ancestry_provider
|
|
37
|
+
|
|
38
|
+
# 3. Check again after filtering
|
|
39
|
+
return types.first if types.size == 1
|
|
40
|
+
|
|
41
|
+
# 4. Multiple elements remain: create new Union
|
|
42
|
+
Types::Union.new(types)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Filter out types whose ancestor is also in the list
|
|
46
|
+
# @param types [Array<Types::Type>] List of types
|
|
47
|
+
# @return [Array<Types::Type>] Filtered list with only the most general types
|
|
48
|
+
def filter_to_most_general_types(types)
|
|
49
|
+
# Extract class names from ClassInstance types
|
|
50
|
+
class_names = types.filter_map do |t|
|
|
51
|
+
t.name if t.is_a?(Types::ClassInstance)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
types.reject do |type|
|
|
55
|
+
next false unless type.is_a?(Types::ClassInstance)
|
|
56
|
+
|
|
57
|
+
ancestors = @ancestry_provider.call(type.name)
|
|
58
|
+
# Check if any ancestor (excluding self) is also in the list
|
|
59
|
+
ancestors.any? { |ancestor| ancestor != type.name && class_names.include?(ancestor) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|