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
@@ -6,34 +6,8 @@ module RubyLsp
6
6
  class Hover
7
7
  # Core layer shortcuts
8
8
  Types = ::TypeGuessr::Core::Types
9
- private_constant :Types
10
-
11
- # Define all node types that should trigger hover content
12
- HOVER_NODE_TYPES = %i[
13
- local_variable_read
14
- local_variable_write
15
- local_variable_target
16
- instance_variable_read
17
- instance_variable_write
18
- instance_variable_target
19
- class_variable_read
20
- class_variable_write
21
- class_variable_target
22
- global_variable_read
23
- global_variable_write
24
- global_variable_target
25
- required_parameter
26
- optional_parameter
27
- rest_parameter
28
- required_keyword_parameter
29
- optional_keyword_parameter
30
- keyword_rest_parameter
31
- block_parameter
32
- forwarding_parameter
33
- call
34
- def
35
- self
36
- ].freeze
9
+ NodeContextHelper = ::TypeGuessr::Core::NodeContextHelper
10
+ private_constant :Types, :NodeContextHelper
37
11
 
38
12
  def initialize(runtime_adapter, response_builder, node_context, dispatcher, global_state)
39
13
  @runtime_adapter = runtime_adapter
@@ -45,18 +19,16 @@ module RubyLsp
45
19
  end
46
20
 
47
21
  # Dynamically define handler methods for each node type
48
- HOVER_NODE_TYPES.each do |node_type|
22
+ Constants::HOVER_NODE_MAPPING.each_key do |node_type|
49
23
  define_method(:"on_#{node_type}_node_enter") do |node|
50
24
  add_hover_content(node)
51
25
  end
52
26
  end
53
27
 
54
- private
55
-
56
- def register_listeners(dispatcher)
28
+ private def register_listeners(dispatcher)
57
29
  dispatcher.register(
58
30
  self,
59
- *HOVER_NODE_TYPES.map { |type| :"on_#{type}_node_enter" }
31
+ *Constants::HOVER_NODE_MAPPING.keys.map { |type| :"on_#{type}_node_enter" }
60
32
  )
61
33
  end
62
34
 
@@ -64,12 +36,12 @@ module RubyLsp
64
36
  IR = ::TypeGuessr::Core::IR
65
37
  private_constant :IR
66
38
 
67
- def add_hover_content(node)
39
+ private def add_hover_content(node)
68
40
  # Generate node_key from scope and Prism node
69
41
  # DefNode is indexed with parent scope (not including the method itself)
70
42
  exclude_method = node.is_a?(Prism::DefNode)
71
- scope_id = generate_scope_id(exclude_method: exclude_method)
72
- node_hash = generate_node_hash(node)
43
+ scope_id = NodeContextHelper.generate_scope_id(@node_context, exclude_method: exclude_method)
44
+ node_hash = NodeContextHelper.generate_node_hash(node, @node_context)
73
45
  return unless node_hash
74
46
 
75
47
  node_key = "#{scope_id}:#{node_hash}"
@@ -105,21 +77,19 @@ module RubyLsp
105
77
  warn "[TypeGuessr] Error in add_hover_content: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
106
78
  end
107
79
 
108
- def add_def_node_hover(def_node)
109
- # Build method signature: (params) -> return_type
110
- params_str = format_params(def_node.params)
111
- return_result = @runtime_adapter.infer_type(def_node)
112
- return_type_str = return_result.type.to_s
113
-
114
- signature = "(#{params_str}) -> #{return_type_str}"
115
- content = "**Guessed Signature:** `#{signature}`"
80
+ private def add_def_node_hover(def_node)
81
+ method_sig = @runtime_adapter.build_method_signature(def_node)
82
+ content = "**Guessed Signature:** `#{method_sig}`"
116
83
 
117
- content += build_debug_info(return_result) if debug_enabled?
84
+ if debug_enabled?
85
+ return_result = @runtime_adapter.infer_type(def_node)
86
+ content += build_debug_info(return_result)
87
+ end
118
88
 
119
89
  @response_builder.push(content, category: :documentation)
120
90
  end
121
91
 
122
- def add_call_node_hover(call_node)
92
+ private def add_call_node_hover(call_node)
123
93
  # Special case: Handle .new calls to show constructor signature
124
94
  # Support both ClassName.new (ConstantNode) and self.new (SelfNode) in singleton methods
125
95
  if call_node.method == :new &&
