type-guessr 0.0.2 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/ruby_lsp/type_guessr/addon.rb +4 -5
  4. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +17 -0
  5. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +3 -3
  6. data/lib/ruby_lsp/type_guessr/debug_server.rb +2 -2
  7. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  8. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  9. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  10. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  11. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  12. data/lib/ruby_lsp/type_guessr/hover.rb +46 -40
  13. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  14. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +90 -16
  15. data/lib/type-guessr.rb +2 -13
  16. data/lib/type_guessr/core/cache/gem_signature_cache.rb +3 -2
  17. data/lib/type_guessr/core/cache.rb +5 -0
  18. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +2 -2
  19. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  20. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  21. data/lib/type_guessr/core/converter/context.rb +144 -0
  22. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  23. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  24. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  25. data/lib/type_guessr/core/converter/prism_converter.rb +9 -1682
  26. data/lib/type_guessr/core/converter/rbs_converter.rb +15 -1
  27. data/lib/type_guessr/core/converter/registration.rb +100 -0
  28. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  29. data/lib/type_guessr/core/converter.rb +4 -0
  30. data/lib/type_guessr/core/index.rb +3 -0
  31. data/lib/type_guessr/core/inference/resolver.rb +206 -208
  32. data/lib/type_guessr/core/inference.rb +4 -0
  33. data/lib/type_guessr/core/ir.rb +3 -0
  34. data/lib/type_guessr/core/logger.rb +3 -5
  35. data/lib/type_guessr/core/registry/method_registry.rb +9 -0
  36. data/lib/type_guessr/core/registry/signature_registry.rb +73 -16
  37. data/lib/type_guessr/core/registry.rb +6 -0
  38. data/lib/type_guessr/core/type_serializer.rb +18 -14
  39. data/lib/type_guessr/core/type_simplifier.rb +5 -5
  40. data/lib/type_guessr/core/types.rb +64 -22
  41. data/lib/type_guessr/core.rb +29 -0
  42. data/lib/type_guessr/mcp/server.rb +55 -46
  43. data/lib/type_guessr/mcp/standalone_runtime.rb +70 -110
  44. data/lib/type_guessr/version.rb +1 -1
  45. metadata +25 -5
  46. data/.mcp.json +0 -9
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "singleton"
4
3
  require "rbs"
5
4
  require_relative "../types"
6
5
  require_relative "../logger"
7
- require_relative "../converter/rbs_converter"
6
+ require_relative "../converter"
8
7
  require_relative "../type_serializer"
9
8
 
10
9
  module TypeGuessr
@@ -13,7 +12,9 @@ module TypeGuessr
13
12
  # Preloads stdlib RBS signatures and provides O(1) hash lookup
14
13
  # Singleton to ensure RBS is loaded only once across all usages
15
14
  class SignatureRegistry
16
- include Singleton
15
+ class << self
16
+ attr_accessor :instance
17
+ end
17
18
 
18
19
  # Represents a method signature entry from RBS
19
20
  # Handles overload resolution and type conversion
@@ -205,11 +206,14 @@ module TypeGuessr
205
206
  class GemMethodEntry
206
207
  attr_reader :params
207
208
 
208
- def initialize(return_type, params = [])
209
+ def initialize(return_type, params = [], skip_stdlib_rbs: false)
209
210
  @return_type = return_type
210
211
  @params = params
212
+ @skip_stdlib_rbs = skip_stdlib_rbs
211
213
  end
212
214
 
215
+ def skip_stdlib_rbs? = @skip_stdlib_rbs
216
+
213
217
  def return_type(_arg_types = [])
214
218
  @return_type
215
219
  end
@@ -233,14 +237,16 @@ module TypeGuessr
233
237
  # Get formatted signature strings for display
234
238
  # @return [Array<String>] human-readable method signatures
235
239
  def signature_strings
236
- param_str = @params.map(&:to_s).join(", ")
240
+ param_str = @params.join(", ")
237
241
  ["(#{param_str}) -> #{@return_type}"]
