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,14 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
-
require_relative "../../type_guessr/core
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
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"
|
|
4
|
+
require_relative "../../type_guessr/core"
|
|
5
|
+
require_relative "dsl_type_registrar"
|
|
6
|
+
require_relative "code_index_adapter"
|
|
12
7
|
require_relative "type_inferrer"
|
|
13
8
|
|
|
14
9
|
module RubyLsp
|
|
@@ -16,56 +11,52 @@ module RubyLsp
|
|
|
16
11
|
# RuntimeAdapter manages the IR graph and inference for TypeGuessr
|
|
17
12
|
# Converts files to IR graphs and provides type inference
|
|
18
13
|
class RuntimeAdapter
|
|
19
|
-
attr_reader :
|
|
14
|
+
attr_reader :signature_registry, :location_index, :resolver, :method_registry
|
|
20
15
|
|
|
21
16
|
def initialize(global_state, message_queue = nil)
|
|
22
17
|
@global_state = global_state
|
|
23
18
|
@message_queue = message_queue
|
|
24
19
|
@converter = ::TypeGuessr::Core::Converter::PrismConverter.new
|
|
25
20
|
@location_index = ::TypeGuessr::Core::Index::LocationIndex.new
|
|
26
|
-
@signature_provider = build_signature_provider
|
|
27
21
|
@indexing_completed = false
|
|
28
22
|
@mutex = Mutex.new
|
|
29
23
|
@original_type_inferrer = nil
|
|
30
24
|
|
|
31
|
-
# Create
|
|
32
|
-
|
|
25
|
+
# Create CodeIndexAdapter wrapping RubyIndexer
|
|
26
|
+
@code_index = CodeIndexAdapter.new(global_state&.index)
|
|
33
27
|
|
|
34
|
-
# Create
|
|
28
|
+
# Create SignatureRegistry with code_index for ancestor chain traversal
|
|
29
|
+
@signature_registry = ::TypeGuessr::Core::Registry::SignatureRegistry.new(code_index: @code_index)
|
|
30
|
+
::TypeGuessr::Core::Registry::SignatureRegistry.instance = @signature_registry
|
|
31
|
+
|
|
32
|
+
# Create method registry with code_index for inheritance lookup
|
|
35
33
|
@method_registry = ::TypeGuessr::Core::Registry::MethodRegistry.new(
|
|
36
|
-
|
|
34
|
+
code_index: @code_index
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Create variable registries (ivar needs code_index for inheritance lookup)
|
|
38
|
+
@ivar_registry = ::TypeGuessr::Core::Registry::InstanceVariableRegistry.new(
|
|
39
|
+
code_index: @code_index
|
|
37
40
|
)
|
|
41
|
+
@cvar_registry = ::TypeGuessr::Core::Registry::ClassVariableRegistry.new
|
|
38
42
|
|
|
39
|
-
# Create
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
# Create type simplifier with code_index for inheritance lookup
|
|
44
|
+
type_simplifier = ::TypeGuessr::Core::TypeSimplifier.new(
|
|
45
|
+
code_index: @code_index
|
|
42
46
|
)
|
|
43
47
|
|
|
44
|
-
# Create resolver with
|
|
48
|
+
# Create resolver with signature_registry and registries
|
|
45
49
|
@resolver = ::TypeGuessr::Core::Inference::Resolver.new(
|
|
46
|
-
@
|
|
50
|
+
@signature_registry,
|
|
51
|
+
code_index: @code_index,
|
|
47
52
|
method_registry: @method_registry,
|
|
48
|
-
|
|
53
|
+
ivar_registry: @ivar_registry,
|
|
54
|
+
cvar_registry: @cvar_registry,
|
|
55
|
+
type_simplifier: type_simplifier
|
|
49
56
|
)
|
|
50
57
|
|
|
51
|
-
#
|
|
52
|
-
@
|
|
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
|
-
)
|
|
58
|
+
# Build method signatures from DefNodes using resolver
|
|
59
|
+
@signature_builder = ::TypeGuessr::Core::SignatureBuilder.new(@resolver)
|
|
69
60
|
end
|
|
70
61
|
|
|
71
62
|
# Swap ruby-lsp's TypeInferrer with TypeGuessr's custom implementation
|
|
@@ -99,27 +90,9 @@ module RubyLsp
|
|
|
99
90
|
file_path = uri.to_standardized_path
|
|
100
91
|
return unless file_path
|
|
101
92
|
|
|
102
|
-
# Parse and convert to IR outside mutex
|
|
103
93
|
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
94
|
|
|
112
|
-
|
|
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
|
|
95
|
+
index_file_with_prism_result(file_path, parsed)
|
|
123
96
|
rescue StandardError => e
|
|
124
97
|
log_message("Error in index_file #{uri}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
|
125
98
|
end
|
|
@@ -134,26 +107,26 @@ module RubyLsp
|
|
|
134
107
|
file_path ||= uri_string.sub(%r{^file://}, "")
|
|
135
108
|
return unless file_path
|
|
136
109
|
|
|
137
|
-
|
|
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
|
|
110
|
+
parsed = Prism.parse(source)
|
|
145
111
|
|
|
146
|
-
|
|
147
|
-
|
|
112
|
+
index_file_with_prism_result(file_path, parsed)
|
|
113
|
+
end
|
|
148
114
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end
|
|
115
|
+
# Build member_index for duck type resolution (exposed for testing)
|
|
116
|
+
def build_member_index!
|
|
117
|
+
@code_index.build_member_index!
|
|
118
|
+
end
|
|
154
119
|
|
|
155
|
-
|
|
156
|
-
|
|
120
|
+
# Remove indexed data for a file
|
|
121
|
+
# @param file_path [String] File path to remove
|
|
122
|
+
def remove_indexed_file(file_path)
|
|
123
|
+
@mutex.synchronize do
|
|
124
|
+
@location_index.remove_file(file_path)
|
|
125
|
+
@method_registry.remove_file(file_path)
|
|
126
|
+
@ivar_registry.remove_file(file_path)
|
|
127
|
+
@cvar_registry.remove_file(file_path)
|
|
128
|
+
@resolver.clear_cache
|
|
129
|
+
@code_index.refresh_member_index!(URI::Generic.from_path(path: file_path))
|
|
157
130
|
end
|
|
158
131
|
end
|
|
159
132
|
|
|
@@ -175,6 +148,51 @@ module RubyLsp
|
|
|
175
148
|
end
|
|
176
149
|
end
|
|
177
150
|
|
|
151
|
+
# Build a MethodSignature from a DefNode
|
|
152
|
+
# @param def_node [TypeGuessr::Core::IR::DefNode] Method definition node
|
|
153
|
+
# @return [TypeGuessr::Core::Types::MethodSignature] Structured method signature
|
|
154
|
+
def build_method_signature(def_node)
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
@signature_builder.build_from_def_node(def_node)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Build a constructor signature for Class.new calls
|
|
161
|
+
# Maps .new to #initialize and returns ClassName instance
|
|
162
|
+
# Checks project methods first, then falls back to RBS
|
|
163
|
+
# @param class_name [String] Class name (e.g., "User")
|
|
164
|
+
# @return [Hash] { signature: MethodSignature, source: :project | :rbs | :default }
|
|
165
|
+
def build_constructor_signature(class_name)
|
|
166
|
+
@mutex.synchronize do
|
|
167
|
+
instance_type = ::TypeGuessr::Core::Types::ClassInstance.for(class_name)
|
|
168
|
+
|
|
169
|
+
# 1. Try project methods first
|
|
170
|
+
init_def = @method_registry.lookup(class_name, "initialize")
|
|
171
|
+
if init_def
|
|
172
|
+
sig = @signature_builder.build_from_def_node(init_def)
|
|
173
|
+
return {
|
|
174
|
+
signature: ::TypeGuessr::Core::Types::MethodSignature.new(sig.params, instance_type),
|
|
175
|
+
source: :project
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# 2. Fall back to RBS
|
|
180
|
+
rbs_sigs = @signature_registry.get_method_signatures(class_name, "initialize")
|
|
181
|
+
if rbs_sigs.any?
|
|
182
|
+
return {
|
|
183
|
+
rbs_signature: rbs_sigs.first,
|
|
184
|
+
source: :rbs
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# 3. Default: no initialize found
|
|
189
|
+
{
|
|
190
|
+
signature: ::TypeGuessr::Core::Types::MethodSignature.new([], instance_type),
|
|
191
|
+
source: :default
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
178
196
|
# Look up a method definition by class name and method name
|
|
179
197
|
# @param class_name [String] Class name (e.g., "User", "Admin::User")
|
|
180
198
|
# @param method_name [String] Method name (e.g., "initialize", "save")
|
|
@@ -185,35 +203,17 @@ module RubyLsp
|
|
|
185
203
|
end
|
|
186
204
|
end
|
|
187
205
|
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
def
|
|
192
|
-
|
|
193
|
-
|
|
206
|
+
# Check if a method should skip stdlib/gem RBS lookup in hover.
|
|
207
|
+
# Returns true for entries registered by DSL adapters (e.g., AR column accessor)
|
|
208
|
+
# so that hover goes directly to the resolver fallback instead of showing RBS signatures.
|
|
209
|
+
def skip_stdlib_rbs_method?(class_name, method_name)
|
|
210
|
+
entry = @signature_registry.lookup(class_name, method_name)
|
|
211
|
+
entry.is_a?(::TypeGuessr::Core::Registry::SignatureRegistry::GemMethodEntry) && entry.skip_stdlib_rbs?
|
|
194
212
|
end
|
|
195
213
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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(:&) || []
|
|
214
|
+
def skip_stdlib_rbs_class_method?(class_name, method_name)
|
|
215
|
+
entry = @signature_registry.lookup_class_method(class_name, method_name)
|
|
216
|
+
entry.is_a?(::TypeGuessr::Core::Registry::SignatureRegistry::GemMethodEntry) && entry.skip_stdlib_rbs?
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
# Start background indexing of all project files
|
|
@@ -224,34 +224,48 @@ module RubyLsp
|
|
|
224
224
|
# Wait for Ruby LSP's initial indexing to complete
|
|
225
225
|
log_message("Waiting for Ruby LSP initial indexing to complete...")
|
|
226
226
|
sleep(0.1) until index.initial_indexing_completed
|
|
227
|
-
log_message("Ruby LSP indexing completed.
|
|
227
|
+
log_message("Ruby LSP indexing completed.")
|
|
228
228
|
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
total = indexable_uris.size
|
|
232
|
-
log_message("Found #{total} files to process.")
|
|
229
|
+
# Preload RBS signatures while waiting for other addons to finish
|
|
230
|
+
@signature_registry.preload
|
|
233
231
|
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
last_report = 0
|
|
237
|
-
report_interval = [total / 10, 50].max
|
|
232
|
+
# Wait for other addons (ruby-lsp-rails, etc.) to finish registering entries
|
|
233
|
+
wait_for_index_stabilization(index)
|
|
238
234
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
235
|
+
# Build member_index AFTER all entries are registered
|
|
236
|
+
@code_index.build_member_index!
|
|
237
|
+
log_message("Member index built.")
|
|
242
238
|
|
|
243
|
-
|
|
239
|
+
# Get all indexable files (project + gems)
|
|
240
|
+
indexable_uris = index.configuration.indexable_uris
|
|
241
|
+
file_paths = indexable_uris.filter_map(&:to_standardized_path)
|
|
242
|
+
total = file_paths.size
|
|
243
|
+
log_message("Found #{total} files to process.")
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
# Try cache-first flow if Gemfile.lock exists
|
|
246
|
+
lockfile_path = find_lockfile
|
|
247
|
+
result = nil
|
|
248
|
+
if lockfile_path
|
|
249
|
+
result = index_with_gem_cache(file_paths, lockfile_path)
|
|
250
|
+
else
|
|
251
|
+
index_all_files(file_paths)
|
|
248
252
|
end
|
|
253
|
+
# Connect on-demand inference callback for Unguessed gem methods
|
|
254
|
+
@signature_registry.on_demand_inferrer = method(:infer_gem_file_on_demand)
|
|
249
255
|
|
|
250
|
-
#
|
|
251
|
-
|
|
256
|
+
# Register DSL types (AR column accessors, enums, associations, scopes)
|
|
257
|
+
register_dsl_types
|
|
252
258
|
|
|
253
|
-
log_message("File indexing completed. Processed #{total} files.")
|
|
254
259
|
@indexing_completed = true
|
|
260
|
+
|
|
261
|
+
# Start schema watch loop for auto-refresh
|
|
262
|
+
start_schema_watch_loop
|
|
263
|
+
|
|
264
|
+
# Background inference: fully infer gems that have Unguessed entries (opt-in)
|
|
265
|
+
if ::TypeGuessr::Core::Config.background_gem_indexing? && result && result[:unguessed_gems].any?
|
|
266
|
+
background_infer_gems(result[:unguessed_gems],
|
|
267
|
+
result[:cache])
|
|
268
|
+
end
|
|
255
269
|
rescue StandardError => e
|
|
256
270
|
log_message("Error during file indexing: #{e.message}\n#{e.backtrace.first(10).join("\n")}")
|
|
257
271
|
@indexing_completed = true
|
|
@@ -269,12 +283,6 @@ module RubyLsp
|
|
|
269
283
|
@location_index.stats
|
|
270
284
|
end
|
|
271
285
|
|
|
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
286
|
# Get all methods for a specific class (thread-safe)
|
|
279
287
|
# @param class_name [String] Class name
|
|
280
288
|
# @return [Hash<String, DefNode>] Methods hash
|
|
@@ -299,200 +307,528 @@ module RubyLsp
|
|
|
299
307
|
end
|
|
300
308
|
end
|
|
301
309
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
# Resolve a short constant name to fully qualified name
|
|
311
|
+
# @param short_name [String] Short constant name
|
|
312
|
+
# @param nesting [Array<String>] Nesting context
|
|
313
|
+
# @return [String, nil] Fully qualified name or nil
|
|
314
|
+
def resolve_constant_name(short_name, nesting)
|
|
315
|
+
@code_index&.resolve_constant_name(short_name, nesting)
|
|
316
|
+
end
|
|
308
317
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
318
|
+
# Look up RBS method signatures with owner resolution
|
|
319
|
+
# Finds the actual class that defines the method (e.g., Object for #tap)
|
|
320
|
+
# @param class_name [String] Receiver class name
|
|
321
|
+
# @param method_name [String] Method name
|
|
322
|
+
# @return [Hash] { signatures: Array<Signature>, owner: String }
|
|
323
|
+
def get_rbs_method_signatures(class_name, method_name)
|
|
324
|
+
@mutex.synchronize do
|
|
325
|
+
# Find actual owner class (e.g., Object for tap on MyClass)
|
|
326
|
+
owner_class = @code_index&.instance_method_owner(class_name, method_name) || class_name
|
|
313
327
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
nodes = parsed.value.statements&.body&.filter_map do |stmt|
|
|
317
|
-
@converter.convert(stmt, context)
|
|
328
|
+
signatures = @signature_registry.get_method_signatures(owner_class, method_name)
|
|
329
|
+
{ signatures: signatures, owner: owner_class }
|
|
318
330
|
end
|
|
331
|
+
end
|
|
319
332
|
|
|
320
|
-
|
|
333
|
+
# Look up RBS class method signatures with owner resolution
|
|
334
|
+
# @param class_name [String] Class name
|
|
335
|
+
# @param method_name [String] Method name
|
|
336
|
+
# @return [Hash] { signatures: Array<Signature>, owner: String }
|
|
337
|
+
def get_rbs_class_method_signatures(class_name, method_name)
|
|
321
338
|
@mutex.synchronize do
|
|
322
|
-
|
|
323
|
-
|
|
339
|
+
# Find actual owner class for class methods
|
|
340
|
+
owner_class = @code_index&.class_method_owner(class_name, method_name) || class_name
|
|
341
|
+
|
|
342
|
+
# Convert singleton format (e.g., "File::<Class:File>") to simple class name ("File")
|
|
343
|
+
# SignatureRegistry expects simple class names for RBS lookup
|
|
344
|
+
owner_class = extract_class_from_singleton(owner_class)
|
|
345
|
+
|
|
346
|
+
signatures = @signature_registry.get_class_method_signatures(owner_class, method_name)
|
|
347
|
+
{ signatures: signatures, owner: owner_class }
|
|
324
348
|
end
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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) }
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Cache-first indexing: process gems with cache, then project files
|
|
352
|
+
# @return [Hash] { cache:, unguessed_gems: [...] }
|
|
353
|
+
private def index_with_gem_cache(file_paths, lockfile_path)
|
|
354
|
+
resolver_class = ::TypeGuessr::Core::Cache::GemDependencyResolver
|
|
355
|
+
dep_resolver = resolver_class.new(lockfile_path)
|
|
356
|
+
partitioned = dep_resolver.partition(file_paths)
|
|
357
|
+
gems = partitioned[:gems]
|
|
358
|
+
project_files = partitioned[:project_files]
|
|
359
|
+
|
|
360
|
+
log_message("Partitioned: #{gems.size} gems, #{project_files.size} project files.")
|
|
361
|
+
|
|
362
|
+
cache = ::TypeGuessr::Core::Cache::GemSignatureCache.new
|
|
363
|
+
ordered = dep_resolver.topological_order(gems.keys)
|
|
364
|
+
|
|
365
|
+
# Process each gem in dependency order, collecting those needing background inference
|
|
366
|
+
unguessed_gems = []
|
|
367
|
+
ordered.each do |gem_name|
|
|
368
|
+
gem_info = gems[gem_name]
|
|
369
|
+
needs_inference = process_gem(gem_name, gem_info, cache)
|
|
370
|
+
unguessed_gems << { name: gem_name, info: gem_info } if needs_inference
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Index project files into main registries
|
|
374
|
+
index_all_files(project_files)
|
|
375
|
+
|
|
376
|
+
{ cache: cache, unguessed_gems: unguessed_gems }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Process a single gem: cache hit → load, cache miss → save unguessed cache
|
|
380
|
+
# @return [Boolean] Whether background inference is needed for this gem
|
|
381
|
+
private def process_gem(gem_name, gem_info, cache)
|
|
382
|
+
version = gem_info[:version]
|
|
383
|
+
deps = gem_info[:transitive_deps]
|
|
384
|
+
file_count = gem_info[:files].size
|
|
385
|
+
|
|
386
|
+
log_message("Processing #{gem_name}-#{version} (#{file_count} files)...")
|
|
386
387
|
|
|
388
|
+
if cache.cached?(gem_name, version, deps)
|
|
389
|
+
data = load_gem_from_cache(gem_name, version, deps, cache)
|
|
390
|
+
return false unless data
|
|
391
|
+
|
|
392
|
+
# Previously timed out — don't retry background inference
|
|
393
|
+
return false if data["inference_timeout"]
|
|
394
|
+
|
|
395
|
+
!data["fully_inferred"]
|
|
387
396
|
else
|
|
388
|
-
|
|
389
|
-
|
|
397
|
+
save_unguessed_cache(gem_name, gem_info, cache)
|
|
398
|
+
true
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Load gem signatures from disk cache into SignatureRegistry
|
|
403
|
+
# @return [Hash, nil] Loaded cache data or nil on failure
|
|
404
|
+
private def load_gem_from_cache(gem_name, version, deps, cache)
|
|
405
|
+
data = cache.load(gem_name, version, deps)
|
|
406
|
+
unless data
|
|
407
|
+
log_message("Cache corrupt for #{gem_name}-#{version}, skipping.")
|
|
408
|
+
return nil
|
|
390
409
|
end
|
|
410
|
+
|
|
411
|
+
@signature_registry.load_gem_cache(data["instance_methods"], kind: :instance)
|
|
412
|
+
@signature_registry.load_gem_cache(data["class_methods"], kind: :class)
|
|
413
|
+
log_message(
|
|
414
|
+
"Loaded cached signatures for #{gem_name}-#{version} " \
|
|
415
|
+
"(fully_inferred=#{data["fully_inferred"]}, inference_timeout=#{data["inference_timeout"]})."
|
|
416
|
+
)
|
|
417
|
+
data
|
|
391
418
|
end
|
|
392
419
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
420
|
+
# Infer gem method signatures using temporary registries, then cache
|
|
421
|
+
# @return [Hash, nil] { instance_methods:, class_methods: } or nil on error
|
|
422
|
+
private def infer_and_cache_gem(gem_name, gem_info, cache)
|
|
423
|
+
version = gem_info[:version]
|
|
424
|
+
files = gem_info[:files]
|
|
425
|
+
deps = gem_info[:transitive_deps]
|
|
426
|
+
|
|
427
|
+
# Phase A: Parse + IR conversion
|
|
428
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
429
|
+
|
|
430
|
+
temp_location_index = ::TypeGuessr::Core::Index::LocationIndex.new
|
|
431
|
+
temp_method_registry = ::TypeGuessr::Core::Registry::MethodRegistry.new(code_index: @code_index)
|
|
432
|
+
temp_ivar_registry = ::TypeGuessr::Core::Registry::InstanceVariableRegistry.new(code_index: @code_index)
|
|
433
|
+
temp_cvar_registry = ::TypeGuessr::Core::Registry::ClassVariableRegistry.new
|
|
434
|
+
|
|
435
|
+
converter = ::TypeGuessr::Core::Converter::PrismConverter.new
|
|
436
|
+
files.each do |file_path|
|
|
437
|
+
parse_and_index_file(file_path, converter, temp_location_index,
|
|
438
|
+
temp_method_registry, temp_ivar_registry, temp_cvar_registry)
|
|
439
|
+
end
|
|
440
|
+
temp_location_index.finalize!
|
|
441
|
+
|
|
442
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
443
|
+
|
|
444
|
+
# Phase B: Inference (extract signatures)
|
|
445
|
+
type_simplifier = ::TypeGuessr::Core::TypeSimplifier.new(code_index: @code_index)
|
|
446
|
+
temp_resolver = ::TypeGuessr::Core::Inference::Resolver.new(
|
|
447
|
+
@signature_registry,
|
|
448
|
+
code_index: @code_index,
|
|
449
|
+
method_registry: temp_method_registry,
|
|
450
|
+
ivar_registry: temp_ivar_registry,
|
|
451
|
+
cvar_registry: temp_cvar_registry,
|
|
452
|
+
type_simplifier: type_simplifier
|
|
453
|
+
)
|
|
454
|
+
temp_builder = ::TypeGuessr::Core::SignatureBuilder.new(temp_resolver)
|
|
455
|
+
|
|
456
|
+
extractor = ::TypeGuessr::Core::Cache::GemSignatureExtractor.new(
|
|
457
|
+
signature_builder: temp_builder,
|
|
458
|
+
method_registry: temp_method_registry,
|
|
459
|
+
location_index: temp_location_index
|
|
460
|
+
)
|
|
461
|
+
timeout = ::TypeGuessr::Core::Config.gem_inference_timeout
|
|
462
|
+
signatures = extractor.extract(files, timeout: timeout)
|
|
463
|
+
|
|
464
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
465
|
+
|
|
466
|
+
# Inference timed out — save as unguessed with timeout flag
|
|
467
|
+
unless signatures
|
|
468
|
+
log_message(
|
|
469
|
+
"Inference timeout for #{gem_name}-#{version} " \
|
|
470
|
+
"(#{files.size} files, parse=#{(t1 - t0).round(2)}s, " \
|
|
471
|
+
"infer>#{timeout}s), deferring to on-demand"
|
|
472
|
+
)
|
|
473
|
+
save_unguessed_cache(gem_name, gem_info, cache, inference_timeout: true)
|
|
474
|
+
return nil
|
|
475
|
+
end
|
|
396
476
|
|
|
397
|
-
|
|
398
|
-
|
|
477
|
+
# Phase C: Disk save
|
|
478
|
+
cache.save(gem_name, version, deps,
|
|
479
|
+
instance_methods: signatures[:instance_methods],
|
|
480
|
+
class_methods: signatures[:class_methods])
|
|
399
481
|
|
|
400
|
-
|
|
401
|
-
|
|
482
|
+
t3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
483
|
+
|
|
484
|
+
# Phase D: Registry load
|
|
485
|
+
@signature_registry.load_gem_cache(signatures[:instance_methods], kind: :instance)
|
|
486
|
+
@signature_registry.load_gem_cache(signatures[:class_methods], kind: :class)
|
|
487
|
+
|
|
488
|
+
t4 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
489
|
+
|
|
490
|
+
log_message(
|
|
491
|
+
"Cached #{gem_name}-#{version} (#{files.size} files, " \
|
|
492
|
+
"#{signatures[:instance_methods].size} classes) " \
|
|
493
|
+
"[parse=#{(t1 - t0).round(2)}s infer=#{(t2 - t1).round(2)}s " \
|
|
494
|
+
"save=#{(t3 - t2).round(2)}s load=#{(t4 - t3).round(2)}s]"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
signatures
|
|
402
498
|
end
|
|
403
499
|
|
|
404
|
-
|
|
405
|
-
|
|
500
|
+
# Generate an Unguessed cache from member_index entries (no parse/infer needed).
|
|
501
|
+
# Method names and parameter structure come from RubyIndexer; types are set to Unguessed.
|
|
502
|
+
private def save_unguessed_cache(gem_name, gem_info, cache, inference_timeout: false)
|
|
503
|
+
version = gem_info[:version]
|
|
504
|
+
files = gem_info[:files]
|
|
505
|
+
deps = gem_info[:transitive_deps]
|
|
506
|
+
|
|
507
|
+
instance_methods = {}
|
|
508
|
+
class_methods = {}
|
|
509
|
+
|
|
510
|
+
files.each do |file_path|
|
|
511
|
+
@code_index.member_entries_for_file(file_path).each do |entry|
|
|
512
|
+
owner_name = entry.owner.name
|
|
513
|
+
|
|
514
|
+
if owner_name.match?(/::<Class:[^>]+>\z/)
|
|
515
|
+
class_name = extract_class_from_singleton(owner_name)
|
|
516
|
+
target = class_methods
|
|
517
|
+
else
|
|
518
|
+
class_name = owner_name
|
|
519
|
+
target = instance_methods
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
target[class_name] ||= {}
|
|
523
|
+
target[class_name][entry.name] = {
|
|
524
|
+
"return_type" => { "_type" => "Unguessed" },
|
|
525
|
+
"params" => build_params_from_entry(entry)
|
|
526
|
+
}
|
|
527
|
+
end
|
|
528
|
+
end
|
|
406
529
|
|
|
407
|
-
|
|
530
|
+
cache.save(gem_name, version, deps,
|
|
531
|
+
instance_methods: instance_methods,
|
|
532
|
+
class_methods: class_methods,
|
|
533
|
+
fully_inferred: false,
|
|
534
|
+
inference_timeout: inference_timeout)
|
|
408
535
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
index_node_recursively(file_path, method, new_scope)
|
|
412
|
-
else
|
|
413
|
-
index_node_recursively(file_path, method, new_scope)
|
|
536
|
+
@signature_registry.load_gem_cache(instance_methods, kind: :instance)
|
|
537
|
+
@signature_registry.load_gem_cache(class_methods, kind: :class)
|
|
414
538
|
|
|
415
|
-
|
|
416
|
-
|
|
539
|
+
log_message(
|
|
540
|
+
"Saved unguessed cache for #{gem_name}-#{version} " \
|
|
541
|
+
"(#{instance_methods.size} instance classes, #{class_methods.size} class method classes)"
|
|
542
|
+
)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Build serialized params array from a RubyIndexer::Entry::Member
|
|
546
|
+
private def build_params_from_entry(entry)
|
|
547
|
+
sigs = entry.signatures
|
|
548
|
+
return [] if sigs.empty?
|
|
549
|
+
|
|
550
|
+
sigs.first.parameters.filter_map do |p|
|
|
551
|
+
kind = case p
|
|
552
|
+
when RubyIndexer::Entry::RequiredParameter then "required"
|
|
553
|
+
when RubyIndexer::Entry::OptionalParameter then "optional"
|
|
554
|
+
when RubyIndexer::Entry::RestParameter then "rest"
|
|
555
|
+
when RubyIndexer::Entry::KeywordParameter then "keyword_required"
|
|
556
|
+
when RubyIndexer::Entry::OptionalKeywordParameter then "keyword_optional"
|
|
557
|
+
when RubyIndexer::Entry::KeywordRestParameter then "keyword_rest"
|
|
558
|
+
when RubyIndexer::Entry::BlockParameter then "block"
|
|
559
|
+
else next
|
|
560
|
+
end
|
|
561
|
+
{ "name" => p.name.to_s, "kind" => kind, "type" => { "_type" => "Unguessed" } }
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Fallback: index all files into main registries (no cache)
|
|
566
|
+
private def index_all_files(file_paths)
|
|
567
|
+
file_paths.each do |file_path|
|
|
568
|
+
next unless File.exist?(file_path)
|
|
569
|
+
|
|
570
|
+
source = File.read(file_path)
|
|
571
|
+
parsed = Prism.parse(source)
|
|
572
|
+
next unless parsed.value
|
|
573
|
+
|
|
574
|
+
@mutex.synchronize do
|
|
575
|
+
@location_index.remove_file(file_path)
|
|
576
|
+
@method_registry.remove_file(file_path)
|
|
577
|
+
@ivar_registry.remove_file(file_path)
|
|
578
|
+
@cvar_registry.remove_file(file_path)
|
|
579
|
+
|
|
580
|
+
context = ::TypeGuessr::Core::Converter::PrismConverter::Context.new(
|
|
581
|
+
file_path: file_path,
|
|
582
|
+
location_index: @location_index,
|
|
583
|
+
method_registry: @method_registry,
|
|
584
|
+
ivar_registry: @ivar_registry,
|
|
585
|
+
cvar_registry: @cvar_registry
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
parsed.value.statements&.body&.each do |stmt|
|
|
589
|
+
@converter.convert(stmt, context)
|
|
590
|
+
end
|
|
417
591
|
end
|
|
592
|
+
rescue StandardError => e
|
|
593
|
+
log_message("Error indexing #{file_path}: #{e.class}: #{e.message}")
|
|
418
594
|
end
|
|
595
|
+
|
|
596
|
+
@mutex.synchronize { @location_index.finalize! }
|
|
597
|
+
log_message("Indexed #{file_paths.size} files.")
|
|
419
598
|
end
|
|
420
599
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
provider = ::TypeGuessr::Core::SignatureProvider.new
|
|
600
|
+
# Parse and index a file into given registries (no mutex, used for temp gem registries)
|
|
601
|
+
private def parse_and_index_file(file_path, converter, location_index,
|
|
602
|
+
method_registry, ivar_registry, cvar_registry)
|
|
603
|
+
return unless File.exist?(file_path)
|
|
426
604
|
|
|
427
|
-
|
|
428
|
-
|
|
605
|
+
source = File.read(file_path)
|
|
606
|
+
parsed = Prism.parse(source)
|
|
607
|
+
return unless parsed.value
|
|
429
608
|
|
|
430
|
-
|
|
431
|
-
|
|
609
|
+
context = ::TypeGuessr::Core::Converter::PrismConverter::Context.new(
|
|
610
|
+
file_path: file_path,
|
|
611
|
+
location_index: location_index,
|
|
612
|
+
method_registry: method_registry,
|
|
613
|
+
ivar_registry: ivar_registry,
|
|
614
|
+
cvar_registry: cvar_registry
|
|
615
|
+
)
|
|
432
616
|
|
|
433
|
-
|
|
617
|
+
parsed.value.statements&.body&.each do |stmt|
|
|
618
|
+
converter.convert(stmt, context)
|
|
619
|
+
end
|
|
620
|
+
rescue StandardError => e
|
|
621
|
+
log_message("Error indexing gem file #{file_path}: #{e.class}: #{e.message}")
|
|
434
622
|
end
|
|
435
623
|
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
# @return [String] Scope with singleton class suffix if applicable
|
|
441
|
-
def singleton_scope_for(scope, singleton:)
|
|
442
|
-
return scope unless singleton
|
|
624
|
+
# Find Gemfile.lock in the workspace
|
|
625
|
+
private def find_lockfile
|
|
626
|
+
workspace_path = @global_state.workspace_path
|
|
627
|
+
return nil unless workspace_path
|
|
443
628
|
|
|
444
|
-
|
|
445
|
-
|
|
629
|
+
lockfile = File.join(workspace_path, "Gemfile.lock")
|
|
630
|
+
File.exist?(lockfile) ? lockfile : nil
|
|
446
631
|
end
|
|
447
632
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
633
|
+
private def index_file_with_prism_result(file_path, prism_result)
|
|
634
|
+
return unless prism_result.value
|
|
635
|
+
|
|
636
|
+
@mutex.synchronize do
|
|
637
|
+
# Clear existing index for this file
|
|
638
|
+
@location_index.remove_file(file_path)
|
|
639
|
+
@method_registry.remove_file(file_path)
|
|
640
|
+
@ivar_registry.remove_file(file_path)
|
|
641
|
+
@cvar_registry.remove_file(file_path)
|
|
642
|
+
@resolver.clear_cache
|
|
643
|
+
|
|
644
|
+
# Create context with index/registry injection - nodes are registered during conversion
|
|
645
|
+
context = ::TypeGuessr::Core::Converter::PrismConverter::Context.new(
|
|
646
|
+
file_path: file_path,
|
|
647
|
+
location_index: @location_index,
|
|
648
|
+
method_registry: @method_registry,
|
|
649
|
+
ivar_registry: @ivar_registry,
|
|
650
|
+
cvar_registry: @cvar_registry
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
prism_result.value.statements&.body&.each do |stmt|
|
|
654
|
+
@converter.convert(stmt, context)
|
|
655
|
+
end
|
|
453
656
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
657
|
+
# Finalize the index for efficient lookups
|
|
658
|
+
@location_index.finalize!
|
|
659
|
+
|
|
660
|
+
# Update member_index (RubyIndexer is already updated by ruby-lsp)
|
|
661
|
+
@code_index.refresh_member_index!(URI::Generic.from_path(path: file_path))
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Extract simple class name from singleton format
|
|
666
|
+
# "File::<Class:File>" -> "File"
|
|
667
|
+
# "Namespace::MyClass::<Class:MyClass>" -> "Namespace::MyClass"
|
|
668
|
+
# @param owner_class [String] Owner class name (may be singleton format)
|
|
669
|
+
# @return [String] Simple class name
|
|
670
|
+
private def extract_class_from_singleton(owner_class)
|
|
671
|
+
# Match singleton pattern: "ClassName::<Class:ClassName>"
|
|
672
|
+
if owner_class.match?(/::<Class:[^>]+>\z/)
|
|
673
|
+
owner_class.sub(/::<Class:[^>]+>\z/, "")
|
|
674
|
+
else
|
|
675
|
+
owner_class
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Background inference: fully infer gems with Unguessed entries after indexing.
|
|
680
|
+
# Runs in the same indexing thread after @indexing_completed = true.
|
|
681
|
+
# Replaces in-memory Unguessed entries and updates disk cache to fully_inferred.
|
|
682
|
+
private def background_infer_gems(unguessed_gems, cache)
|
|
683
|
+
log_message("Starting background inference for #{unguessed_gems.size} gems...")
|
|
684
|
+
|
|
685
|
+
unguessed_gems.each do |entry|
|
|
686
|
+
signatures = infer_and_cache_gem(entry[:name], entry[:info], cache)
|
|
687
|
+
next unless signatures
|
|
688
|
+
|
|
689
|
+
@signature_registry.replace_unguessed_entries(signatures[:instance_methods], kind: :instance)
|
|
690
|
+
@signature_registry.replace_unguessed_entries(signatures[:class_methods], kind: :class)
|
|
691
|
+
rescue StandardError => e
|
|
692
|
+
log_message("Background inference failed for #{entry[:name]}: #{e.message}")
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
log_message("Background inference completed.")
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# On-demand inference for a single gem file.
|
|
699
|
+
# Triggered when SignatureRegistry encounters an Unguessed return type.
|
|
700
|
+
# Parses and infers the file containing the method definition,
|
|
701
|
+
# then replaces Unguessed entries with actual inferred types.
|
|
702
|
+
private def infer_gem_file_on_demand(class_name, method_name, kind)
|
|
703
|
+
return if @inferring_on_demand # re-entrancy guard
|
|
704
|
+
|
|
705
|
+
singleton = kind == :class
|
|
706
|
+
file_path = @code_index.method_definition_file_path(class_name, method_name, singleton: singleton)
|
|
707
|
+
return unless file_path
|
|
708
|
+
|
|
709
|
+
@inferring_on_demand = true
|
|
710
|
+
begin
|
|
711
|
+
temp_location_index = ::TypeGuessr::Core::Index::LocationIndex.new
|
|
712
|
+
temp_method_registry = ::TypeGuessr::Core::Registry::MethodRegistry.new(code_index: @code_index)
|
|
713
|
+
temp_ivar_registry = ::TypeGuessr::Core::Registry::InstanceVariableRegistry.new(code_index: @code_index)
|
|
714
|
+
temp_cvar_registry = ::TypeGuessr::Core::Registry::ClassVariableRegistry.new
|
|
715
|
+
|
|
716
|
+
converter = ::TypeGuessr::Core::Converter::PrismConverter.new
|
|
717
|
+
parse_and_index_file(file_path, converter, temp_location_index,
|
|
718
|
+
temp_method_registry, temp_ivar_registry, temp_cvar_registry)
|
|
719
|
+
temp_location_index.finalize!
|
|
720
|
+
|
|
721
|
+
type_simplifier = ::TypeGuessr::Core::TypeSimplifier.new(code_index: @code_index)
|
|
722
|
+
temp_resolver = ::TypeGuessr::Core::Inference::Resolver.new(
|
|
723
|
+
@signature_registry,
|
|
724
|
+
code_index: @code_index,
|
|
725
|
+
method_registry: temp_method_registry,
|
|
726
|
+
ivar_registry: temp_ivar_registry,
|
|
727
|
+
cvar_registry: temp_cvar_registry,
|
|
728
|
+
type_simplifier: type_simplifier
|
|
729
|
+
)
|
|
730
|
+
temp_builder = ::TypeGuessr::Core::SignatureBuilder.new(temp_resolver)
|
|
731
|
+
extractor = ::TypeGuessr::Core::Cache::GemSignatureExtractor.new(
|
|
732
|
+
signature_builder: temp_builder,
|
|
733
|
+
method_registry: temp_method_registry,
|
|
734
|
+
location_index: temp_location_index
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
signatures = extractor.extract([file_path])
|
|
738
|
+
|
|
739
|
+
@signature_registry.replace_unguessed_entries(signatures[:instance_methods], kind: :instance)
|
|
740
|
+
@signature_registry.replace_unguessed_entries(signatures[:class_methods], kind: :class)
|
|
741
|
+
rescue StandardError => e
|
|
742
|
+
log_message("On-demand inference failed for #{class_name}##{method_name}: #{e.message}")
|
|
743
|
+
ensure
|
|
744
|
+
@inferring_on_demand = false
|
|
745
|
+
end
|
|
457
746
|
end
|
|
458
747
|
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
748
|
+
# Poll index entry count until it stops growing for stable_threshold consecutive checks.
|
|
749
|
+
# Other addons (ruby-lsp-rails, etc.) may register entries after initial_indexing_completed.
|
|
750
|
+
private def wait_for_index_stabilization(index, interval: 1, stable_threshold: 3)
|
|
751
|
+
previous_count = index.length
|
|
752
|
+
stable_ticks = 0
|
|
464
753
|
|
|
465
|
-
|
|
466
|
-
|
|
754
|
+
loop do
|
|
755
|
+
sleep(interval)
|
|
756
|
+
current_count = index.length
|
|
467
757
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
758
|
+
if current_count == previous_count
|
|
759
|
+
stable_ticks += 1
|
|
760
|
+
break if stable_ticks >= stable_threshold
|
|
761
|
+
else
|
|
762
|
+
log_message("Index still growing: #{current_count} entries (+#{current_count - previous_count})")
|
|
763
|
+
stable_ticks = 0
|
|
764
|
+
previous_count = current_count
|
|
765
|
+
end
|
|
472
766
|
end
|
|
767
|
+
|
|
768
|
+
log_message("Index stabilized at #{previous_count} entries.")
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
private def discover_runner_client
|
|
772
|
+
rails_addon = ::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0.4.0", "< 0.5.0")
|
|
773
|
+
rails_addon.rails_runner_client
|
|
774
|
+
rescue ::RubyLsp::Addon::AddonNotFoundError, ::RubyLsp::Addon::IncompatibleApiError
|
|
775
|
+
nil
|
|
776
|
+
rescue StandardError => e
|
|
777
|
+
log_message("Failed to discover RunnerClient: #{e.message}")
|
|
778
|
+
nil
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
private def register_dsl_types
|
|
782
|
+
workspace_path = @global_state.workspace_path
|
|
783
|
+
return unless workspace_path
|
|
784
|
+
|
|
785
|
+
@dsl_registrar = DslTypeRegistrar.new(
|
|
786
|
+
signature_registry: @signature_registry,
|
|
787
|
+
code_index: @code_index,
|
|
788
|
+
project_root: workspace_path
|
|
789
|
+
)
|
|
790
|
+
@dsl_registrar.on_log { |msg| log_message("[DSL] #{msg}") }
|
|
791
|
+
|
|
792
|
+
runner_client = wait_for_runner_client
|
|
793
|
+
@dsl_registrar.register_all(runner_client: runner_client)
|
|
794
|
+
rescue StandardError => e
|
|
795
|
+
log_message("DSL type registration failed: #{e.message}\n#{e.backtrace.first(3).join("\n")}")
|
|
473
796
|
end
|
|
474
797
|
|
|
475
|
-
#
|
|
476
|
-
#
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
798
|
+
# Wait for ruby-lsp-rails RunnerClient to transition from NullClient.
|
|
799
|
+
# 3s interval × max 10 attempts = max 30s.
|
|
800
|
+
private def wait_for_runner_client
|
|
801
|
+
log_message("[DSL] Waiting for RunnerClient...")
|
|
802
|
+
max_attempts = 10
|
|
803
|
+
|
|
804
|
+
max_attempts.times do |_i|
|
|
805
|
+
client = discover_runner_client
|
|
806
|
+
if client.respond_to?(:connected?) && client.connected?
|
|
807
|
+
log_message("[DSL] RunnerClient ready (#{client.class})")
|
|
808
|
+
return client
|
|
809
|
+
end
|
|
481
810
|
|
|
482
|
-
|
|
483
|
-
|
|
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?
|
|
811
|
+
sleep(3)
|
|
812
|
+
end
|
|
488
813
|
|
|
489
|
-
|
|
490
|
-
entry.owner&.name
|
|
491
|
-
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
814
|
+
log_message("[DSL] RunnerClient timeout after #{max_attempts} attempts")
|
|
492
815
|
nil
|
|
493
816
|
end
|
|
494
817
|
|
|
495
|
-
def
|
|
818
|
+
private def start_schema_watch_loop
|
|
819
|
+
return unless @dsl_registrar
|
|
820
|
+
|
|
821
|
+
Thread.new do
|
|
822
|
+
loop do
|
|
823
|
+
sleep(45)
|
|
824
|
+
@dsl_registrar.check_and_refresh(runner_client: discover_runner_client)
|
|
825
|
+
rescue StandardError => e
|
|
826
|
+
log_message("Schema watch error: #{e.message}")
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
private def log_message(message)
|
|
496
832
|
return unless @message_queue
|
|
497
833
|
return if @message_queue.closed?
|
|
498
834
|
|