@@ -128,6 +98,15 @@ module RubyLsp
128
98
  return
129
99
  end
130
100
 
101
+ # Handle implicit self calls (receiver is nil)
102
+ unless call_node.receiver
103
+ def_node = lookup_def_node_for_implicit_self(call_node)
104
+ if def_node
105
+ add_def_node_hover(def_node)
106
+ return
107
+ end
108
+ end
109
+
131
110
  # Get receiver type to look up method signature
132
111
  if call_node.receiver
133
112
  # For ConstantNode receiver (e.g., File.exist?, RBS::Environment.from_loader),
@@ -142,34 +121,55 @@ module RubyLsp
142
121
  class_name = extract_class_name(receiver_type)
143
122
 
144
123
  if class_name
145
- # Look up signature via SignatureProvider
146
- # Use class method lookup for SingletonType (e.g., RBS::Environment.from_loader)
147
- signatures = if receiver_type.is_a?(Types::SingletonType)
148
- @runtime_adapter.signature_provider.get_class_method_signatures(
149
- class_name, call_node.method.to_s
150
- )
151
- else
152
- @runtime_adapter.signature_provider.get_method_signatures(
153
- class_name, call_node.method.to_s
154
- )
155
- end
156
-
157
- if signatures.any?
158
- # Format the signature(s)
159
- sig_strs = signatures.map { |sig| sig.method_type.to_s }
160
- content = "**Guessed Signature:** `#{sig_strs.first}`"
161
-
162
- if debug_enabled?
163
- content += "\n\n**[TypeGuessr Debug]**"
164
- content += "\n\n**Receiver:** `#{receiver_type}`"
165
- if sig_strs.size > 1
166
- content += "\n\n**Overloads:**\n"
167
- sig_strs.each { |s| content += "- `#{s}`\n" }
124
+ method_name_str = call_node.method.to_s
125
+ skip_stdlib_rbs = if receiver_type.is_a?(Types::SingletonType)
126
+ @runtime_adapter.skip_stdlib_rbs_class_method?(class_name, method_name_str)
127
+ else
128
+ @runtime_adapter.skip_stdlib_rbs_method?(class_name, method_name_str)
129
+ end
130
+
131
+ # Methods marked skip_stdlib_rbs skip DefNode + RBS → go straight to fallback (resolver)
132
+ unless skip_stdlib_rbs
133
+ # Try to find DefNode first (for project methods)
134
+ unless receiver_type.is_a?(Types::SingletonType)
135
+ def_node = @runtime_adapter.lookup_method(class_name, method_name_str)
136
+ if def_node
137
+ add_def_node_hover(def_node)
138
+ return
168
139
  end
169
140
  end
170
141
 
171
- @response_builder.push(content, category: :documentation)
172
- return
142
+ # Fall back to RBS signature lookup (for stdlib/gems)
143
+ rbs_result = if receiver_type.is_a?(Types::SingletonType)
144
+ @runtime_adapter.get_rbs_class_method_signatures(
145
+ class_name, method_name_str
146
+ )
147
+ else
148
+ @runtime_adapter.get_rbs_method_signatures(
149
+ class_name, method_name_str
150
+ )
151
+ end
152
+
153
+ signatures = rbs_result[:signatures]
154
+ owner = rbs_result[:owner]
155
+
156
+ if signatures.any?
157
+ sig_strs = signatures.map { |sig| sig.method_type.to_s }
158
+ content = "**Guessed Signature:** `#{sig_strs.first}`"
159
+
160
+ if debug_enabled?
161
+ content += "\n\n**[TypeGuessr Debug]**"
162
+ content += "\n\n**Receiver:** `#{receiver_type}`"
163
+ content += "\n\n**Defined in:** `#{owner}`" if owner != class_name
164
+ if sig_strs.size > 1
165
+ content += "\n\n**Overloads:**\n"
166
+ sig_strs.each { |s| content += "- `#{s}`\n" }
167
+ end
168
+ end
169
+
170
+ @response_builder.push(content, category: :documentation)
171
+ return
172
+ end
173
173
  end
174
174
  end
175
175
  end
@@ -187,7 +187,7 @@ module RubyLsp
187
187
  end
188
188
 
189
189
  # Build parameter signature for a method call using RubyIndexer
190
- def build_call_signature_params(call_node)
190
+ private def build_call_signature_params(call_node)
191
191
  method_entry = lookup_method_entry_for_call(call_node)
192
192
 