238
242
  end
239
243
  end
240
244
 
245
+ attr_accessor :code_index
241
246
  attr_writer :on_demand_inferrer # ->(class_name, method_name, kind) { ... }
242
247
 
243
- def initialize
248
+ def initialize(code_index: nil)
249
+ @code_index = code_index
244
250
  @instance_methods = {} # { "String" => { "upcase" => MethodEntry } }
245
251
  @class_methods = {} # { "File" => { "read" => MethodEntry } }
246
252
  @converter = Converter::RBSConverter.new
@@ -265,19 +271,42 @@ module TypeGuessr
265
271
  end
266
272
 
267
273
  # Look up instance method entry
274
+ # Falls back to ancestor chain traversal when code_index is available
268
275
  # @param class_name [String] the class name
269
276
  # @param method_name [String] the method name
270
277
  # @return [MethodEntry, nil] method entry or nil if not found
271
278
  def lookup(class_name, method_name)
272
- @instance_methods.dig(class_name, method_name)
279
+ result = @instance_methods.dig(class_name, method_name)
280
+ return result if result
281
+
282
+ return nil unless @code_index
283
+
284
+ @code_index.ancestors_of(class_name).each do |ancestor|
285
+ next if ancestor == class_name
286
+
287
+ result = @instance_methods.dig(ancestor, method_name)
288
+ return result if result
289
+ end
290
+ nil
273
291
  end
274
292
 
275
293
  # Look up class method entry
294
+ # Falls back to ancestor chain traversal when code_index is available
276
295
  # @param class_name [String] the class name
277
296
  # @param method_name [String] the method name
278
297
  # @return [MethodEntry, nil] method entry or nil if not found
279
298
  def lookup_class_method(class_name, method_name)
280
- @class_methods.dig(class_name, method_name)
299
+ result = @class_methods.dig(class_name, method_name)
300
+ return result if result
301
+ return nil unless @code_index
302
+
303
+ @code_index.ancestors_of(class_name).each do |ancestor|
304
+ next if ancestor == class_name
305
+
306
+ result = @class_methods.dig(ancestor, method_name)
307
+ return result if result
308
+ end
309
+ nil
281
310
  end
282
311
 
283
312
  # Get method return type (convenience method matching old SignatureProvider API)
@@ -358,21 +387,33 @@ module TypeGuessr
358
387
  end
359
388
 
360
389
  # Register a single gem instance method
361
- # Skips if RBS entry already exists (RBS takes priority)
362
- def register_gem_method(class_name, method_name, return_type, params = [])
390
+ # Skips if RBS entry already exists (RBS takes priority).
391
+ # force: true overwrites existing GemMethodEntry (but never MethodEntry/RBS).
392
+ def register_gem_method(class_name, method_name, return_type, params = [], force: false)
363
393
  @instance_methods[class_name] ||= {}
364
- return if @instance_methods[class_name].key?(method_name)
365
394
 
366
- @instance_methods[class_name][method_name] = GemMethodEntry.new(return_type, params)
395
+ existing = @instance_methods[class_name][method_name]
396
+ if existing
397
+ return if existing.is_a?(MethodEntry)
398
+ return unless force
399
+ end
400
+
401
+ @instance_methods[class_name][method_name] = GemMethodEntry.new(return_type, params, skip_stdlib_rbs: force)
367
402
  end
368
403
 
369
404
  # Register a single gem class method
370
- # Skips if RBS entry already exists (RBS takes priority)
371
- def register_gem_class_method(class_name, method_name, return_type, params = [])
405
+ # Skips if RBS entry already exists (RBS takes priority).
406
+ # force: true overwrites existing GemMethodEntry (but never MethodEntry/RBS).
407
+ def register_gem_class_method(class_name, method_name, return_type, params = [], force: false)
372
408
  @class_methods[class_name] ||= {}
373
- return if @class_methods[class_name].key?(method_name)
374
409
 
