Class: RubyLsp::TypeGuessr::RuntimeAdapter

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_lsp/type_guessr/runtime_adapter.rb

Overview

RuntimeAdapter manages the IR graph and inference for TypeGuessr Converts files to IR graphs and provides type inference

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(global_state, message_queue = nil) ⇒ RuntimeAdapter

Returns a new instance of RuntimeAdapter.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 16

def initialize(global_state, message_queue = nil)
  @global_state = global_state
  @message_queue = message_queue
  @converter = ::TypeGuessr::Core::Converter::PrismConverter.new
  @location_index = ::TypeGuessr::Core::Index::LocationIndex.new
  @indexing_completed = false
  @mutex = Mutex.new
  @original_type_inferrer = nil

  # Create CodeIndexAdapter wrapping RubyIndexer
  @code_index = CodeIndexAdapter.new(global_state&.index)

  # Create SignatureRegistry with code_index for ancestor chain traversal
  @signature_registry = ::TypeGuessr::Core::Registry::SignatureRegistry.new(code_index: @code_index)
  ::TypeGuessr::Core::Registry::SignatureRegistry.instance = @signature_registry

  # Create method registry with code_index for inheritance lookup
  @method_registry = ::TypeGuessr::Core::Registry::MethodRegistry.new(
    code_index: @code_index
  )

  # Create variable registries (ivar needs code_index for inheritance lookup)
  @ivar_registry = ::TypeGuessr::Core::Registry::InstanceVariableRegistry.new(
    code_index: @code_index
  )
  @cvar_registry = ::TypeGuessr::Core::Registry::ClassVariableRegistry.new

  # Create type simplifier with code_index for inheritance lookup
  type_simplifier = ::TypeGuessr::Core::TypeSimplifier.new(
    code_index: @code_index
  )

  # Create resolver with signature_registry and registries
  @resolver = ::TypeGuessr::Core::Inference::Resolver.new(
    @signature_registry,
    code_index: @code_index,
    method_registry: @method_registry,
    ivar_registry: @ivar_registry,
    cvar_registry: @cvar_registry,
    type_simplifier: type_simplifier
  )

  # Build method signatures from DefNodes using resolver
  @signature_builder = ::TypeGuessr::Core::SignatureBuilder.new(@resolver)
end

Instance Attribute Details

#location_indexObject (readonly)

Returns the value of attribute location_index.



14
15
16
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 14

def location_index
  @location_index
end

#method_registryObject (readonly)

Returns the value of attribute method_registry.



14
15
16
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 14

def method_registry
  @method_registry
end

#resolverObject (readonly)

Returns the value of attribute resolver.



14
15
16
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 14

def resolver
  @resolver
end

#signature_registryObject (readonly)

Returns the value of attribute signature_registry.



14
15
16
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 14

def signature_registry
  @signature_registry
end

Instance Method Details

#build_constructor_signature(class_name) ⇒ Hash

Build a constructor signature for Class.new calls Maps .new to #initialize and returns ClassName instance Checks project methods first, then falls back to RBS

Parameters:

  • class_name (String)

    Class name (e.g., “User”)

Returns:

  • (Hash)

    { signature: MethodSignature, source: :project | :rbs | :default }



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 165

def build_constructor_signature(class_name)
  @mutex.synchronize do
    instance_type = ::TypeGuessr::Core::Types::ClassInstance.for(class_name)

    # 1. Try project methods first
    init_def = @method_registry.lookup(class_name, "initialize")
    if init_def
      sig = @signature_builder.build_from_def_node(init_def)
      return {
        signature: ::TypeGuessr::Core::Types::MethodSignature.new(sig.params, instance_type),
        source: :project
      }
    end

    # 2. Fall back to RBS
    rbs_sigs = @signature_registry.get_method_signatures(class_name, "initialize")
    if rbs_sigs.any?
      return {
        rbs_signature: rbs_sigs.first,
        source: :rbs
      }
    end

    # 3. Default: no initialize found
    {
      signature: ::TypeGuessr::Core::Types::MethodSignature.new([], instance_type),
      source: :default
    }
  end
end

#build_member_index!Object

Build member_index for duck type resolution (exposed for testing)



116
117
118
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 116

def build_member_index!
  @code_index.build_member_index!
end

#build_method_signature(def_node) ⇒ TypeGuessr::Core::Types::MethodSignature

Build a MethodSignature from a DefNode

Parameters:

Returns:



154
155
156
157
158
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 154

def build_method_signature(def_node)
  @mutex.synchronize do
    @signature_builder.build_from_def_node(def_node)
  end
end

#find_node_by_key(node_key) ⇒ TypeGuessr::Core::IR::Node?

Find IR node by its unique key

Parameters:

  • node_key (String)

    The node key (scope_id:node_hash)

Returns:

  • (TypeGuessr::Core::IR::Node, nil)

    IR node or nil if not found



136
137
138
139
140
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 136

def find_node_by_key(node_key)
  @mutex.synchronize do
    @location_index.find_by_key(node_key)
  end
end