193
193
  if method_entry&.signatures&.any?
@@ -200,7 +200,7 @@ module RubyLsp
200
200
  end
201
201
 
202
202
  # Look up method entry from RubyIndexer based on call node
203
- def lookup_method_entry_for_call(call_node)
203
+ private def lookup_method_entry_for_call(call_node)
204
204
  return nil unless @global_state&.index
205
205
  return nil unless call_node.receiver
206
206
 
@@ -215,7 +215,7 @@ module RubyLsp
215
215
  end
216
216
 
217
217
  # Format parameters from RubyIndexer method entry with inferred argument types
218
- def format_params_from_entry(method_entry, args)
218
+ private def format_params_from_entry(method_entry, args)
219
219
  params = method_entry.signatures.first.parameters
220
220
  return "" if params.nil? || params.empty?
221
221
 
@@ -230,7 +230,7 @@ module RubyLsp
230
230
  end
231
231
 
232
232
  # Format a single parameter based on its type
233
- def format_single_param(param, arg_type)
233
+ private def format_single_param(param, arg_type)
234
234
  param_name = param.name.to_s
235
235
 
236
236
  case param
@@ -254,7 +254,7 @@ module RubyLsp
254
254
  end
255
255
 
256
256
  # Format arguments when no method entry is available
257
- def format_params_from_args(args)
257
+ private def format_params_from_args(args)
258
258
  args.each_with_index.map do |arg, i|
259
259
  arg_type = @runtime_adapter.infer_type(arg).type.to_s
260
260
  "#{arg_type} arg#{i + 1}"
@@ -262,12 +262,12 @@ module RubyLsp
262
262
  end
263
263
 
264
264
  # Look up class method entry from RubyIndexer
265
- def lookup_class_method_entry(class_name, method_name)
265
+ private def lookup_class_method_entry(class_name, method_name)
266
266
  return nil unless @global_state&.index
267
267
 
268
268
  # Query singleton class for the method
269
269
  # Ruby LSP uses unqualified name for singleton class (e.g., "RBS::Environment::<Class:Environment>")
270
- unqualified_name = class_name.split("::").last
270
+ unqualified_name = ::TypeGuessr::Core::IR.extract_last_name(class_name)
271
271
  singleton_name = "#{class_name}::<Class:#{unqualified_name}>"
272
272
  entries = @global_state.index.resolve_method(method_name, singleton_name)
273
273
  return nil if entries.nil? || entries.empty?
@@ -278,7 +278,7 @@ module RubyLsp
278
278
  end
279
279
 
280
280
  # Look up instance method entry from RubyIndexer
281
- def lookup_instance_method_entry(class_name, method_name)
281
+ private def lookup_instance_method_entry(class_name, method_name)
282
282
  return nil unless @global_state&.index
283
283
 
284
284
  entries = @global_state.index.resolve_method(method_name, class_name)
@@ -289,13 +289,29 @@ module RubyLsp
289
289
  nil
290
290
  end
291
291
 
292
- def extract_class_name(type)
292
+ # Look up DefNode for implicit self calls (receiver is nil)
293
+ # Searches in current class scope and falls back to top-level
294
+ private def lookup_def_node_for_implicit_self(call_node)
295
+ method_name = call_node.method.to_s
296
+
297
+ # Get current class scope from node_context
298
+ class_name = @node_context.nesting.map { |n| n.is_a?(String) ? n : n.name.to_s }.join("::")
299
+
300
+ # Try current class scope first
301
+ def_node = @runtime_adapter.lookup_method(class_name, method_name) if class_name && !class_name.empty?
302
+ return def_node if def_node
303
+
304
+ # Fall back to top-level (empty class name)
305
+ @runtime_adapter.lookup_method("", method_name)
306
+ end
307
+
308
+ private def extract_class_name(type)
293
309
  case type
294
310
  when Types::ClassInstance
295
311
  type.name
296
312
  when Types::SingletonType
297
313
  type.name
298
- when Types::ArrayType
314
+ when Types::ArrayType, Types::TupleType
299
315
  "Array"
300
316
  when Types::HashType, Types::HashShape
301
317
  "Hash"
@@ -303,99 +319,51 @@ module RubyLsp
303
319
  end
304
320
 
305
321
  # Handle .new calls to show constructor signature