375
- @class_methods[class_name][method_name] = GemMethodEntry.new(return_type, params)
410
+ existing = @class_methods[class_name][method_name]
411
+ if existing
412
+ return if existing.is_a?(MethodEntry)
413
+ return unless force
414
+ end
415
+
416
+ @class_methods[class_name][method_name] = GemMethodEntry.new(return_type, params, skip_stdlib_rbs: force)
376
417
  end
377
418
 
378
419
  # Bulk load gem cache data into the registry
@@ -447,9 +488,25 @@ module TypeGuessr
447
488
  env = RBS::Environment.from_loader(loader).resolve_type_names
448
489
  builder = RBS::DefinitionBuilder.new(env: env)
449
490
 
491
+ # Collect type parameter names for generic classes (e.g., "Set" => [:A])
492
+ # This lets RBSConverter build ClassInstance with named type_params
493
+ class_type_params = {}
494
+ env.class_decls.each do |type_name, entry|
495
+ params = entry.type_params.map(&:name)
496
+ class_type_params[type_name.to_s.delete_prefix("::")] = params if params.any?
497
+ end
498
+ @converter = Converter::RBSConverter.new(class_type_params)
499
+
450
500
  env.class_decls.each_key do |type_name|
451
501
  load_class_definitions(type_name, builder)
452
502
  end
503
+
504
+ env.class_alias_decls.each_value do |entry|
505
+ alias_name = entry.decl.new_name.to_s.delete_prefix("::")
506
+ original_name = entry.decl.old_name.to_s.delete_prefix("::")
507
+ @instance_methods[alias_name] ||= @instance_methods[original_name] if @instance_methods[original_name]
508
+ @class_methods[alias_name] ||= @class_methods[original_name] if @class_methods[original_name]
509
+ end
453
510
  rescue StandardError => e
454
511
  Logger.error("Failed to preload RBS environment", e)
455
512
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry/method_registry"
4
+ require_relative "registry/instance_variable_registry"
5
+ require_relative "registry/class_variable_registry"
6
+ require_relative "registry/signature_registry"
@@ -14,7 +14,9 @@ module TypeGuessr
14
14
  when Types::Unknown
15
15
  { "_type" => "Unknown" }
16
16
  when Types::ClassInstance
17
- { "_type" => "ClassInstance", "name" => type.name }
17
+ h = { "_type" => "ClassInstance", "name" => type.name }
18
+ h["type_params"] = type.type_params.to_h { |k, v| [k.to_s, serialize(v)] } if type.type_params&.any?
19
+ h
18
20
  when Types::SingletonType
19
21
  { "_type" => "SingletonType", "name" => type.name }
20
22
  when Types::TupleType
@@ -49,19 +51,21 @@ module TypeGuessr
49
51
  module_function def deserialize(hash)
50
52
  case hash["_type"]
51
53
  when "Unguessed" then Types::Unguessed.instance
