type-guessr 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +138 -0
- data/lib/ruby_lsp/type_guessr/config.rb +90 -0
- data/lib/ruby_lsp/type_guessr/debug_server.rb +861 -0
- data/lib/ruby_lsp/type_guessr/graph_builder.rb +349 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +565 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +506 -0
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +200 -0
- data/lib/type-guessr.rb +28 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +1649 -0
- data/lib/type_guessr/core/converter/rbs_converter.rb +88 -0
- data/lib/type_guessr/core/index/location_index.rb +72 -0
- data/lib/type_guessr/core/inference/resolver.rb +664 -0
- data/lib/type_guessr/core/inference/result.rb +41 -0
- data/lib/type_guessr/core/ir/nodes.rb +599 -0
- data/lib/type_guessr/core/logger.rb +43 -0
- data/lib/type_guessr/core/rbs_provider.rb +304 -0
- data/lib/type_guessr/core/registry/method_registry.rb +106 -0
- data/lib/type_guessr/core/registry/variable_registry.rb +87 -0
- data/lib/type_guessr/core/signature_provider.rb +101 -0
- data/lib/type_guessr/core/type_simplifier.rb +64 -0
- data/lib/type_guessr/core/types.rb +425 -0
- data/lib/type_guessr/version.rb +5 -0
- metadata +81 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module TypeGuessr
|
|
5
|
+
# Hover provider for TypeGuessr
|
|
6
|
+
class Hover
|
|
7
|
+
# Core layer shortcuts
|
|
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
|
|
37
|
+
|
|
38
|
+
def initialize(runtime_adapter, response_builder, node_context, dispatcher, global_state)
|
|
39
|
+
@runtime_adapter = runtime_adapter
|
|
40
|
+
@response_builder = response_builder
|
|
41
|
+
@node_context = node_context
|
|
42
|
+
@global_state = global_state
|
|
43
|
+
|
|
44
|
+
register_listeners(dispatcher)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Dynamically define handler methods for each node type
|
|
48
|
+
HOVER_NODE_TYPES.each do |node_type|
|
|
49
|
+
define_method(:"on_#{node_type}_node_enter") do |node|
|
|
50
|
+
add_hover_content(node)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def register_listeners(dispatcher)
|
|
57
|
+
dispatcher.register(
|
|
58
|
+
self,
|
|
59
|
+
*HOVER_NODE_TYPES.map { |type| :"on_#{type}_node_enter" }
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Core IR module shortcut
|
|
64
|
+
IR = ::TypeGuessr::Core::IR
|
|
65
|
+
private_constant :IR
|
|
66
|
+
|
|
67
|
+
def add_hover_content(node)
|
|
68
|
+
# Generate node_key from scope and Prism node
|
|
69
|
+
# DefNode is indexed with parent scope (not including the method itself)
|
|
70
|
+
exclude_method = node.is_a?(Prism::DefNode)
|
|
71
|
+
scope_id = generate_scope_id(exclude_method: exclude_method)
|
|
72
|
+
node_hash = generate_node_hash(node)
|
|
73
|
+
return unless node_hash
|
|
74
|
+
|
|
75
|
+
node_key = "#{scope_id}:#{node_hash}"
|
|
76
|
+
|
|
77
|
+
# Find IR node by key (O(1) lookup)
|
|
78
|
+
ir_node = @runtime_adapter.find_node_by_key(node_key)
|
|
79
|
+
return unless ir_node
|
|
80
|
+
|
|
81
|
+
# Handle DefNode specially - show method signature
|
|
82
|
+
if ir_node.is_a?(IR::DefNode)
|
|
83
|
+
add_def_node_hover(ir_node)
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Handle CallNode specially - show RBS method signature
|
|
88
|
+
if ir_node.is_a?(IR::CallNode)
|
|
89
|
+
add_call_node_hover(ir_node)
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Infer type
|
|
94
|
+
result = @runtime_adapter.infer_type(ir_node)
|
|
95
|
+
|
|
96
|
+
# Format type with definition link if available
|
|
97
|
+
formatted_type = format_type_with_link(result.type)
|
|
98
|
+
|
|
99
|
+
# Build hover content
|
|
100
|
+
content = "**Guessed Type:** #{formatted_type}"
|
|
101
|
+
content += build_debug_info(result, ir_node) if debug_enabled?
|
|
102
|
+
|
|
103
|
+
@response_builder.push(content, category: :documentation)
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
warn "[TypeGuessr] Error in add_hover_content: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
|
106
|
+
end
|
|
107
|
+
|
|
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}`"
|
|
116
|
+
|
|
117
|
+
content += build_debug_info(return_result) if debug_enabled?
|
|
118
|
+
|
|
119
|
+
@response_builder.push(content, category: :documentation)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def add_call_node_hover(call_node)
|
|
123
|
+
# Special case: Handle .new calls to show constructor signature
|
|
124
|
+
# Support both ClassName.new (ConstantNode) and self.new (SelfNode) in singleton methods
|
|
125
|
+
if call_node.method == :new &&
|
|
126
|
+
(call_node.receiver.is_a?(IR::ConstantNode) || call_node.receiver.is_a?(IR::SelfNode))
|
|
127
|
+
add_new_call_hover(call_node)
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get receiver type to look up method signature
|
|
132
|
+
if call_node.receiver
|
|
133
|
+
# For ConstantNode receiver (e.g., File.exist?, RBS::Environment.from_loader),
|
|
134
|
+
# directly create SingletonType without relying on constant_kind_provider
|
|
135
|
+
receiver_type = if call_node.receiver.is_a?(IR::ConstantNode)
|
|
136
|
+
Types::SingletonType.new(call_node.receiver.name)
|
|
137
|
+
else
|
|
138
|
+
@runtime_adapter.infer_type(call_node.receiver).type
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get the class name for signature lookup
|
|
142
|
+
class_name = extract_class_name(receiver_type)
|
|
143
|
+
|
|
144
|
+
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" }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
@response_builder.push(content, category: :documentation)
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Fallback: show inferred return type as signature format
|
|
178
|
+
# All method calls should show signature format (not just project methods)
|
|
179
|
+
result = @runtime_adapter.infer_type(call_node)
|
|
180
|
+
type_str = result.type.to_s
|
|
181
|
+
|
|
182
|
+
# Build signature with parameter info from RubyIndexer
|
|
183
|
+
params_str = build_call_signature_params(call_node)
|
|
184
|
+
content = "**Guessed Signature:** `(#{params_str}) -> #{type_str}`"
|
|
185
|
+
content += build_debug_info(result) if debug_enabled?
|
|
186
|
+
@response_builder.push(content, category: :documentation)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Build parameter signature for a method call using RubyIndexer
|
|
190
|
+
def build_call_signature_params(call_node)
|
|
191
|
+
method_entry = lookup_method_entry_for_call(call_node)
|
|
192
|
+
|
|
193
|
+
if method_entry&.signatures&.any?
|
|
194
|
+
format_params_from_entry(method_entry, call_node.args)
|
|
195
|
+
elsif call_node.args&.any?
|
|
196
|
+
format_params_from_args(call_node.args)
|
|
197
|
+
else
|
|
198
|
+
""
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Look up method entry from RubyIndexer based on call node
|
|
203
|
+
def lookup_method_entry_for_call(call_node)
|
|
204
|
+
return nil unless @global_state&.index
|
|
205
|
+
return nil unless call_node.receiver
|
|
206
|
+
|
|
207
|
+
receiver_result = @runtime_adapter.infer_type(call_node.receiver)
|
|
208
|
+
|
|
209
|
+
case receiver_result.type
|
|
210
|
+
when Types::SingletonType
|
|
211
|
+
lookup_class_method_entry(receiver_result.type.name, call_node.method.to_s)
|
|
212
|
+
when Types::ClassInstance
|
|
213
|
+
lookup_instance_method_entry(receiver_result.type.name, call_node.method.to_s)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Format parameters from RubyIndexer method entry with inferred argument types
|
|
218
|
+
def format_params_from_entry(method_entry, args)
|
|
219
|
+
params = method_entry.signatures.first.parameters
|
|
220
|
+
return "" if params.nil? || params.empty?
|
|
221
|
+
|
|
222
|
+
params.each_with_index.map do |param, i|
|
|
223
|
+
arg_type = if args && i < args.size
|
|
224
|
+
@runtime_adapter.infer_type(args[i]).type.to_s
|
|
225
|
+
else
|
|
226
|
+
"untyped"
|
|
227
|
+
end
|
|
228
|
+
format_single_param(param, arg_type)
|
|
229
|
+
end.join(", ")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Format a single parameter based on its type
|
|
233
|
+
def format_single_param(param, arg_type)
|
|
234
|
+
param_name = param.name.to_s
|
|
235
|
+
|
|
236
|
+
case param
|
|
237
|
+
when RubyIndexer::Entry::RequiredParameter
|
|
238
|
+
"#{arg_type} #{param_name}"
|
|
239
|
+
when RubyIndexer::Entry::OptionalParameter
|
|
240
|
+
"?#{arg_type} #{param_name}"
|
|
241
|
+
when RubyIndexer::Entry::RestParameter
|
|
242
|
+
"*#{arg_type} #{param_name}"
|
|
243
|
+
when RubyIndexer::Entry::KeywordParameter
|
|
244
|
+
"#{param_name}: #{arg_type}"
|
|
245
|
+
when RubyIndexer::Entry::OptionalKeywordParameter
|
|
246
|
+
"?#{param_name}: #{arg_type}"
|
|
247
|
+
when RubyIndexer::Entry::KeywordRestParameter
|
|
248
|
+
"**#{arg_type} #{param_name}"
|
|
249
|
+
when RubyIndexer::Entry::BlockParameter
|
|
250
|
+
"&#{param_name}"
|
|
251
|
+
else
|
|
252
|
+
"#{arg_type} #{param_name}"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Format arguments when no method entry is available
|
|
257
|
+
def format_params_from_args(args)
|
|
258
|
+
args.each_with_index.map do |arg, i|
|
|
259
|
+
arg_type = @runtime_adapter.infer_type(arg).type.to_s
|
|
260
|
+
"#{arg_type} arg#{i + 1}"
|
|
261
|
+
end.join(", ")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Look up class method entry from RubyIndexer
|
|
265
|
+
def lookup_class_method_entry(class_name, method_name)
|
|
266
|
+
return nil unless @global_state&.index
|
|
267
|
+
|
|
268
|
+
# Query singleton class for the method
|
|
269
|
+
# Ruby LSP uses unqualified name for singleton class (e.g., "RBS::Environment::<Class:Environment>")
|
|
270
|
+
unqualified_name = class_name.split("::").last
|
|
271
|
+
singleton_name = "#{class_name}::<Class:#{unqualified_name}>"
|
|
272
|
+
entries = @global_state.index.resolve_method(method_name, singleton_name)
|
|
273
|
+
return nil if entries.nil? || entries.empty?
|
|
274
|
+
|
|
275
|
+
entries.first
|
|
276
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Look up instance method entry from RubyIndexer
|
|
281
|
+
def lookup_instance_method_entry(class_name, method_name)
|
|
282
|
+
return nil unless @global_state&.index
|
|
283
|
+
|
|
284
|
+
entries = @global_state.index.resolve_method(method_name, class_name)
|
|
285
|
+
return nil if entries.nil? || entries.empty?
|
|
286
|
+
|
|
287
|
+
entries.first
|
|
288
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def extract_class_name(type)
|
|
293
|
+
case type
|
|
294
|
+
when Types::ClassInstance
|
|
295
|
+
type.name
|
|
296
|
+
when Types::SingletonType
|
|
297
|
+
type.name
|
|
298
|
+
when Types::ArrayType
|
|
299
|
+
"Array"
|
|
300
|
+
when Types::HashType, Types::HashShape
|
|
301
|
+
"Hash"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# 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}`"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
content += build_debug_info_for_new(init_info, class_name) if debug_enabled?
|
|
332
|
+
|
|
333
|
+
@response_builder.push(content, category: :documentation)
|
|
334
|
+
end
|
|
335
|
+
|
|
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?
|
|
347
|
+
|
|
348
|
+
nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Build debug info for .new calls
|
|
352
|
+
def build_debug_info_for_new(init_info, class_name)
|
|
353
|
+
info = "\n\n**[TypeGuessr Debug]**"
|
|
354
|
+
info += "\n\n**Class:** `#{class_name}`"
|
|
355
|
+
info += "\n\n**Source:** #{init_info&.[](:source) || :inferred}"
|
|
356
|
+
info
|
|
357
|
+
end
|
|
358
|
+
|
|
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
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def debug_enabled?
|
|
395
|
+
Config.debug?
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def build_debug_info(result, ir_node = nil)
|
|
399
|
+
info = "\n\n**[TypeGuessr Debug]**"
|
|
400
|
+
info += "\n\n**Reason:** #{result.reason}"
|
|
401
|
+
info += "\n\n**Source:** #{result.source}"
|
|
402
|
+
if ir_node
|
|
403
|
+
called_methods = extract_called_methods(ir_node)
|
|
404
|
+
info += "\n\n**Method calls:** #{called_methods.join(", ")}" if called_methods.any?
|
|
405
|
+
end
|
|
406
|
+
info
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def extract_called_methods(ir_node)
|
|
410
|
+
case ir_node
|
|
411
|
+
when IR::LocalWriteNode, IR::LocalReadNode,
|
|
412
|
+
IR::InstanceVariableWriteNode, IR::InstanceVariableReadNode,
|
|
413
|
+
IR::ClassVariableWriteNode, IR::ClassVariableReadNode,
|
|
414
|
+
IR::ParamNode
|
|
415
|
+
ir_node.called_methods || []
|
|
416
|
+
when IR::BlockParamSlot
|
|
417
|
+
# For block params, check the underlying param node
|
|
418
|
+
[]
|
|
419
|
+
else
|
|
420
|
+
[]
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
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
|
+
# Format type with definition link if available
|
|
525
|
+
def format_type_with_link(type)
|
|
526
|
+
formatted = type.to_s
|
|
527
|
+
|
|
528
|
+
# Only link ClassInstance types
|
|
529
|
+
return "`#{formatted}`" unless type.is_a?(Types::ClassInstance)
|
|
530
|
+
|
|
531
|
+
# Try to find the class definition in the index
|
|
532
|
+
entry = find_type_entry(type.name)
|
|
533
|
+
return "`#{formatted}`" unless entry
|
|
534
|
+
|
|
535
|
+
location_link = build_location_link(entry)
|
|
536
|
+
return "`#{formatted}`" unless location_link
|
|
537
|
+
|
|
538
|
+
"[`#{formatted}`](#{location_link})"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Find entry for a type name in RubyIndexer
|
|
542
|
+
def find_type_entry(type_name)
|
|
543
|
+
return nil unless @global_state&.index
|
|
544
|
+
|
|
545
|
+
entries = @global_state.index.resolve(type_name, [])
|
|
546
|
+
return nil if entries.nil? || entries.empty?
|
|
547
|
+
|
|
548
|
+
# Return the first class/module entry
|
|
549
|
+
entries.find { |e| e.is_a?(RubyIndexer::Entry::Namespace) }
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Build a location link from an entry
|
|
553
|
+
def build_location_link(entry)
|
|
554
|
+
uri = entry.uri
|
|
555
|
+
return nil if uri.nil?
|
|
556
|
+
|
|
557
|
+
location = entry.respond_to?(:name_location) ? entry.name_location : entry.location
|
|
558
|
+
return nil if location.nil?
|
|
559
|
+
|
|
560
|
+
"#{uri}#L#{location.start_line},#{location.start_column + 1}-" \
|
|
561
|
+
"#{location.end_line},#{location.end_column + 1}"
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
end
|