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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -0
  3. data/exe/type-guessr +30 -0
  4. data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
  5. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
  6. data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
  7. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
  8. data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
  9. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  10. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  11. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  12. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  13. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  14. data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
  15. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  16. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
  17. data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
  18. data/lib/type-guessr.rb +3 -11
  19. data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
  20. data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
  21. data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
  22. data/lib/type_guessr/core/cache.rb +5 -0
  23. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
  24. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  25. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  26. data/lib/type_guessr/core/converter/context.rb +144 -0
  27. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  28. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  29. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  30. data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
  31. data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
  32. data/lib/type_guessr/core/converter/registration.rb +100 -0
  33. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  34. data/lib/type_guessr/core/converter.rb +4 -0
  35. data/lib/type_guessr/core/index/location_index.rb +32 -0
  36. data/lib/type_guessr/core/index.rb +3 -0
  37. data/lib/type_guessr/core/inference/resolver.rb +516 -349
  38. data/lib/type_guessr/core/inference.rb +4 -0
  39. data/lib/type_guessr/core/ir/nodes.rb +362 -103
  40. data/lib/type_guessr/core/ir.rb +3 -0
  41. data/lib/type_guessr/core/logger.rb +6 -13
  42. data/lib/type_guessr/core/node_context_helper.rb +126 -0
  43. data/lib/type_guessr/core/node_key_generator.rb +31 -0
  44. data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
  45. data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
  46. data/lib/type_guessr/core/registry/method_registry.rb +65 -38
  47. data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
  48. data/lib/type_guessr/core/registry.rb +6 -0
  49. data/lib/type_guessr/core/signature_builder.rb +39 -0
  50. data/lib/type_guessr/core/type_serializer.rb +96 -0
  51. data/lib/type_guessr/core/type_simplifier.rb +15 -12
  52. data/lib/type_guessr/core/types.rb +250 -32
  53. data/lib/type_guessr/core.rb +29 -0
  54. data/lib/type_guessr/mcp/file_watcher.rb +87 -0
  55. data/lib/type_guessr/mcp/server.rb +463 -0
  56. data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
  57. data/lib/type_guessr/version.rb +1 -1
  58. metadata +57 -8
  59. data/lib/type_guessr/core/rbs_provider.rb +0 -304
  60. data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
  61. data/lib/type_guessr/core/signature_provider.rb +0 -101