52
- when "Unknown" then Types::Unknown.instance
53
- when "ClassInstance" then Types::ClassInstance.for(hash["name"])
54
- when "SingletonType" then Types::SingletonType.new(hash["name"])
55
- when "ArrayType" then Types::ArrayType.new(deserialize(hash["element_type"]))
56
- when "TupleType" then Types::TupleType.new(hash["element_types"].map { |t| deserialize(t) })
57
- when "HashType" then Types::HashType.new(deserialize(hash["key_type"]), deserialize(hash["value_type"]))
58
- when "RangeType" then Types::RangeType.new(deserialize(hash["element_type"]))
59
- when "HashShape" then Types::HashShape.new(hash["fields"].to_h { |k, v| [k.to_sym, deserialize(v)] })
60
- when "Union" then Types::Union.new(hash["types"].map { |t| deserialize(t) })
61
- when "TypeVariable" then Types::TypeVariable.new(hash["name"].to_sym)
62
- when "SelfType" then Types::SelfType.instance
63
- when "ForwardingArgs" then Types::ForwardingArgs.instance
64
- when "MethodSignature" then deserialize_method_signature(hash)
54
+ when "Unknown" then Types::Unknown.instance
55
+ when "ClassInstance"
56
+ type_params = hash["type_params"]&.to_h { |k, v| [k.to_sym, deserialize(v)] }
57
+ Types::ClassInstance.for(hash["name"], type_params)
58
+ when "SingletonType" then Types::SingletonType.new(hash["name"])
59
+ when "ArrayType" then Types::ArrayType.new(deserialize(hash["element_type"]))
60
+ when "TupleType" then Types::TupleType.new(hash["element_types"].map { |t| deserialize(t) })
61
+ when "HashType" then Types::HashType.new(deserialize(hash["key_type"]), deserialize(hash["value_type"]))
62
+ when "RangeType" then Types::RangeType.new(deserialize(hash["element_type"]))
63
+ when "HashShape" then Types::HashShape.new(hash["fields"].to_h { |k, v| [k.to_sym, deserialize(v)] })
64
+ when "Union" then Types::Union.new(hash["types"].map { |t| deserialize(t) })
65
+ when "TypeVariable" then Types::TypeVariable.new(hash["name"].to_sym)
66
+ when "SelfType" then Types::SelfType.instance
67
+ when "ForwardingArgs" then Types::ForwardingArgs.instance
68
+ when "MethodSignature" then deserialize_method_signature(hash)
65
69
  else
66
70
  raise ArgumentError, "Unknown type: #{hash["_type"]}"
67
71
  end
@@ -33,8 +33,8 @@ module TypeGuessr
33
33
  # 1. Single element: unwrap
34
34
  return types.first if types.size == 1
35
35
 
36
- # 2. Filter to most general types (remove children when parent is present)
37
- types = filter_to_most_general_types(types) if @code_index
36
+ # 2. Remove subclass types when parent class is also present
37
+ types = remove_subclass_types(types) if @code_index
38
38
 
39
39
  # 3. Check again after filtering
40
40
  return types.first if types.size == 1
@@ -45,10 +45,10 @@ module TypeGuessr
45
45
  Types::Union.new(types)
46
46
  end
47
47
 
48
- # Filter out types whose ancestor is also in the list
48
+ # Remove types whose ancestor is also in the list (e.g., remove Integer when Numeric is present)
49
49
  # @param types [Array<Types::Type>] List of types
50
- # @return [Array<Types::Type>] Filtered list with only the most general types
51
- private def filter_to_most_general_types(types)
50
+ # @return [Array<Types::Type>] Filtered list with subclass types removed
51
+ private def remove_subclass_types(types)
52
52
  # Extract class names from ClassInstance types
53
53
  class_names = types.filter_map do |t|
54
54
  t.name if t.is_a?(Types::ClassInstance)
@@ -36,7 +36,7 @@ module TypeGuessr
36
36
  # Get type variable substitutions for this type
37
37
  # Used for substituting type variables in block parameters
38
38
  # @return [Hash{Symbol => Type}] type variable substitutions (e.g., { Elem: Integer, K: Symbol, V: String })
39
- def type_variable_substitutions
39
+ def type_parameter_bindings
40
40
  {}
41
41
  end
42
42
 
@@ -68,42 +68,73 @@ module TypeGuessr
68
68
  end
69
69
 
70
70
  # ClassInstance - instance of a class
71
+ # @param type_params [Hash{Symbol => Type}, nil] type parameters (e.g., { A: String } for Set[String])
71
72
  class ClassInstance < Type
72
73
  CACHE = {} # rubocop:disable Style/MutableConstant
74
+ GENERIC_CACHE = {} # rubocop:disable Style/MutableConstant
73
75
 
74
76
  # Factory method that caches instances for reuse
75
77
  # @param name [String] The class name
78
+ # @param type_params [Hash{Symbol => Type}, nil] type parameters
76
79
  # @return [ClassInstance] Cached or new instance
77
- def self.for(name)
78
- CACHE[name] ||= new(name).freeze
80
+ def self.for(name, type_params = nil)
81
+ if type_params.nil?
82
+ CACHE[name] ||= new(name).freeze
83
+ else
84
+ key = [name, type_params]
85
+ GENERIC_CACHE[key] ||= new(name, type_params).freeze
86
+ end
79
87
  end