#get_rbs_class_method_signatures(class_name, method_name) ⇒ Hash

Look up RBS class method signatures with owner resolution

Parameters:

  • class_name (String)

    Class name

  • method_name (String)

    Method name

Returns:

  • (Hash)

    { signatures: Array<Signature>, owner: String }



337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 337

def get_rbs_class_method_signatures(class_name, method_name)
  @mutex.synchronize do
    # Find actual owner class for class methods
    owner_class = @code_index&.class_method_owner(class_name, method_name) || class_name

    # Convert singleton format (e.g., "File::<Class:File>") to simple class name ("File")
    # SignatureRegistry expects simple class names for RBS lookup
    owner_class = extract_class_from_singleton(owner_class)

    signatures = @signature_registry.get_class_method_signatures(owner_class, method_name)
    { signatures: signatures, owner: owner_class }
  end
end

#get_rbs_method_signatures(class_name, method_name) ⇒ Hash

Look up RBS method signatures with owner resolution Finds the actual class that defines the method (e.g., Object for #tap)

Parameters:

  • class_name (String)

    Receiver class name

  • method_name (String)

    Method name

Returns:

  • (Hash)

    { signatures: Array<Signature>, owner: String }



323
324
325
326
327
328
329
330
331
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 323

def get_rbs_method_signatures(class_name, method_name)
  @mutex.synchronize do
    # Find actual owner class (e.g., Object for tap on MyClass)
    owner_class = @code_index&.instance_method_owner(class_name, method_name) || class_name

    signatures = @signature_registry.get_method_signatures(owner_class, method_name)
    { signatures: signatures, owner: owner_class }
  end
end

#index_file(uri, document) ⇒ Object

Index a file by converting its Prism AST to IR graph

Parameters:

  • uri (URI::Generic)

    File URI

  • document (RubyLsp::Document)

    Document to index



89
90
91
92
93
94
95
96
97
98
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 89

def index_file(uri, document)
  file_path = uri.to_standardized_path
  return unless file_path

  parsed = document.parse_result

  index_file_with_prism_result(file_path, parsed)
rescue StandardError => e
  log_message("Error in index_file #{uri}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
end

#index_source(uri_string, source) ⇒ Object

Index source code directly (for testing)

Parameters:

  • uri_string (String)

    File URI as string

  • source (String)

    Source code to index



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 103

def index_source(uri_string, source)
  require "uri"
  uri = URI(uri_string)
  file_path = uri.respond_to?(:to_standardized_path) ? uri.to_standardized_path : uri.path
  file_path ||= uri_string.sub(%r{^file://}, "")
  return unless file_path

  parsed = Prism.parse(source)

  index_file_with_prism_result(file_path, parsed)
end

#indexing_completed?Boolean

Check if initial indexing has completed

Returns:

  • (Boolean)


276
277
278
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 276

def indexing_completed?
  @indexing_completed
end

#infer_type(node) ⇒ TypeGuessr::Core::Inference::Result

Infer type for an IR node

Parameters:

  • node (TypeGuessr::Core::IR::Node)

    IR node

Returns:



145
146
147
148
149
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 145

def infer_type(node)
  @mutex.synchronize do
    @resolver.infer(node)
  end
end

#lookup_method(class_name, method_name) ⇒ TypeGuessr::Core::IR::DefNode?

Look up a method definition by class name and method name

Parameters:

  • class_name (String)

    Class name (e.g., “User”, “Admin::User”)

  • method_name (String)

    Method name (e.g., “initialize”, “save”)

Returns:



200
201
202
203
204
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 200

def lookup_method(class_name, method_name)
  @mutex.synchronize do
    @method_registry.lookup(class_name, method_name)
  end
end

#methods_for_class(class_name) ⇒ Hash<String, DefNode>

Get all methods for a specific class (thread-safe)

Parameters:

  • class_name (String)

    Class name

Returns:

  • (Hash<String, DefNode>)

    Methods hash



289
290
291
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 289

def methods_for_class(class_name)
  @mutex.synchronize { @method_registry.methods_for_class(class_name) }
end

#remove_indexed_file(file_path) ⇒ Object

Remove indexed data for a file

Parameters:

  • file_path (String)

    File path to remove



122
123
124
125
126
127
128
129
130
131
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 122

def remove_indexed_file(file_path)
  @mutex.synchronize do
    @location_index.remove_file(file_path)
    @method_registry.remove_file(file_path)
    @ivar_registry.remove_file(file_path)
    @cvar_registry.remove_file(file_path)
    @resolver.clear_cache
    @code_index.refresh_member_index!(URI::Generic.from_path(path: file_path))
  end
end

#resolve_constant_name(short_name, nesting) ⇒ String?

Resolve a short constant name to fully qualified name

Parameters:

  • short_name (String)

    Short constant name

  • nesting (Array<String>)

    Nesting context

Returns:

  • (String, nil)

    Fully qualified name or nil



314
315
316
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 314

def resolve_constant_name(short_name, nesting)
  @code_index&.resolve_constant_name(short_name, nesting)
end

#restore_type_inferrerObject

Restore the original TypeInferrer



76
77
78
79
80
81
82
83
84
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 76

def restore_type_inferrer
  return unless @original_type_inferrer

  @global_state.instance_variable_set(:@type_inferrer, @original_type_inferrer)
  @original_type_inferrer = nil
  log_message("TypeInferrer restored")
rescue StandardError => e
  log_message("Failed to restore TypeInferrer: #{e.message}")
end

#search_project_methods(query) ⇒ Array<Hash>

Search for methods matching a pattern (thread-safe)

Parameters:

  • query (String)

    Search query (e.g., “User#save” or “save”)

Returns:

  • (Array<Hash>)

    Array of method info hashes



296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 296

def search_project_methods(query)
  @mutex.synchronize do
    @method_registry.search(query).map do |class_name, method_name, def_node|
      {
        class_name: class_name,
        method_name: method_name,
        full_name: "#{class_name}##{method_name}",
        node_key: def_node.node_key(class_name),
        location: { line: def_node.loc&.line }
      }
    end
  end
end

#skip_stdlib_rbs_class_method?(class_name, method_name) ⇒ Boolean

Returns:

  • (Boolean)


214
215
216
217
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 214

def skip_stdlib_rbs_class_method?(class_name, method_name)
  entry = @signature_registry.lookup_class_method(class_name, method_name)
  entry.is_a?(::TypeGuessr::Core::Registry::SignatureRegistry::GemMethodEntry) && entry.skip_stdlib_rbs?
end

#skip_stdlib_rbs_method?(class_name, method_name) ⇒ Boolean

Check if a method should skip stdlib/gem RBS lookup in hover. Returns true for entries registered by DSL adapters (e.g., AR column accessor) so that hover goes directly to the resolver fallback instead of showing RBS signatures.

Returns:

  • (Boolean)


209
210
211
212
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 209

def skip_stdlib_rbs_method?(class_name, method_name)
  entry = @signature_registry.lookup(class_name, method_name)
  entry.is_a?(::TypeGuessr::Core::Registry::SignatureRegistry::GemMethodEntry) && entry.skip_stdlib_rbs?
end

#start_indexingObject

Start background indexing of all project files



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 220

def start_indexing
  Thread.new do
    index = @global_state.index

    # Wait for Ruby LSP's initial indexing to complete
    log_message("Waiting for Ruby LSP initial indexing to complete...")
    sleep(0.1) until index.initial_indexing_completed
    log_message("Ruby LSP indexing completed.")

    # Preload RBS signatures while waiting for other addons to finish
    @signature_registry.preload

    # Wait for other addons (ruby-lsp-rails, etc.) to finish registering entries
    wait_for_index_stabilization(index)

    # Build member_index AFTER all entries are registered
    @code_index.build_member_index!
    log_message("Member index built.")

    # Get all indexable files (project + gems)
    indexable_uris = index.configuration.indexable_uris
    file_paths = indexable_uris.filter_map(&:to_standardized_path)
    total = file_paths.size
    log_message("Found #{total} files to process.")

    # Try cache-first flow if Gemfile.lock exists
    lockfile_path = find_lockfile
    result = nil
    if lockfile_path
      result = index_with_gem_cache(file_paths, lockfile_path)
    else
      index_all_files(file_paths)
    end
    # Connect on-demand inference callback for Unguessed gem methods
    @signature_registry.on_demand_inferrer = method(:infer_gem_file_on_demand)

    # Register DSL types (AR column accessors, enums, associations, scopes)
    register_dsl_types

    @indexing_completed = true

    # Start schema watch loop for auto-refresh
    start_schema_watch_loop

    # Background inference: fully infer gems that have Unguessed entries (opt-in)
    if ::TypeGuessr::Core::Config.background_gem_indexing? && result && result[:unguessed_gems].any?
      background_infer_gems(result[:unguessed_gems],
                            result[:cache])
    end
  rescue StandardError => e
    log_message("Error during file indexing: #{e.message}\n#{e.backtrace.first(10).join("\n")}")
    @indexing_completed = true
  end
end

#statsHash

Get statistics about the index

Returns:

  • (Hash)

    Statistics



282
283
284
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 282

def stats
  @location_index.stats
end

#swap_type_inferrerObject

Swap ruby-lsp’s TypeInferrer with TypeGuessr’s custom implementation This enhances Go to Definition and other features with heuristic type inference



64
65
66
67
68
69
70
71
72
73
# File 'lib/ruby_lsp/type_guessr/runtime_adapter.rb', line 64

def swap_type_inferrer
  return unless @global_state.respond_to?(:type_inferrer)

  @original_type_inferrer = @global_state.type_inferrer
  custom_inferrer = TypeInferrer.new(@global_state.index, self)
  @global_state.instance_variable_set(:@type_inferrer, custom_inferrer)
  log_message("TypeInferrer swapped for enhanced type inference")
rescue StandardError => e
  log_message("Failed to swap TypeInferrer: #{e.message}")
end