@@ -0,0 +1,463 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "prism"
5
+ require "rbs"
6
+ require "mcp"
7
+ require "ruby_indexer/ruby_indexer"
8
+
9
+ # Load ruby-lsp for RubyDocument.locate and NodeContext
10
+ require "ruby_lsp/internal"
11
+
12
+ # Load core components
13
+ require_relative "../core"
14
+
15
+ # Load CodeIndexAdapter, StandaloneRuntime, and FileWatcher
16
+ require_relative "../../ruby_lsp/type_guessr/code_index_adapter"
17
+ require_relative "standalone_runtime"
18
+ require_relative "file_watcher"
19
+
20
+ module TypeGuessr
21
+ module MCP
22
+ # MCP server that exposes type-guessr's inference engine as tools.
23
+ # Indexes the target project on startup and serves type queries via stdio transport.
24
+ #
25
+ # Usage:
26
+ # server = TypeGuessr::MCP::Server.new(project_path: "/path/to/project")
27
+ # server.start
28
+ class Server
29
+ # @return [StandaloneRuntime] The runtime used for inference (available after #start)
30
+ attr_reader :runtime
31
+
32
+ # @param project_path [String] Path to the Ruby project to analyze
33
+ def initialize(project_path:)
34
+ @project_path = File.expand_path(project_path)
35
+ @runtime = nil
36
+ end
37
+
38
+ # Build index and start the MCP server (blocks on stdio transport)
39
+ def start
40
+ warn "[type-guessr] Initializing for project: #{@project_path}"
41
+
42
+ index = build_ruby_index
43
+ @runtime = build_runtime(index)
44
+ index_project_files
45
+ start_file_watcher
46
+
47
+ server = ::MCP::Server.new(
48
+ name: "type-guessr",
49
+ version: TypeGuessr::VERSION,
50
+ tools: build_tools
51
+ )
52
+
53
+ warn "[type-guessr] Server ready"
54
+ transport = ::MCP::Server::Transports::StdioTransport.new(server)
55
+ transport.open
56
+ end
57
+
58
+ private def build_ruby_index
59
+ @ruby_index = Dir.chdir(@project_path) do
60
+ config = RubyIndexer::Configuration.new
61
+ index = RubyIndexer::Index.new
62
+ uris = config.indexable_uris
63
+ warn "[type-guessr] Indexing #{uris.size} files with RubyIndexer..."
64
+ index.index_all(uris: uris)
65
+ index
66
+ end
67
+ end
68
+
69
+ private def build_runtime(ruby_index)
70
+ code_index = RubyLsp::TypeGuessr::CodeIndexAdapter.new(ruby_index)
71
+ signature_registry = Core::Registry::SignatureRegistry.new(code_index: code_index)
72
+ Core::Registry::SignatureRegistry.instance = signature_registry
73
+
74
+ method_registry = Core::Registry::MethodRegistry.new(code_index: code_index)
75
+ ivar_registry = Core::Registry::InstanceVariableRegistry.new(code_index: code_index)
76
+ cvar_registry = Core::Registry::ClassVariableRegistry.new
77
+ type_simplifier = Core::TypeSimplifier.new(code_index: code_index)
78
+
79
+ resolver = Core::Inference::Resolver.new(
80
+ signature_registry,
81
+ code_index: code_index,
82
+ method_registry: method_registry,
83
+ ivar_registry: ivar_registry,
84
+ cvar_registry: cvar_registry,
85
+ type_simplifier: type_simplifier
86
+ )
87
+
88
+ StandaloneRuntime.new(
89
+ converter: Core::Converter::PrismConverter.new,
90
+ location_index: Core::Index::LocationIndex.new,
91
+ signature_registry: signature_registry,
92
+ method_registry: method_registry,
93
+ ivar_registry: ivar_registry,
94
+ cvar_registry: cvar_registry,
95
+ resolver: resolver,
96
+ signature_builder: Core::SignatureBuilder.new(resolver),
97
+ code_index: code_index
98
+ )
99
+ end
100
+
101
+ private def index_project_files
102
+ config = Dir.chdir(@project_path) { RubyIndexer::Configuration.new }
103
+ uris = Dir.chdir(@project_path) { config.indexable_uris }
104
+ file_paths = uris.filter_map do |uri|
105
+ uri.respond_to?(:to_standardized_path) ? uri.to_standardized_path : uri.path
106
+ end
107
+ total = file_paths.size
108
+
109
+ # Preload RBS signatures first (needed for gem inference)
110
+ @runtime.preload_signatures!
111
+
112
+ # Build member_index BEFORE gem inference (duck type resolution needs it)
113
+ @runtime.build_member_index!
114
+
115
+ # Try cache-first flow if Gemfile.lock exists
116
+ lockfile_path = File.join(@project_path, "Gemfile.lock")
117
+ if File.exist?(lockfile_path)
118
+ index_with_gem_cache(file_paths, lockfile_path)
119
+ else
120
+ index_all_files(file_paths)
121
+ end
122
+
123
+ @runtime.finalize_index!
124
+ warn "[type-guessr] Indexed #{total} files"
125
+ end
126
+
127
+ # Cache-first indexing: process gems with cache, then project files
128
+ private def index_with_gem_cache(file_paths, lockfile_path)
129
+ dep_resolver = Core::Cache::GemDependencyResolver.new(lockfile_path)
130
+ partitioned = dep_resolver.partition(file_paths)
131
+ gems = partitioned[:gems]
132
+ project_files = partitioned[:project_files]
133
+
134
+ warn "[type-guessr] Partitioned: #{gems.size} gems, #{project_files.size} project files"
135
+
136
+ cache = Core::Cache::GemSignatureCache.new
137
+ ordered = dep_resolver.topological_order(gems.keys)
138
+
139
+ ordered.each do |gem_name|
140
+ gem_info = gems[gem_name]
141
+ process_gem(gem_name, gem_info, cache)
142
+ end
143
+
144
+ # Index project files only
145
+ index_all_files(project_files)
146
+ end
147
+
148
+ # Process a single gem: cache hit → load, cache miss → save unguessed cache
149
+ private def process_gem(gem_name, gem_info, cache)
150
+ version = gem_info[:version]
151
+ deps = gem_info[:transitive_deps]
152
+ signature_registry = @runtime.instance_variable_get(:@signature_registry)
153
+ warn "[type-guessr] Processing #{gem_name}-#{version} (#{gem_info[:files].size} files)..."
154
+
155
+ if cache.cached?(gem_name, version, deps)
156
+ data = cache.load(gem_name, version, deps)
157
+ if data
158
+ signature_registry.load_gem_cache(data["instance_methods"], kind: :instance)
159
+ signature_registry.load_gem_cache(data["class_methods"], kind: :class)
160
+ fully_inferred = data.fetch("fully_inferred", true)
161
+ warn "[type-guessr] Loaded cached: #{gem_name}-#{version} (fully_inferred=#{fully_inferred})"
162
+ else
163
+ warn "[type-guessr] Cache corrupt for #{gem_name}-#{version}, skipping"
164
+ end
165
+ else
166
+ save_unguessed_cache(gem_name, gem_info, cache, signature_registry)
167
+ end
168
+ end
169
+
170
+ # Infer gem signatures using temporary registries
171
+ private def infer_and_cache_gem(gem_name, gem_info, cache, signature_registry)
172
+ version = gem_info[:version]
173
+ files = gem_info[:files]
174
+ deps = gem_info[:transitive_deps]
175
+ code_index = @runtime.instance_variable_get(:@code_index)
176
+
177
+ # Phase A: Parse + IR conversion
178
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
179
+
180
+ temp_location_index = Core::Index::LocationIndex.new
181
+ temp_method_registry = Core::Registry::MethodRegistry.new(code_index: code_index)
182
+ temp_ivar_registry = Core::Registry::InstanceVariableRegistry.new(code_index: code_index)
183
+ temp_cvar_registry = Core::Registry::ClassVariableRegistry.new
184
+ converter = Core::Converter::PrismConverter.new
185
+
186
+ files.each do |file_path|
187
+ next unless File.exist?(file_path)
188
+
189
+ parsed = Prism.parse(File.read(file_path))
190
+ next unless parsed.value
191
+
192
+ context = Core::Converter::PrismConverter::Context.new(
193
+ file_path: file_path,
194
+ location_index: temp_location_index,
195
+ method_registry: temp_method_registry,
196
+ ivar_registry: temp_ivar_registry,
197
+ cvar_registry: temp_cvar_registry
198
+ )
199
+ parsed.value.statements&.body&.each { |stmt| converter.convert(stmt, context) }
200
+ rescue StandardError => e
201
+ warn "[type-guessr] Error indexing gem file #{file_path}: #{e.message}"
202
+ end
203
+ temp_location_index.finalize!
204
+
205
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
206
+
207
+ # Phase B: Inference (extract signatures)
208
+ type_simplifier = Core::TypeSimplifier.new(code_index: code_index)
209
+ temp_resolver = Core::Inference::Resolver.new(
210
+ signature_registry,
211
+ code_index: code_index,
212
+ method_registry: temp_method_registry,
213
+ ivar_registry: temp_ivar_registry,
214
+ cvar_registry: temp_cvar_registry,
215
+ type_simplifier: type_simplifier
216
+ )
217
+ temp_builder = Core::SignatureBuilder.new(temp_resolver)
218
+
219
+ extractor = Core::Cache::GemSignatureExtractor.new(
220
+ signature_builder: temp_builder,
221
+ method_registry: temp_method_registry,
222
+ location_index: temp_location_index
223
+ )
224
+ signatures = extractor.extract(files)
225
+
226
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
227
+
228
+ # Phase C: Disk save
229
+ cache.save(gem_name, version, deps,
230
+ instance_methods: signatures[:instance_methods],
231
+ class_methods: signatures[:class_methods])
232
+
233
+ t3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
234
+
235
+ # Phase D: Registry load
236
+ signature_registry.load_gem_cache(signatures[:instance_methods], kind: :instance)
237
+ signature_registry.load_gem_cache(signatures[:class_methods], kind: :class)
238
+
239
+ t4 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
240
+
241
+ warn "[type-guessr] Cached #{gem_name}-#{version} (#{files.size} files, " \
242
+ "#{signatures[:instance_methods].size} classes) " \
243
+ "[parse=#{(t1 - t0).round(2)}s infer=#{(t2 - t1).round(2)}s " \
244
+ "save=#{(t3 - t2).round(2)}s load=#{(t4 - t3).round(2)}s]"
245
+ end
246
+
247
+ # Generate an Unguessed cache from member_index entries (no parse/infer needed).
248
+ private def save_unguessed_cache(gem_name, gem_info, cache, signature_registry)
249
+ version = gem_info[:version]
250
+ files = gem_info[:files]
251
+ deps = gem_info[:transitive_deps]
252
+ code_index = @runtime.instance_variable_get(:@code_index)
253
+
254
+ instance_methods = {}
255
+ class_methods = {}
256
+
257
+ files.each do |file_path|
258
+ code_index.member_entries_for_file(file_path).each do |entry|
259
+ owner_name = entry.owner.name
260
+
261
+ if owner_name.match?(/::<Class:[^>]+>\z/)
262
+ class_name = extract_class_from_singleton(owner_name)
263
+ target = class_methods
264
+ else
265
+ class_name = owner_name
266
+ target = instance_methods
267
+ end
268
+
269
+ target[class_name] ||= {}
270
+ target[class_name][entry.name] = {
271
+ "return_type" => { "_type" => "Unguessed" },
272
+ "params" => build_params_from_entry(entry)
273
+ }
274
+ end
275
+ end
276
+
277
+ cache.save(gem_name, version, deps,
278
+ instance_methods: instance_methods,
279
+ class_methods: class_methods,
280
+ fully_inferred: false)
281
+
282
+ signature_registry.load_gem_cache(instance_methods, kind: :instance)
283
+ signature_registry.load_gem_cache(class_methods, kind: :class)
284
+
285
+ warn "[type-guessr] Saved unguessed cache for #{gem_name}-#{version} " \
286
+ "(#{instance_methods.size} instance classes, #{class_methods.size} class method classes)"
287
+ end
288
+
289
+ # Extract simple class name from singleton format
290
+ # "File::<Class:File>" -> "File"
291
+ private def extract_class_from_singleton(owner_class)
292
+ if owner_class.match?(/::<Class:[^>]+>\z/)
293
+ owner_class.sub(/::<Class:[^>]+>\z/, "")
294
+ else
295
+ owner_class
296
+ end
297
+ end
298
+
299
+ # Build serialized params array from a RubyIndexer::Entry::Member
300
+ private def build_params_from_entry(entry)
301
+ sigs = entry.signatures
302
+ return [] if sigs.empty?
303
+
304
+ sigs.first.parameters.filter_map do |p|
305
+ kind = case p
306
+ when RubyIndexer::Entry::RequiredParameter then "required"
307
+ when RubyIndexer::Entry::OptionalParameter then "optional"
308
+ when RubyIndexer::Entry::RestParameter then "rest"
309
+ when RubyIndexer::Entry::KeywordParameter then "keyword_required"
310
+ when RubyIndexer::Entry::OptionalKeywordParameter then "keyword_optional"
311
+ when RubyIndexer::Entry::KeywordRestParameter then "keyword_rest"
312
+ when RubyIndexer::Entry::BlockParameter then "block"
313
+ else next
314
+ end
315
+ { "name" => p.name.to_s, "kind" => kind, "type" => { "_type" => "Unguessed" } }
316
+ end
317
+ end
318
+
319
+ # Index files into the main runtime (project files only)
320
+ private def index_all_files(file_paths)
321
+ file_paths.each do |file_path|
322
+ next unless File.exist?(file_path)
323
+
324
+ parsed = Prism.parse(File.read(file_path))
325
+ next unless parsed.value
326
+
327
+ @runtime.index_parsed_file(file_path, parsed)
328
+ rescue StandardError => e
329
+ warn "[type-guessr] Error indexing #{file_path}: #{e.message}"
330
+ end
331
+ end
332
+
333
+ private def start_file_watcher
334
+ @file_watcher = FileWatcher.new(
335
+ project_path: @project_path,
336
+ on_change: method(:handle_file_changes)
337
+ )
338
+ @file_watcher.start
339
+ warn "[type-guessr] File watcher started (polling every 2s)"
340
+ end
341
+
342
+ private def handle_file_changes(modified, added, removed)
343
+ (modified + added).each do |file_path|
344
+ source = File.read(file_path)
345
+ uri = URI::Generic.from_path(path: file_path)
346
+ parsed = Prism.parse(source)
347
+ @ruby_index.handle_change(uri, source)
348
+ @runtime.index_parsed_file(file_path, parsed)
349
+ @runtime.refresh_member_index!(uri)
350
+ warn "[type-guessr] Re-indexed: #{file_path}"
351
+ rescue StandardError => e
352
+ warn "[type-guessr] Error re-indexing #{file_path}: #{e.message}"
353
+ end
354
+
355
+ removed.each do |file_path|
356
+ uri = URI::Generic.from_path(path: file_path)
357
+ @ruby_index.delete(uri)
358
+ @runtime.remove_indexed_file(file_path)
359
+ @runtime.refresh_member_index!(uri)
360
+ warn "[type-guessr] Removed from index: #{file_path}"
361
+ end
362
+ end
363
+
364
+ private def build_tools
365
+ [build_get_method_sources_tool, build_get_method_signatures_tool, build_search_methods_tool]
366
+ end
367
+
368
+ private def build_get_method_sources_tool
369
+ runtime = @runtime
370
+ to_response = method(:json_response)
371
+
372
+ ::MCP::Tool.define(
373
+ name: "get_method_sources",
374
+ description: "Get source code of methods by class and method name. " \
375
+ "Returns the full method definition with file path and line number. " \
376
+ "Use when you need to read a method's implementation without knowing " \
377
+ "the exact file location.",
378
+ input_schema: {
379
+ type: "object",
380
+ properties: {
381
+ methods: {
382
+ type: "array",
383
+ items: {
384
+ type: "object",
385
+ properties: {
386
+ class_name: { type: "string", description: "Fully qualified class name (e.g., 'User', 'Admin::User')" },
387
+ method_name: { type: "string", description: "Method name (e.g., 'save', 'initialize')" }
388
+ },
389
+ required: %w[class_name method_name]
390
+ },
391
+ description: "Array of {class_name, method_name} to look up"
392
+ }
393
+ },
394
+ required: %w[methods]
395
+ }
396
+ ) do |methods:, **|
397
+ to_response.call(runtime.method_sources(methods))
398
+ end
399
+ end
400
+
401
+ private def build_get_method_signatures_tool
402
+ runtime = @runtime
403
+ to_response = method(:json_response)
404
+
405
+ ::MCP::Tool.define(
406
+ name: "get_method_signatures",
407
+ description: "Get inferred signatures for multiple methods in one call. " \
408
+ "More efficient than calling get_method_signature repeatedly " \
409
+ "when investigating a class or module.",
410
+ input_schema: {
411
+ type: "object",
412
+ properties: {
413
+ methods: {
414
+ type: "array",
415
+ items: {
416
+ type: "object",
417
+ properties: {
418
+ class_name: { type: "string", description: "Fully qualified class name" },
419
+ method_name: { type: "string", description: "Method name" }
420
+ },
421
+ required: %w[class_name method_name]
422
+ },
423
+ description: "Array of {class_name, method_name} to look up"
424
+ }
425
+ },
426
+ required: %w[methods]
427
+ }
428
+ ) do |methods:, **|
429
+ to_response.call(runtime.method_signatures(methods))
430
+ end
431
+ end
432
+
433
+ private def build_search_methods_tool
434
+ runtime = @runtime
435
+ to_response = method(:json_response)
436
+
437
+ ::MCP::Tool.define(
438
+ name: "search_methods",
439
+ description: "Search for method definitions across the project with type-aware results. " \
440
+ "Returns matching methods with their inferred signatures. Unlike grep, results " \
441
+ "include class hierarchy context and inferred types. Supports patterns: " \
442
+ "'User#save' (specific), 'save' (all classes), 'Admin::*' (namespace). " \
443
+ "Use when exploring an unfamiliar codebase or finding which classes implement " \
444
+ "a given method.",
445
+ input_schema: {
446
+ type: "object",
447
+ properties: {
448
+ query: { type: "string", description: "Search query (e.g., 'User#save', 'save', 'initialize')" },
449
+ include_signatures: { type: "boolean", description: "Include inferred method signatures in results (default: false)" }
450
+ },
451
+ required: %w[query]
452
+ }
453
+ ) do |query:, include_signatures: false, **|
454
+ to_response.call(runtime.search_methods(query, include_signatures: include_signatures))
455
+ end
456
+ end
457
+
458
+ private def json_response(data)
459
+ ::MCP::Tool::Response.new([{ type: "text", text: JSON.generate(data) }])
460
+ end
461
+ end
462
+ end
463
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeGuessr
4
+ module MCP
5
+ # Standalone runtime that mirrors RuntimeAdapter's query interface
6
+ # without depending on ruby-lsp's GlobalState.
7
+ #
8
+ # Provides type inference, method signature lookup, and method search
9
+ # for use by the MCP server. All public query methods are thread-safe.
10
+ class StandaloneRuntime
11
+ # @param converter [Core::Converter::PrismConverter]
12
+ # @param location_index [Core::Index::LocationIndex]
13
+ # @param signature_registry [Core::Registry::SignatureRegistry]
14
+ # @param method_registry [Core::Registry::MethodRegistry]
15
+ # @param ivar_registry [Core::Registry::InstanceVariableRegistry]
16
+ # @param cvar_registry [Core::Registry::ClassVariableRegistry]
17
+ # @param resolver [Core::Inference::Resolver]
18
+ # @param signature_builder [Core::SignatureBuilder]
19
+ # @param code_index [RubyLsp::TypeGuessr::CodeIndexAdapter]
20
+ def initialize(converter:, location_index:, signature_registry:, method_registry:,
21
+ ivar_registry:, cvar_registry:, resolver:, signature_builder:, code_index:)
22
+ @converter = converter
23
+ @location_index = location_index
24
+ @signature_registry = signature_registry
25
+ @method_registry = method_registry
26
+ @ivar_registry = ivar_registry
27
+ @cvar_registry = cvar_registry
28
+ @resolver = resolver
29
+ @signature_builder = signature_builder
30
+ @code_index = code_index
31
+ @mutex = Mutex.new
32
+ end
33
+
34
+ # Index a pre-parsed file into the IR graph
35
+ # @param file_path [String] Absolute path to the file
36
+ # @param prism_result [Prism::ParseResult] Parsed AST
37
+ def index_parsed_file(file_path, prism_result)
38
+ return unless prism_result.value
39
+
40
+ @mutex.synchronize do
41
+ @location_index.remove_file(file_path)
42
+ @method_registry.remove_file(file_path)
43
+ @resolver.clear_cache
44
+
45
+ context = Core::Converter::PrismConverter::Context.new(
46
+ file_path: file_path,
47
+ location_index: @location_index,
48
+ method_registry: @method_registry,
49
+ ivar_registry: @ivar_registry,
50
+ cvar_registry: @cvar_registry
51
+ )
52
+
53
+ prism_result.value.statements&.body&.each do |stmt|
54
+ @converter.convert(stmt, context)
55
+ end
56
+ end
57
+ end
58
+
59
+ # Remove all indexed data for a file
60
+ # @param file_path [String] Absolute path to the file
61
+ def remove_indexed_file(file_path)
62
+ @mutex.synchronize do
63
+ @location_index.remove_file(file_path)
64
+ @method_registry.remove_file(file_path)
65
+ @resolver.clear_cache
66
+ end
67
+ end
68
+
69
+ # Delegate member_index build to code_index
70
+ def build_member_index!
71
+ @code_index.build_member_index!
72
+ end
73
+
74
+ # Delegate member_index refresh to code_index
75
+ # @param file_uri [URI::Generic] File URI
76
+ def refresh_member_index!(file_uri)
77
+ @code_index.refresh_member_index!(file_uri)
78
+ end
79
+
80
+ # Finalize location index after all files are indexed
81
+ def finalize_index!
82
+ @mutex.synchronize { @location_index.finalize! }
83
+ end
84
+
85
+ # Preload RBS signatures for inference
86
+ def preload_signatures!
87
+ @signature_registry.preload
88
+ end
89
+
90
+ # Get method signature for a class#method
91
+ # @param class_name [String] Fully qualified class name (e.g., "User", "Admin::User")
92
+ # @param method_name [String] Method name (e.g., "save", "initialize")
93
+ # @return [Hash] Signature result with :source and :signature keys, or :error on failure
94
+ def method_signature(class_name, method_name)
95
+ def_node = @mutex.synchronize { @method_registry.lookup(class_name, method_name) }
96
+
97
+ unless def_node
98
+ entry = @signature_registry.lookup(class_name, method_name)
99
+
100
+ if entry.is_a?(Core::Registry::SignatureRegistry::MethodEntry)
101
+ return {
102
+ source: "rbs",
103
+ signatures: entry.signature_strings,
104
+ class_name: class_name,
105
+ method_name: method_name
106
+ }
107
+ end
108
+
109
+ if entry.is_a?(Core::Registry::SignatureRegistry::GemMethodEntry)
110
+ return {
111
+ source: "gem_cache",
112
+ signatures: entry.signature_strings,
113
+ class_name: class_name,
114
+ method_name: method_name
115
+ }
116
+ end
117
+
118
+ return { error: "Method not found: #{class_name}##{method_name}", class_name: class_name,
119
+ method_name: method_name }
120
+ end
121
+
122
+ sig = @mutex.synchronize { @signature_builder.build_from_def_node(def_node) }
123
+ {
124
+ source: "project",
125
+ signature: sig.to_s,
126
+ class_name: class_name,
127
+ method_name: method_name
128
+ }
129
+ rescue StandardError => e
130
+ { error: e.message, class_name: class_name, method_name: method_name }
131
+ end
132
+
133
+ # Get signatures for multiple methods in one call
134
+ # @param methods [Array<Hash>] Array of { class_name:, method_name: } hashes
135
+ # @return [Array<Hash>] Array of signature results (same format as method_signature)
136
+ def method_signatures(methods)
137
+ methods.map do |entry|
138
+ method_signature(entry[:class_name], entry[:method_name])
139
+ end
140
+ end
141
+
142
+ # Get source code for a single method
143
+ # @param class_name [String] Fully qualified class name
144
+ # @param method_name [String] Method name
145
+ # @return [Hash] Source result with :source, :file_path, :line keys, or :error on failure
146
+ def method_source(class_name, method_name)
147
+ def_node = @mutex.synchronize { @method_registry.lookup(class_name, method_name) }
148
+ unless def_node
149
+ return { error: "Method not found: #{class_name}##{method_name}",
150
+ class_name: class_name, method_name: method_name }
151
+ end
152
+
153
+ file_path = @mutex.synchronize { @method_registry.source_file_for(class_name, method_name) }
154
+ unless file_path
155
+ return { error: "Source file not found: #{class_name}##{method_name}",
156
+ class_name: class_name, method_name: method_name }
157
+ end
158
+
159
+ source = File.read(file_path)
160
+ prism_result = Prism.parse(source)
161
+ node_context = RubyLsp::RubyDocument.locate(
162
+ prism_result.value, def_node.loc,
163
+ code_units_cache: prism_result.code_units_cache(Encoding::UTF_8)
164
+ )
165
+ prism_def = node_context.node.is_a?(Prism::DefNode) ? node_context.node : node_context.parent
166
+
167
+ {
168
+ class_name: class_name,
169
+ method_name: method_name,
170
+ source: prism_def.slice,
171
+ file_path: file_path,
172
+ line: prism_def.location.start_line
173
+ }
174
+ rescue StandardError => e
175
+ { error: e.message, class_name: class_name, method_name: method_name }
176
+ end
177
+
178
+ # Get source code for multiple methods in one call
179
+ # @param methods [Array<Hash>] Array of { class_name:, method_name: } hashes
180
+ # @return [Array<Hash>] Array of source results (same format as method_source)
181
+ def method_sources(methods)
182
+ methods.map do |entry|
183
+ method_source(entry[:class_name], entry[:method_name])
184
+ end
185
+ end
186
+
187
+ # Search for methods matching a query pattern
188
+ # @param query [String] Search query (e.g., "User#save", "save", "initialize")
189
+ # @param include_signatures [Boolean] When true, include inferred signature for each result
190
+ # @return [Array<Hash>] Array of matching methods with :class_name, :method_name, :full_name, :location
191
+ def search_methods(query, include_signatures: false)
192
+ @mutex.synchronize do
193
+ results = @method_registry.search(query)
194
+ results.map do |class_name, method_name, def_node|
195
+ entry = {
196
+ class_name: class_name,
197
+ method_name: method_name,
198
+ full_name: "#{class_name}##{method_name}",
199
+ location: def_node.loc ? { offset: def_node.loc } : nil
200
+ }
201
+ if include_signatures
202
+ sig = @signature_builder.build_from_def_node(def_node)
203
+ entry[:signature] = sig.to_s
204
+ end
205
+ entry
206
+ end
207
+ end
208
+ rescue StandardError => e
209
+ { error: e.message }
210
+ end
211
+ end
212
+ end
213
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypeGuessr
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.3"
5
5
  end