80
88
 
81
- attr_reader :name
89
+ attr_reader :name, :type_params
82
90
 
83
- def initialize(name)
91
+ def initialize(name, type_params = nil)
84
92
  super()
85
93
  @name = name
94
+ @type_params = type_params&.freeze
86
95
  end
87
96
 
88
97
  def eql?(other)
89
- super && @name == other.name
98
+ super && @name == other.name && @type_params == other.type_params
90
99
  end
91
100
 
92
101
  def hash
93
- [self.class, @name].hash
102
+ [self.class, @name, @type_params].hash
94
103
  end
95
104
 
96
105
  def to_s
97
- case @name
98
- when "NilClass" then "nil"
99
- when "TrueClass" then "true"
100
- when "FalseClass" then "false"
101
- else @name
106
+ base = case @name
107
+ when "NilClass" then "nil"
108
+ when "TrueClass" then "true"
109
+ when "FalseClass" then "false"
110
+ else @name
111
+ end
112
+ if @type_params&.any?
113
+ "#{base}[#{@type_params.values.join(", ")}]"
114
+ else
115
+ base
102
116
  end
103
117
  end
104
118
 
105
119
  def inspect
106
- "#<ClassInstance:#{@name}>"
120
+ if @type_params&.any?
121
+ "#<ClassInstance:#{@name}[#{@type_params.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")}]>"
122
+ else
123
+ "#<ClassInstance:#{@name}>"
124
+ end
125
+ end
126
+
127
+ def type_parameter_bindings
128
+ @type_params || {}
129
+ end
130
+
131
+ def substitute(substitutions)
132
+ return self if @type_params.nil? || @type_params.empty?
133
+
134
+ new_params = @type_params.transform_values { |t| t.substitute(substitutions) }
135
+ return self if new_params == @type_params
136
+
137
+ ClassInstance.new(@name, new_params)
107
138
  end
108
139
 
109
140
  def rbs_class_name
@@ -164,15 +195,17 @@ module TypeGuessr
164
195
  def to_s
165
196
  if bool_type?
166
197
  "bool"
198
+ elsif nullable_bool_type?
199
+ "bool?"
167
200
  elsif optional_type?
168
- "?#{non_nil_type}"
201
+ "#{non_nil_type}?"
169
202
  else
170
203
  @types.map(&:to_s).sort.join(" | ")
171
204
  end
172
205
  end
173
206
 
174
207
  def inspect
175
- "#<Union:#{@types.map(&:to_s).join("|")}>"
208
+ "#<Union:#{@types.join("|")}>"
176
209
  end
177
210
 
178
211
  def substitute(substitutions)
@@ -190,6 +223,15 @@ module TypeGuessr
190
223
  has_true && has_false
191
224
  end
192
225
 
226
+ private def nullable_bool_type?
227
+ return false unless @types.size == 3
228
+
229
+ has_true = @types.any? { |t| t.is_a?(ClassInstance) && t.name == "TrueClass" }
230
+ has_false = @types.any? { |t| t.is_a?(ClassInstance) && t.name == "FalseClass" }
231
+ has_nil = @types.any? { |t| nil_type?(t) }
232
+ has_true && has_false && has_nil
233
+ end
234
+
193
235
  private def optional_type?
194
236
  @types.size == 2 && @types.any? { |t| nil_type?(t) }
195
237
  end
@@ -225,7 +267,7 @@ module TypeGuessr
225
267
  private def simplify_if_unknown_present(types)
226
268
  return types if types.size <= 1
227
269
 
228
- has_unknown = types.any? { |t| t.is_a?(Unknown) }
270
+ has_unknown = types.any?(Unknown)
229
271
  has_unknown ? [Unknown.instance] : types
230
272
  end
231
273
 
@@ -270,7 +312,7 @@ module TypeGuessr
270
312
  "Array"