306
- def add_new_call_hover(call_node)
307
- # Resolve constant to get class name (handles aliases)
308
- receiver_result = @runtime_adapter.infer_type(call_node.receiver)
309
- class_name = case receiver_result.type
310
- when Types::SingletonType then receiver_result.type.name
311
- else call_node.receiver.name
312
- end
313
-
314
- # Look up initialize method signature
315
- init_info = lookup_initialize_signature(class_name)
316
-
317
- content = if init_info
318
- case init_info[:source]
319
- when :project
320
- params_str = format_params(init_info[:params])
321
- "**Guessed Signature:** `(#{params_str}) -> #{class_name}`"
322
- when :rbs
323
- sig_str = init_info[:signature].method_type.to_s
324
- sig_str = sig_str.sub(/-> .+$/, "-> #{class_name}")
325
- "**Guessed Signature:** `#{sig_str}`"
326
- end
327
- else
328
- "**Guessed Signature:** `() -> #{class_name}`"
322
+ private def add_new_call_hover(call_node)
323
+ class_name = resolve_receiver_class_name(call_node.receiver)
324
+ result = @runtime_adapter.build_constructor_signature(class_name)
325
+
326
+ content = case result[:source]
327
+ when :project, :default
328
+ "**Guessed Signature:** `#{result[:signature]}`"
329
+ when :rbs
330
+ sig_str = result[:rbs_signature].method_type.to_s
331
+ sig_str = sig_str.sub(/-> .+$/, "-> #{class_name}")
332
+ "**Guessed Signature:** `#{sig_str}`"
329
333
  end
330
334
 
331
- content += build_debug_info_for_new(init_info, class_name) if debug_enabled?
335
+ content += build_debug_info_for_new(result[:source], class_name) if debug_enabled?
332
336
 
333
337
  @response_builder.push(content, category: :documentation)
334
338
  end
335
339
 
336
- # Look up initialize method signature for a class
337
- def lookup_initialize_signature(class_name)
338
- # 1. Try project methods first
339
- def_node = @runtime_adapter.lookup_method(class_name, "initialize")
340
- return { params: def_node.params, source: :project } if def_node
341
-
342
- # 2. Fall back to RBS signatures
343
- signatures = @runtime_adapter.signature_provider.get_method_signatures(
344
- class_name, "initialize"
345
- )
346
- return { signature: signatures.first, source: :rbs } if signatures.any?
340
+ # Resolve receiver to class name (handles aliases via type inference)
341
+ # Uses CodeIndexAdapter to resolve short constant names to fully qualified names
342
+ private def resolve_receiver_class_name(receiver)
343
+ receiver_result = @runtime_adapter.infer_type(receiver)
344
+ short_name = case receiver_result.type
345
+ when Types::SingletonType then receiver_result.type.name
346
+ else receiver.name
347
+ end
347
348
 
348
- nil
349
+ # Use CodeIndexAdapter to resolve full qualified name with nesting context
350
+ nesting = @node_context.nesting.map { |n| n.is_a?(String) ? n : n.name.to_s }
351
+ @runtime_adapter.resolve_constant_name(short_name, nesting) || short_name
349
352
  end
350
353
 
351
354
  # Build debug info for .new calls
352
- def build_debug_info_for_new(init_info, class_name)
355
+ private def build_debug_info_for_new(source, class_name)
353
356
  info = "\n\n**[TypeGuessr Debug]**"
354
357
  info += "\n\n**Class:** `#{class_name}`"
355
- info += "\n\n**Source:** #{init_info&.[](:source) || :inferred}"
358
+ info += "\n\n**Source:** #{source}"
356
359
  info
357
360
  end
358
361
 
359
- def format_params(params)
360
- return "" if params.nil? || params.empty?
361
-
362
- params.map do |param|
363
- param_type = infer_param_type(param)
364
- type_str = param_type.to_s
365
-
366
- case param.kind
367
- when :required
368
- "#{type_str} #{param.name}"
369
- when :optional
370
- "?#{type_str} #{param.name}"
371
- when :rest
372
- "*#{type_str} #{param.name}"
373
- when :keyword_required
374
- "#{param.name}: #{type_str}"
375
- when :keyword_optional
376
- "#{param.name}: ?#{type_str}"
377
- when :keyword_rest
378
- "**#{type_str} #{param.name}"
379
- when :block
380
- "&#{type_str} #{param.name}"
381
- when :forwarding
382
- "..."
383
- else
384
- "#{type_str} #{param.name}"
385
- end
386
- end.join(", ")
387
- end
388
-
389
- def infer_param_type(param)
390
- result = @runtime_adapter.infer_type(param)
391
- result.type
362
+ private def debug_enabled?
363
+ ::TypeGuessr::Core::Config.debug?
392
364
  end
