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,506 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../../type_guessr/core/converter/prism_converter"
|
|
5
|
+
require_relative "../../type_guessr/core/index/location_index"
|
|
6
|
+
require_relative "../../type_guessr/core/registry/method_registry"
|
|
7
|
+
require_relative "../../type_guessr/core/registry/variable_registry"
|
|
8
|
+
require_relative "../../type_guessr/core/inference/resolver"
|
|
9
|
+
require_relative "../../type_guessr/core/signature_provider"
|
|
10
|
+
require_relative "../../type_guessr/core/rbs_provider"
|
|
11
|
+
require_relative "../../type_guessr/core/type_simplifier"
|
|
12
|
+
require_relative "type_inferrer"
|
|
13
|
+
|
|
14
|
+
module RubyLsp
|
|
15
|
+
module TypeGuessr
|
|
16
|
+
# RuntimeAdapter manages the IR graph and inference for TypeGuessr
|
|
17
|
+
# Converts files to IR graphs and provides type inference
|
|
18
|
+
class RuntimeAdapter
|
|
19
|
+
attr_reader :signature_provider
|
|
20
|
+
|
|
21
|
+
def initialize(global_state, message_queue = nil)
|
|
22
|
+
@global_state = global_state
|
|
23
|
+
@message_queue = message_queue
|
|
24
|
+
@converter = ::TypeGuessr::Core::Converter::PrismConverter.new
|
|
25
|
+
@location_index = ::TypeGuessr::Core::Index::LocationIndex.new
|
|
26
|
+
@signature_provider = build_signature_provider
|
|
27
|
+
@indexing_completed = false
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@original_type_inferrer = nil
|
|
30
|
+
|
|
31
|
+
# Create shared ancestry provider
|
|
32
|
+
ancestry_provider = ->(class_name) { get_class_ancestors(class_name) }
|
|
33
|
+
|
|
34
|
+
# Create method registry with ancestry provider
|
|
35
|
+
@method_registry = ::TypeGuessr::Core::Registry::MethodRegistry.new(
|
|
36
|
+
ancestry_provider: ancestry_provider
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Create variable registry with ancestry provider
|
|
40
|
+
@variable_registry = ::TypeGuessr::Core::Registry::VariableRegistry.new(
|
|
41
|
+
ancestry_provider: ancestry_provider
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Create resolver with injected registries
|
|
45
|
+
@resolver = ::TypeGuessr::Core::Inference::Resolver.new(
|
|
46
|
+
@signature_provider,
|
|
47
|
+
method_registry: @method_registry,
|
|
48
|
+
variable_registry: @variable_registry
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Set up method list resolver callback
|
|
52
|
+
@resolver.method_list_resolver = ->(methods) { resolve_methods_to_class(methods) }
|
|
53
|
+
|
|
54
|
+
# Set up ancestry provider callback
|
|
55
|
+
@resolver.ancestry_provider = ancestry_provider
|
|
56
|
+
|
|
57
|
+
# Set up constant kind provider callback
|
|
58
|
+
@resolver.constant_kind_provider = ->(name) { get_constant_kind(name) }
|
|
59
|
+
|
|
60
|
+
# Set up class method lookup provider callback
|
|
61
|
+
@resolver.class_method_lookup_provider = lambda do |class_name, method_name|
|
|
62
|
+
lookup_class_method_owner(class_name, method_name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Set up type simplifier with ancestry provider
|
|
66
|
+
@resolver.type_simplifier = ::TypeGuessr::Core::TypeSimplifier.new(
|
|
67
|
+
ancestry_provider: ancestry_provider
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Swap ruby-lsp's TypeInferrer with TypeGuessr's custom implementation
|
|
72
|
+
# This enhances Go to Definition and other features with heuristic type inference
|
|
73
|
+
def swap_type_inferrer
|
|
74
|
+
return unless @global_state.respond_to?(:type_inferrer)
|
|
75
|
+
|
|
76
|
+
@original_type_inferrer = @global_state.type_inferrer
|
|
77
|
+
custom_inferrer = TypeInferrer.new(@global_state.index, self)
|
|
78
|
+
@global_state.instance_variable_set(:@type_inferrer, custom_inferrer)
|
|
79
|
+
log_message("TypeInferrer swapped for enhanced type inference")
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
log_message("Failed to swap TypeInferrer: #{e.message}")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Restore the original TypeInferrer
|
|
85
|
+
def restore_type_inferrer
|
|
86
|
+
return unless @original_type_inferrer
|
|
87
|
+
|
|
88
|
+
@global_state.instance_variable_set(:@type_inferrer, @original_type_inferrer)
|
|
89
|
+
@original_type_inferrer = nil
|
|
90
|
+
log_message("TypeInferrer restored")
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
log_message("Failed to restore TypeInferrer: #{e.message}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Index a file by converting its Prism AST to IR graph
|
|
96
|
+
# @param uri [URI::Generic] File URI
|
|
97
|
+
# @param document [RubyLsp::Document] Document to index
|
|
98
|
+
def index_file(uri, document)
|
|
99
|
+
file_path = uri.to_standardized_path
|
|
100
|
+
return unless file_path
|
|
101
|
+
|
|
102
|
+
# Parse and convert to IR outside mutex
|
|
103
|
+
parsed = document.parse_result
|
|
104
|
+
return unless parsed.value
|
|
105
|
+
|
|
106
|
+
# Create a shared context for all statements
|
|
107
|
+
context = ::TypeGuessr::Core::Converter::PrismConverter::Context.new
|
|
108
|
+
nodes = parsed.value.statements&.body&.filter_map do |stmt|
|
|
109
|
+
@converter.convert(stmt, context)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
# Clear existing index for this file
|
|
114
|
+
@location_index.remove_file(file_path)
|
|
115
|
+
@resolver.clear_cache
|
|
116
|
+
|
|
117
|
+
# Index all nodes recursively with scope tracking
|
|
118
|
+
nodes&.each { |node| index_node_recursively(file_path, node, "") }
|
|
119
|
+
|
|
120
|
+
# Finalize the index for efficient lookups
|
|
121
|
+
@location_index.finalize!
|
|
122
|
+
end
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
log_message("Error in index_file #{uri}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Index source code directly (for testing)
|
|
128
|
+
# @param uri_string [String] File URI as string
|
|
129
|
+
# @param source [String] Source code to index
|
|
130
|
+
def index_source(uri_string, source)
|
|
131
|
+
require "uri"
|
|
132
|
+
uri = URI(uri_string)
|
|
133
|
+
file_path = uri.respond_to?(:to_standardized_path) ? uri.to_standardized_path : uri.path
|
|
134
|
+
file_path ||= uri_string.sub(%r{^file://}, "")
|
|
135
|
+
return unless file_path
|
|
136
|
+
|
|
137
|
+
@mutex.synchronize do
|
|
138
|
+
# Clear existing index for this file
|
|
139
|
+
@location_index.remove_file(file_path)
|
|
140
|
+
@resolver.clear_cache
|
|
141
|
+
|
|
142
|
+
# Parse source code
|
|
143
|
+
parsed = Prism.parse(source)
|
|
144
|
+
return unless parsed.value
|
|
145
|
+
|
|
146
|
+
# Create a shared context for all statements
|
|
147
|
+
context = ::TypeGuessr::Core::Converter::PrismConverter::Context.new
|
|
148
|
+
|
|
149
|
+
# Convert statements to IR nodes and index with scope tracking
|
|
150
|
+
parsed.value.statements&.body&.each do |stmt|
|
|
151
|
+
node = @converter.convert(stmt, context)
|
|
152
|
+
index_node_recursively(file_path, node, "") if node
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Finalize the index for efficient lookups
|
|
156
|
+
@location_index.finalize!
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Find IR node by its unique key
|
|
161
|
+
# @param node_key [String] The node key (scope_id:node_hash)
|
|
162
|
+
# @return [TypeGuessr::Core::IR::Node, nil] IR node or nil if not found
|
|
163
|
+
def find_node_by_key(node_key)
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
@location_index.find_by_key(node_key)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Infer type for an IR node
|
|
170
|
+
# @param node [TypeGuessr::Core::IR::Node] IR node
|
|
171
|
+
# @return [TypeGuessr::Core::Inference::Result] Inference result
|
|
172
|
+
def infer_type(node)
|
|
173
|
+
@mutex.synchronize do
|
|
174
|
+
@resolver.infer(node)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Look up a method definition by class name and method name
|
|
179
|
+
# @param class_name [String] Class name (e.g., "User", "Admin::User")
|
|
180
|
+
# @param method_name [String] Method name (e.g., "initialize", "save")
|
|
181
|
+
# @return [TypeGuessr::Core::IR::DefNode, nil] DefNode or nil if not found
|
|
182
|
+
def lookup_method(class_name, method_name)
|
|
183
|
+
@mutex.synchronize do
|
|
184
|
+
@method_registry.lookup(class_name, method_name)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Resolve method list to a type (for use during inference)
|
|
189
|
+
# @param methods [Array<Symbol>] Method names called on the parameter
|
|
190
|
+
# @return [Type, nil] Resolved type or nil if not resolvable
|
|
191
|
+
def resolve_methods_to_class(methods)
|
|
192
|
+
matching_classes = find_classes_defining_methods(methods)
|
|
193
|
+
@resolver.classes_to_type(matching_classes)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Find classes that define all given methods
|
|
197
|
+
def find_classes_defining_methods(methods)
|
|
198
|
+
return [] if methods.empty?
|
|
199
|
+
|
|
200
|
+
index = @global_state.index
|
|
201
|
+
return [] unless index
|
|
202
|
+
|
|
203
|
+
# For each method, find classes that define it using fuzzy_search
|
|
204
|
+
method_sets = methods.map do |method_name|
|
|
205
|
+
entries = index.fuzzy_search(method_name.to_s) do |entry|
|
|
206
|
+
entry.is_a?(RubyIndexer::Entry::Method) && entry.name == method_name.to_s
|
|
207
|
+
end
|
|
208
|
+
entries.filter_map do |entry|
|
|
209
|
+
entry.owner.name if entry.respond_to?(:owner) && entry.owner
|
|
210
|
+
end.uniq
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
return [] if method_sets.empty? || method_sets.any?(&:empty?)
|
|
214
|
+
|
|
215
|
+
# Find intersection - classes that define ALL methods
|
|
216
|
+
method_sets.reduce(:&) || []
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Start background indexing of all project files
|
|
220
|
+
def start_indexing
|
|
221
|
+
Thread.new do
|
|
222
|
+
index = @global_state.index
|
|
223
|
+
|
|
224
|
+
# Wait for Ruby LSP's initial indexing to complete
|
|
225
|
+
log_message("Waiting for Ruby LSP initial indexing to complete...")
|
|
226
|
+
sleep(0.1) until index.initial_indexing_completed
|
|
227
|
+
log_message("Ruby LSP indexing completed. Starting TypeGuessr file indexing.")
|
|
228
|
+
|
|
229
|
+
# Get all indexable files (project + gems)
|
|
230
|
+
indexable_uris = index.configuration.indexable_uris
|
|
231
|
+
total = indexable_uris.size
|
|
232
|
+
log_message("Found #{total} files to process.")
|
|
233
|
+
|
|
234
|
+
# Index each file with progress reporting
|
|
235
|
+
processed = 0
|
|
236
|
+
last_report = 0
|
|
237
|
+
report_interval = [total / 10, 50].max
|
|
238
|
+
|
|
239
|
+
indexable_uris.each do |uri|
|
|
240
|
+
traverse_file(uri)
|
|
241
|
+
processed += 1
|
|
242
|
+
|
|
243
|
+
next unless processed - last_report >= report_interval
|
|
244
|
+
|
|
245
|
+
percent = (processed * 100.0 / total).round(1)
|
|
246
|
+
log_message("Indexing progress: #{processed}/#{total} (#{percent}%)")
|
|
247
|
+
last_report = processed
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Finalize the index ONCE after all files are processed
|
|
251
|
+
@mutex.synchronize { @location_index.finalize! }
|
|
252
|
+
|
|
253
|
+
log_message("File indexing completed. Processed #{total} files.")
|
|
254
|
+
@indexing_completed = true
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
log_message("Error during file indexing: #{e.message}\n#{e.backtrace.first(10).join("\n")}")
|
|
257
|
+
@indexing_completed = true
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Check if initial indexing has completed
|
|
262
|
+
def indexing_completed?
|
|
263
|
+
@indexing_completed
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Get statistics about the index
|
|
267
|
+
# @return [Hash] Statistics
|
|
268
|
+
def stats
|
|
269
|
+
@location_index.stats
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Get all registered class names (thread-safe)
|
|
273
|
+
# @return [Array<String>] List of class names
|
|
274
|
+
def registered_classes
|
|
275
|
+
@mutex.synchronize { @method_registry.registered_classes }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Get all methods for a specific class (thread-safe)
|
|
279
|
+
# @param class_name [String] Class name
|
|
280
|
+
# @return [Hash<String, DefNode>] Methods hash
|
|
281
|
+
def methods_for_class(class_name)
|
|
282
|
+
@mutex.synchronize { @method_registry.methods_for_class(class_name) }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Search for methods matching a pattern (thread-safe)
|
|
286
|
+
# @param query [String] Search query (e.g., "User#save" or "save")
|
|
287
|
+
# @return [Array<Hash>] Array of method info hashes
|
|
288
|
+
def search_project_methods(query)
|
|
289
|
+
@mutex.synchronize do
|
|
290
|
+
@method_registry.search(query).map do |class_name, method_name, def_node|
|
|
291
|
+
{
|
|
292
|
+
class_name: class_name,
|
|
293
|
+
method_name: method_name,
|
|
294
|
+
full_name: "#{class_name}##{method_name}",
|
|
295
|
+
node_key: def_node.node_key(class_name),
|
|
296
|
+
location: { line: def_node.loc&.line }
|
|
297
|
+
}
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
private
|
|
303
|
+
|
|
304
|
+
# Traverse and index a single file
|
|
305
|
+
def traverse_file(uri)
|
|
306
|
+
file_path = uri.to_standardized_path
|
|
307
|
+
return unless file_path && File.exist?(file_path)
|
|
308
|
+
|
|
309
|
+
# Parse outside mutex (CPU-bound, no shared state)
|
|
310
|
+
source = File.read(file_path)
|
|
311
|
+
parsed = Prism.parse(source)
|
|
312
|
+
return unless parsed.value
|
|
313
|
+
|
|
314
|
+
# Create context and convert nodes outside mutex
|
|
315
|
+
context = ::TypeGuessr::Core::Converter::PrismConverter::Context.new
|
|
316
|
+
nodes = parsed.value.statements&.body&.filter_map do |stmt|
|
|
317
|
+
@converter.convert(stmt, context)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Only hold mutex while modifying shared state
|
|
321
|
+
@mutex.synchronize do
|
|
322
|
+
@location_index.remove_file(file_path)
|
|
323
|
+
nodes&.each { |node| index_node_recursively(file_path, node, "") }
|
|
324
|
+
end
|
|
325
|
+
# NOTE: finalize! is called once after ALL files are indexed in start_indexing
|
|
326
|
+
rescue StandardError => e
|
|
327
|
+
bt = e.backtrace&.first(5)&.join("\n") || "(no backtrace)"
|
|
328
|
+
log_message("Error indexing #{uri}: #{e.class}: #{e.message}\n#{bt}")
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Recursively index a node and all its children with scope tracking
|
|
332
|
+
# @param file_path [String] Absolute file path
|
|
333
|
+
# @param node [TypeGuessr::Core::IR::Node] IR node to index
|
|
334
|
+
# @param scope_id [String] Current scope identifier (e.g., "User#save")
|
|
335
|
+
def index_node_recursively(file_path, node, scope_id)
|
|
336
|
+
return unless node
|
|
337
|
+
|
|
338
|
+
case node
|
|
339
|
+
when ::TypeGuessr::Core::IR::DefNode
|
|
340
|
+
index_def_node(file_path, node, scope_id)
|
|
341
|
+
|
|
342
|
+
when ::TypeGuessr::Core::IR::ClassModuleNode
|
|
343
|
+
index_class_module_node(file_path, node, scope_id)
|
|
344
|
+
|
|
345
|
+
when ::TypeGuessr::Core::IR::CallNode
|
|
346
|
+
@location_index.add(file_path, node, scope_id)
|
|
347
|
+
index_node_recursively(file_path, node.receiver, scope_id) if node.receiver
|
|
348
|
+
node.args&.each { |arg| index_node_recursively(file_path, arg, scope_id) }
|
|
349
|
+
node.block_params&.each { |param| index_node_recursively(file_path, param, scope_id) }
|
|
350
|
+
index_node_recursively(file_path, node.block_body, scope_id) if node.block_body
|
|
351
|
+
|
|
352
|
+
when ::TypeGuessr::Core::IR::MergeNode
|
|
353
|
+
@location_index.add(file_path, node, scope_id)
|
|
354
|
+
node.branches&.each { |branch| index_node_recursively(file_path, branch, scope_id) }
|
|
355
|
+
|
|
356
|
+
when ::TypeGuessr::Core::IR::InstanceVariableWriteNode
|
|
357
|
+
@location_index.add(file_path, node, scope_id)
|
|
358
|
+
@variable_registry.register_instance_variable(node.class_name, node.name, node) if node.class_name
|
|
359
|
+
index_node_recursively(file_path, node.value, scope_id) if node.value
|
|
360
|
+
|
|
361
|
+
when ::TypeGuessr::Core::IR::ClassVariableWriteNode
|
|
362
|
+
@location_index.add(file_path, node, scope_id)
|
|
363
|
+
@variable_registry.register_class_variable(node.class_name, node.name, node) if node.class_name
|
|
364
|
+
index_node_recursively(file_path, node.value, scope_id) if node.value
|
|
365
|
+
|
|
366
|
+
when ::TypeGuessr::Core::IR::LocalWriteNode
|
|
367
|
+
@location_index.add(file_path, node, scope_id)
|
|
368
|
+
index_node_recursively(file_path, node.value, scope_id) if node.value
|
|
369
|
+
|
|
370
|
+
when ::TypeGuessr::Core::IR::ParamNode
|
|
371
|
+
@location_index.add(file_path, node, scope_id)
|
|
372
|
+
index_node_recursively(file_path, node.default_value, scope_id) if node.default_value
|
|
373
|
+
|
|
374
|
+
when ::TypeGuessr::Core::IR::ReturnNode
|
|
375
|
+
@location_index.add(file_path, node, scope_id)
|
|
376
|
+
index_node_recursively(file_path, node.value, scope_id) if node.value
|
|
377
|
+
|
|
378
|
+
when ::TypeGuessr::Core::IR::ConstantNode
|
|
379
|
+
@location_index.add(file_path, node, scope_id)
|
|
380
|
+
index_node_recursively(file_path, node.dependency, scope_id) if node.dependency
|
|
381
|
+
|
|
382
|
+
when ::TypeGuessr::Core::IR::LiteralNode
|
|
383
|
+
@location_index.add(file_path, node, scope_id)
|
|
384
|
+
# Index value nodes (e.g., variable references in arrays/hashes/keyword args)
|
|
385
|
+
node.values&.each { |value| index_node_recursively(file_path, value, scope_id) }
|
|
386
|
+
|
|
387
|
+
else
|
|
388
|
+
# LocalReadNode, InstanceVariableReadNode, ClassVariableReadNode, etc.
|
|
389
|
+
@location_index.add(file_path, node, scope_id)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def index_def_node(file_path, node, scope_id)
|
|
394
|
+
method_scope = singleton_scope_for(scope_id, singleton: node.singleton)
|
|
395
|
+
@location_index.add(file_path, node, method_scope)
|
|
396
|
+
|
|
397
|
+
new_scope = method_scope.empty? ? "##{node.name}" : "#{method_scope}##{node.name}"
|
|
398
|
+
@method_registry.register("", node.name.to_s, node) if scope_id.empty?
|
|
399
|
+
|
|
400
|
+
node.params&.each { |param| index_node_recursively(file_path, param, new_scope) }
|
|
401
|
+
node.body_nodes&.each { |body_node| index_node_recursively(file_path, body_node, new_scope) }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def index_class_module_node(file_path, node, scope_id)
|
|
405
|
+
@location_index.add(file_path, node, scope_id)
|
|
406
|
+
|
|
407
|
+
new_scope = scope_id.empty? ? node.name : "#{scope_id}::#{node.name}"
|
|
408
|
+
|
|
409
|
+
node.methods&.each do |method|
|
|
410
|
+
if method.is_a?(::TypeGuessr::Core::IR::ClassModuleNode)
|
|
411
|
+
index_node_recursively(file_path, method, new_scope)
|
|
412
|
+
else
|
|
413
|
+
index_node_recursively(file_path, method, new_scope)
|
|
414
|
+
|
|
415
|
+
method_scope = singleton_scope_for(new_scope, singleton: method.singleton)
|
|
416
|
+
@method_registry.register(method_scope, method.name.to_s, method)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Build SignatureProvider with configured type sources
|
|
422
|
+
# Currently uses RBSProvider for stdlib types
|
|
423
|
+
# Can be extended to add project RBS, Sorbet, etc.
|
|
424
|
+
def build_signature_provider
|
|
425
|
+
provider = ::TypeGuessr::Core::SignatureProvider.new
|
|
426
|
+
|
|
427
|
+
# Add stdlib RBS provider (lowest priority)
|
|
428
|
+
provider.add_provider(::TypeGuessr::Core::RBSProvider.instance)
|
|
429
|
+
|
|
430
|
+
# Future: Add project RBS provider (high priority)
|
|
431
|
+
# provider.add_provider(ProjectRBSProvider.new, priority: :high)
|
|
432
|
+
|
|
433
|
+
provider
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Build singleton class scope for method registration/lookup
|
|
437
|
+
# Singleton methods use "<Class:ClassName>" suffix to match RubyIndexer convention
|
|
438
|
+
# @param scope [String] Base scope (e.g., "RBS::Environment2")
|
|
439
|
+
# @param singleton [Boolean] Whether the method is a singleton method
|
|
440
|
+
# @return [String] Scope with singleton class suffix if applicable
|
|
441
|
+
def singleton_scope_for(scope, singleton:)
|
|
442
|
+
return scope unless singleton
|
|
443
|
+
|
|
444
|
+
parent_name = scope.split("::").last || "Object"
|
|
445
|
+
scope.empty? ? "<Class:Object>" : "#{scope}::<Class:#{parent_name}>"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Get class ancestors from RubyIndexer
|
|
449
|
+
# @param class_name [String] Class name
|
|
450
|
+
# @return [Array<String>] Array of ancestor names
|
|
451
|
+
def get_class_ancestors(class_name)
|
|
452
|
+
return [] unless @global_state.index
|
|
453
|
+
|
|
454
|
+
@global_state.index.linearized_ancestors_of(class_name)
|
|
455
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
456
|
+
[]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Get constant kind from RubyIndexer
|
|
460
|
+
# @param constant_name [String] Constant name
|
|
461
|
+
# @return [Symbol, nil] :class, :module, or nil
|
|
462
|
+
def get_constant_kind(constant_name)
|
|
463
|
+
return nil unless @global_state.index
|
|
464
|
+
|
|
465
|
+
entries = @global_state.index[constant_name]
|
|
466
|
+
return nil if entries.nil? || entries.empty?
|
|
467
|
+
|
|
468
|
+
entry = entries.first
|
|
469
|
+
case entry
|
|
470
|
+
when RubyIndexer::Entry::Class then :class
|
|
471
|
+
when RubyIndexer::Entry::Module then :module
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Look up class method owner via RubyIndexer singleton class ancestry
|
|
476
|
+
# @param class_name [String] Class name (e.g., "C")
|
|
477
|
+
# @param method_name [String] Method name (e.g., "foo")
|
|
478
|
+
# @return [String, nil] Owner name (module or singleton class name) or nil
|
|
479
|
+
def lookup_class_method_owner(class_name, method_name)
|
|
480
|
+
return nil unless @global_state.index
|
|
481
|
+
|
|
482
|
+
# Query singleton class (e.g., "C::<Class:C>") for the method
|
|
483
|
+
# Ruby LSP uses unqualified name for singleton class (e.g., "RBS::Environment::<Class:Environment>")
|
|
484
|
+
unqualified_name = class_name.split("::").last
|
|
485
|
+
singleton_name = "#{class_name}::<Class:#{unqualified_name}>"
|
|
486
|
+
entries = @global_state.index.resolve_method(method_name, singleton_name)
|
|
487
|
+
return nil if entries.nil? || entries.empty?
|
|
488
|
+
|
|
489
|
+
entry = entries.first
|
|
490
|
+
entry.owner&.name
|
|
491
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
492
|
+
nil
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def log_message(message)
|
|
496
|
+
return unless @message_queue
|
|
497
|
+
return if @message_queue.closed?
|
|
498
|
+
|
|
499
|
+
@message_queue << RubyLsp::Notification.window_log_message(
|
|
500
|
+
"[TypeGuessr] #{message}",
|
|
501
|
+
type: RubyLsp::Constant::MessageType::LOG
|
|
502
|
+
)
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|