271
313
  end
272
314
 
273
- def type_variable_substitutions
315
+ def type_parameter_bindings
274
316
  { Elem: @element_type }
275
317
  end
276
318
  end
@@ -321,7 +363,7 @@ module TypeGuessr
321
363
  "Array"
322
364
  end
323
365
 
324
- def type_variable_substitutions
366
+ def type_parameter_bindings
325
367
  return { Elem: Unknown.instance } if @element_types.empty?
326
368
 
327
369
  unique = @element_types.uniq
@@ -368,7 +410,7 @@ module TypeGuessr
368
410
  "Hash"
369
411
  end
370
412
 
371
- def type_variable_substitutions
413
+ def type_parameter_bindings
372
414
  { K: @key_type, V: @value_type }
373
415
  end
374
416
  end
@@ -409,7 +451,7 @@ module TypeGuessr
409
451
  "Range"
410
452
  end
411
453
 
412
- def type_variable_substitutions
454
+ def type_parameter_bindings
413
455
  { Elem: @element_type }
414
456
  end
415
457
  end
@@ -469,7 +511,7 @@ module TypeGuessr
469
511
  "Hash"
470
512
  end
471
513
 
472
- def type_variable_substitutions
514
+ def type_parameter_bindings
473
515
  key_type = ClassInstance.for("Symbol")
474
516
  value_types = @fields.values.uniq
475
517
  value_type = if value_types.empty?
@@ -574,7 +616,7 @@ module TypeGuessr
574
616
  end
575
617
 
576
618
  def to_s
577
- params_str = @params.map(&:to_s).join(", ")
619
+ params_str = @params.join(", ")
578
620
  "(#{params_str}) -> #{@return_type}"
579
621
  end
580
622
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configuration
4
+ require_relative "core/config"
5
+
6
+ # Core type system
7
+ require_relative "core/types"
8
+
9
+ # Node infrastructure
10
+ require_relative "core/node_key_generator"
11
+ require_relative "core/node_context_helper"
12
+ require_relative "core/ir"
13
+ require_relative "core/index"
14
+
15
+ # Registries
16
+ require_relative "core/registry"
17
+
18
+ # Converters
19
+ require_relative "core/converter"
20
+
21
+ # Inference
22
+ require_relative "core/inference"
23
+
24
+ # Utilities
25
+ require_relative "core/signature_builder"
26
+ require_relative "core/type_simplifier"
27
+ require_relative "core/type_serializer"
28
+ require_relative "core/cache"
29
+ require_relative "core/logger"
@@ -10,26 +10,7 @@ require "ruby_indexer/ruby_indexer"
10
10
  require "ruby_lsp/internal"
11
11
 
12
12
  # Load core components
13
- require_relative "../core/types"
14
- require_relative "../core/node_key_generator"
15
- require_relative "../core/node_context_helper"
16
- require_relative "../core/ir/nodes"
17
- require_relative "../core/index/location_index"
18
- require_relative "../core/converter/prism_converter"
19
- require_relative "../core/converter/rbs_converter"
20
- require_relative "../core/inference/result"
21
- require_relative "../core/inference/resolver"
22
- require_relative "../core/registry/signature_registry"
23
- require_relative "../core/registry/method_registry"
24
- require_relative "../core/registry/instance_variable_registry"
25
- require_relative "../core/registry/class_variable_registry"
26
- require_relative "../core/signature_builder"
27
- require_relative "../core/type_simplifier"
28
- require_relative "../core/type_serializer"
29
- require_relative "../core/logger"
30
- require_relative "../core/cache/gem_signature_cache"
31
- require_relative "../core/cache/gem_dependency_resolver"
32
- require_relative "../core/cache/gem_signature_extractor"
13
+ require_relative "../core"
33
14
 
34
15
  # Load CodeIndexAdapter, StandaloneRuntime, and FileWatcher
35
16
  require_relative "../../ruby_lsp/type_guessr/code_index_adapter"
@@ -87,7 +68,8 @@ module TypeGuessr
87
68
 