393
365
 
394
- def debug_enabled?
395
- Config.debug?
396
- end
397
-
398
- def build_debug_info(result, ir_node = nil)
366
+ private def build_debug_info(result, ir_node = nil)
399
367
  info = "\n\n**[TypeGuessr Debug]**"
400
368
  info += "\n\n**Reason:** #{result.reason}"
401
369
  info += "\n\n**Source:** #{result.source}"
@@ -406,7 +374,7 @@ module RubyLsp
406
374
  info
407
375
  end
408
376
 
409
- def extract_called_methods(ir_node)
377
+ private def extract_called_methods(ir_node)
410
378
  case ir_node
411
379
  when IR::LocalWriteNode, IR::LocalReadNode,
412
380
  IR::InstanceVariableWriteNode, IR::InstanceVariableReadNode,
@@ -421,108 +389,8 @@ module RubyLsp
421
389
  end
422
390
  end
423
391
 
424
- # Generate scope_id from node_context
425
- # Format: "ClassName#method_name" or "ClassName" or "#method_name" or ""
426
- # @param exclude_method [Boolean] Whether to exclude method from scope (for DefNode)
427
- def generate_scope_id(exclude_method: false)
428
- class_path = @node_context.nesting.map do |n|
429
- n.is_a?(String) ? n : n.name.to_s
430
- end.join("::")
431
-
432
- method_name = exclude_method ? nil : @node_context.surrounding_method
433
-
434
- if method_name
435
- "#{class_path}##{method_name}"
436
- else
437
- class_path
438
- end
439
- end
440
-
441
- # Generate node_hash from Prism node to match IR node_hash format
442
- def generate_node_hash(node)
443
- line = node.location.start_line
444
- case node
445
- when Prism::LocalVariableWriteNode, Prism::LocalVariableTargetNode
446
- "local_write:#{node.name}:#{line}"
447
- when Prism::LocalVariableReadNode
448
- "local_read:#{node.name}:#{line}"
449
- when Prism::InstanceVariableWriteNode, Prism::InstanceVariableTargetNode
450
- "ivar_write:#{node.name}:#{line}"
451
- when Prism::InstanceVariableReadNode
452
- "ivar_read:#{node.name}:#{line}"
453
- when Prism::ClassVariableWriteNode, Prism::ClassVariableTargetNode
454
- "cvar_write:#{node.name}:#{line}"
455
- when Prism::ClassVariableReadNode
456
- "cvar_read:#{node.name}:#{line}"
457
- when Prism::GlobalVariableWriteNode, Prism::GlobalVariableTargetNode
458
- "global_write:#{node.name}:#{line}"
459
- when Prism::GlobalVariableReadNode
460
- "global_read:#{node.name}:#{line}"
461
- when Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
462
- Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
463
- Prism::KeywordRestParameterNode, Prism::BlockParameterNode
464
- # Check if this is a block parameter (parent is BlockParametersNode)
465
- if block_parameter?(node)
466
- index = block_parameter_index(node)
467
- "bparam:#{index}:#{line}"
468
- else
469
- "param:#{node.name}:#{line}"
470
- end
471
- when Prism::ForwardingParameterNode
472
- "param:...:#{line}"
473
- when Prism::CallNode
474
- # Use message_loc for accurate line number
475
- call_line = node.message_loc&.start_line || line
476
- "call:#{node.name}:#{call_line}"
477
- when Prism::DefNode
478
- # Use name_loc for accurate line number
479
- def_line = node.name_loc&.start_line || line
480
- "def:#{node.name}:#{def_line}"
481
- when Prism::SelfNode
482
- class_path = @node_context.nesting.map do |n|
483
- n.is_a?(String) ? n : n.name.to_s
484
- end.join("::")
485
- "self:#{class_path}:#{line}"
486
- end
487
- end
488
-
489
- # Check if a parameter node is inside a block (not a method definition)
490
- def block_parameter?(node)
491
- call_node = @node_context.call_node
492
- return false unless call_node&.block
493
-
494
- # Check if this parameter is in the block's parameters
495
- block_params = call_node.block.parameters&.parameters
496
- return false unless block_params
497
-
498
- all_params = collect_block_params(block_params)
499
- all_params.include?(node)
500
- end
501
-
502
- # Get the index of a block parameter
503
- def block_parameter_index(node)
504
- call_node = @node_context.call_node
505
- return 0 unless call_node&.block
506
-
507
- block_params = call_node.block.parameters&.parameters
508
- return 0 unless block_params
509
-
510
- all_params = collect_block_params(block_params)
511
- all_params.index(node) || 0
512
- end
513
-
514
- # Collect all positional parameters from a ParametersNode
515
- def collect_block_params(params_node)
516
- all_params = []
517
- all_params.concat(params_node.requireds || [])
518
- all_params.concat(params_node.optionals || [])
519
- all_params << params_node.rest if params_node.rest
520
- all_params.concat(params_node.posts || [])
521
- all_params
522
- end
523
-
524
392
  # Format type with definition link if available
525
- def format_type_with_link(type)
393
+ private def format_type_with_link(type)
526
394
  formatted = type.to_s
527
395
 
528
396
  # Only link ClassInstance types
@@ -539,7 +407,7 @@ module RubyLsp
539
407
  end
540
408
 
541
409
  # Find entry for a type name in RubyIndexer
542
- def find_type_entry(type_name)
410
+ private def find_type_entry(type_name)
543
411
  return nil unless @global_state&.index
544
412
 
545
413
  entries = @global_state.index.resolve(type_name, [])
@@ -550,7 +418,7 @@ module RubyLsp
550
418
  end
551
419
 
552
420
  # Build a location link from an entry
553
- def build_location_link(entry)
421
+ private def build_location_link(entry)
554
422
  uri = entry.uri
555
423
  return nil if uri.nil?
556
424
 
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is loaded in the Rails runner process (not the LSP server)
4
+ # via RunnerClient.register_server_addon.
5
+ # RubyLsp::Rails::ServerAddon is already defined when this file is required.
6
+
7
+ module RubyLsp
8
+ module TypeGuessr
9
+ # ServerAddon that runs inside the Rails runner process.
10
+ # Queries ActiveRecord runtime for model metadata (columns, enums, associations, scopes).
11
+ class RailsServerAddon < ::RubyLsp::Rails::ServerAddon
12
+ def name
13
+ "TypeGuessr"
14
+ end
15
+
16
+ def execute(request, params)
17
+ case request
18
+ when "model_metadata"
19
+ handle_model_metadata(params)
20
+ end
21
+ end
22
+
23
+ private def handle_model_metadata(params)
24
+ class_name = params[:name]
25
+ klass = class_name.constantize
26
+
27
+ return send_error_response("#{class_name} is not an ActiveRecord model") unless active_record_model?(klass)
28
+
29
+ send_result({
30
+ columns: extract_columns(klass),
31
+ enums: extract_enums(klass),
32
+ associations: extract_associations(klass),
33
+ scopes: extract_scopes(klass)
34
+ })
35
+ rescue NameError
36
+ send_error_response("#{class_name} is not a valid class")
37
+ rescue StandardError => e
38
+ send_error_response("#{e.class}: #{e.message}")
39
+ end
40
+
41
+ private def active_record_model?(klass)
42
+ klass < ::ActiveRecord::Base
43
+ rescue StandardError
44
+ false
45
+ end
46
+
47
+ private def extract_columns(klass)
48
+ klass.columns.map do |col|
49
+ [col.name, col.type.to_s, col.null]
50
+ end
51
+ end
52
+
53
+ private def extract_enums(klass)
54
+ return {} unless klass.respond_to?(:defined_enums)
55
+
56
+ klass.defined_enums
57
+ end
58
+
59
+ private def extract_associations(klass)
60
+ klass.reflect_on_all_associations.map do |assoc|
61
+ {
62
+ name: assoc.name.to_s,
63
+ macro: assoc.macro.to_s,
64
+ class_name: assoc.class_name
65
+ }
66
+ end
67
+ rescue StandardError
68
+ []
69
+ end
70
+
71
+ private def extract_scopes(klass)
72
+ enum_values = klass.respond_to?(:defined_enums) ? klass.defined_enums.values.flat_map(&:keys) : []
73
+ exclude = Set.new(enum_values + enum_values.map { |v| "not_#{v}" })
74
+
75
+ klass.singleton_methods(false)
76
+ .map(&:to_s)
77
+ .reject { |m| exclude.include?(m) || m.start_with?("find_by_", "create_", "build_") }
78
+ rescue StandardError
79
+ []
80
+ end
81
+ end
82
+ end
83
+ end