88
69
  private def build_runtime(ruby_index)
89
70
  code_index = RubyLsp::TypeGuessr::CodeIndexAdapter.new(ruby_index)
90
- signature_registry = Core::Registry::SignatureRegistry.instance
71
+ signature_registry = Core::Registry::SignatureRegistry.new(code_index: code_index)
72
+ Core::Registry::SignatureRegistry.instance = signature_registry
91
73
 
92
74
  method_registry = Core::Registry::MethodRegistry.new(code_index: code_index)
93
75
  ivar_registry = Core::Registry::InstanceVariableRegistry.new(code_index: code_index)
@@ -380,49 +362,71 @@ module TypeGuessr
380
362
  end
381
363
 
382
364
  private def build_tools
383
- [build_infer_type_tool, build_get_method_signature_tool, build_search_methods_tool]
365
+ [build_get_method_sources_tool, build_get_method_signatures_tool, build_search_methods_tool]
384
366
  end
385
367
 
386
- private def build_infer_type_tool
368
+ private def build_get_method_sources_tool
387
369
  runtime = @runtime
388
370
  to_response = method(:json_response)
389
371
 
390
372
  ::MCP::Tool.define(
391
- name: "infer_type",
392
- description: "Infer the type of a variable, expression, or method at a specific location in a Ruby file. " \
393
- "Returns the guessed type based on heuristic analysis.",
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.",
394
378
  input_schema: {
395
379
  type: "object",
396
380
  properties: {
397
- file_path: { type: "string", description: "Absolute path to the Ruby file" },
398
- line: { type: "integer", description: "Line number (1-based)" },
399
- column: { type: "integer", description: "Column number (0-based)" }
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
+ }
400
393
  },
401
- required: %w[file_path line column]
394
+ required: %w[methods]
402
395
  }
403
- ) do |file_path:, line:, column:, **|
404
- to_response.call(runtime.infer_at(file_path, line, column))
396
+ ) do |methods:, **|
397
+ to_response.call(runtime.method_sources(methods))
405
398
  end
406
399
  end
407
400
 
408
- private def build_get_method_signature_tool
401
+ private def build_get_method_signatures_tool
409
402
  runtime = @runtime
410
403
  to_response = method(:json_response)
411
404
 
412
405
  ::MCP::Tool.define(
413
- name: "get_method_signature",
414
- description: "Get the inferred signature of a method defined in the project. " \
415
- "Returns parameter types and return type.",
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.",
416
410
  input_schema: {
417
411
  type: "object",
418
412
  properties: {
419
- class_name: { type: "string", description: "Fully qualified class name (e.g., 'User', 'Admin::User')" },
420
- method_name: { type: "string", description: "Method name (e.g., 'save', 'initialize')" }
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
+ }
421
425
  },
422
- required: %w[class_name method_name]
426
+ required: %w[methods]
423
427
  }
424
- ) do |class_name:, method_name:, **|
425
- to_response.call(runtime.method_signature(class_name, method_name))
428
+ ) do |methods:, **|
429
+ to_response.call(runtime.method_signatures(methods))
426
430
  end
427
431
  end
428
432
 
@@ -432,17 +436,22 @@ module TypeGuessr
432
436
 
433
437
  ::MCP::Tool.define(
434
438
  name: "search_methods",
435
- description: "Search for method definitions in the project. " \
436
- "Supports patterns like 'User#save', 'save', or 'Admin::*'.",
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.",
437
445
  input_schema: {
438
446
  type: "object",
439
447
  properties: {
440
- query: { type: "string", description: "Search query (e.g., 'User#save', 'save', 'initialize')" }
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)" }
441
450
  },
442
451
  required: %w[query]
443
452
  }
444
- ) do |query:, **|
445
- to_response.call(runtime.search_methods(query))
453
+ ) do |query:, include_signatures: false, **|
454
+ to_response.call(runtime.search_methods(query, include_signatures: include_signatures))
446
455
  end
447
456
  end
448
457