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,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module TypeGuessr
|
|
5
|
+
# Builds graph data from IR nodes for visualization
|
|
6
|
+
# Uses body_nodes structure with value/receiver/args edges
|
|
7
|
+
class GraphBuilder
|
|
8
|
+
def initialize(runtime_adapter)
|
|
9
|
+
@runtime_adapter = runtime_adapter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Build graph data starting from a node key
|
|
13
|
+
# @param node_key [String] The starting node key (e.g., "User#save:def:save:10")
|
|
14
|
+
# @return [Hash, nil] Graph data with nodes and edges, or nil if node not found
|
|
15
|
+
def build(node_key)
|
|
16
|
+
root_node = @runtime_adapter.find_node_by_key(node_key)
|
|
17
|
+
return nil unless root_node
|
|
18
|
+
|
|
19
|
+
@nodes = {}
|
|
20
|
+
@edges = []
|
|
21
|
+
@scope_id = extract_scope_id(node_key)
|
|
22
|
+
|
|
23
|
+
if root_node.is_a?(::TypeGuessr::Core::IR::DefNode)
|
|
24
|
+
build_def_node_graph(root_node, node_key)
|
|
25
|
+
else
|
|
26
|
+
# Fallback for non-DefNode: use BFS with dependencies
|
|
27
|
+
traverse_dependencies(root_node, node_key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result = {
|
|
31
|
+
nodes: @nodes.values,
|
|
32
|
+
edges: @edges,
|
|
33
|
+
root_key: node_key
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
result[:def_node_inspect] = root_node.tree_inspect(root: true) if root_node.is_a?(::TypeGuessr::Core::IR::DefNode)
|
|
37
|
+
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Limit to MAX_NODES to prevent infinite/huge graphs
|
|
42
|
+
MAX_NODES = 200
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_def_node_graph(def_node, def_key)
|
|
47
|
+
method_scope = @scope_id.empty? ? "##{def_node.name}" : "#{@scope_id}##{def_node.name}"
|
|
48
|
+
|
|
49
|
+
# 1. Add DefNode
|
|
50
|
+
add_node(def_node, @scope_id)
|
|
51
|
+
|
|
52
|
+
# 2. Add params and connect to DefNode
|
|
53
|
+
def_node.params&.each do |param|
|
|
54
|
+
param_key = add_node(param, method_scope)
|
|
55
|
+
add_edge(param_key, def_key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 3. Process body_nodes - collect all nodes and create edges based on value/receiver/args
|
|
59
|
+
return_sources = []
|
|
60
|
+
last_body_key = nil
|
|
61
|
+
def_node.body_nodes&.each do |body_node|
|
|
62
|
+
last_body_key = process_body_node(body_node, method_scope, return_sources)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Add last expression as implicit return (if not already a ReturnNode)
|
|
66
|
+
return_sources << last_body_key if last_body_key && !return_sources.include?(last_body_key)
|
|
67
|
+
|
|
68
|
+
# 4. Create virtual Return node and connect return sources
|
|
69
|
+
return unless return_sources.any?
|
|
70
|
+
|
|
71
|
+
return_key = "#{method_scope}:return:virtual"
|
|
72
|
+
@nodes[return_key] = {
|
|
73
|
+
key: return_key,
|
|
74
|
+
type: "Return",
|
|
75
|
+
line: def_node.loc&.line,
|
|
76
|
+
inferred_type: infer_type_str(def_node),
|
|
77
|
+
details: { virtual: true }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return_sources.each do |source_key|
|
|
81
|
+
add_edge(source_key, return_key)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
add_edge(return_key, def_key)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def process_body_node(node, scope_id, return_sources)
|
|
88
|
+
return unless node
|
|
89
|
+
return if @nodes.size >= MAX_NODES
|
|
90
|
+
|
|
91
|
+
node_key = add_node(node, scope_id)
|
|
92
|
+
|
|
93
|
+
# Track return sources
|
|
94
|
+
return_sources << node_key if node.is_a?(::TypeGuessr::Core::IR::ReturnNode)
|
|
95
|
+
|
|
96
|
+
# Create edges based on node structure (value, receiver, args)
|
|
97
|
+
case node
|
|
98
|
+
when ::TypeGuessr::Core::IR::LocalWriteNode,
|
|
99
|
+
::TypeGuessr::Core::IR::InstanceVariableWriteNode,
|
|
100
|
+
::TypeGuessr::Core::IR::ClassVariableWriteNode
|
|
101
|
+
if node.value
|
|
102
|
+
value_key = process_body_node(node.value, scope_id, return_sources)
|
|
103
|
+
add_edge(value_key, node_key) if value_key
|
|
104
|
+
end
|
|
105
|
+
when ::TypeGuessr::Core::IR::CallNode
|
|
106
|
+
if node.receiver
|
|
107
|
+
receiver_key = process_body_node(node.receiver, scope_id, return_sources)
|
|
108
|
+
add_edge(receiver_key, node_key) if receiver_key
|
|
109
|
+
end
|
|
110
|
+
node.args&.each do |arg|
|
|
111
|
+
next unless arg
|
|
112
|
+
|
|
113
|
+
arg_key = process_body_node(arg, scope_id, return_sources)
|
|
114
|
+
add_edge(arg_key, node_key) if arg_key
|
|
115
|
+
end
|
|
116
|
+
when ::TypeGuessr::Core::IR::ReturnNode
|
|
117
|
+
if node.value
|
|
118
|
+
value_key = process_body_node(node.value, scope_id, return_sources)
|
|
119
|
+
add_edge(value_key, node_key) if value_key
|
|
120
|
+
end
|
|
121
|
+
when ::TypeGuessr::Core::IR::LocalReadNode
|
|
122
|
+
# LocalReadNode references its write_node (variable definition)
|
|
123
|
+
if node.write_node
|
|
124
|
+
write_key = process_body_node(node.write_node, scope_id, return_sources)
|
|
125
|
+
add_edge(write_key, node_key) if write_key
|
|
126
|
+
end
|
|
127
|
+
when ::TypeGuessr::Core::IR::InstanceVariableReadNode,
|
|
128
|
+
::TypeGuessr::Core::IR::ClassVariableReadNode
|
|
129
|
+
# Instance/class variable reads may reference writes in other methods
|
|
130
|
+
# We don't follow these edges in the graph to avoid complexity
|
|
131
|
+
nil
|
|
132
|
+
when ::TypeGuessr::Core::IR::ParamNode
|
|
133
|
+
# ParamNode is a leaf node, no edges to create
|
|
134
|
+
nil
|
|
135
|
+
when ::TypeGuessr::Core::IR::MergeNode
|
|
136
|
+
node.branches&.each do |branch|
|
|
137
|
+
branch_key = process_body_node(branch, scope_id, return_sources)
|
|
138
|
+
add_edge(branch_key, node_key) if branch_key
|
|
139
|
+
end
|
|
140
|
+
when ::TypeGuessr::Core::IR::LiteralNode
|
|
141
|
+
# Process internal values (for Hash/Array literals with expressions) # -- values is an Array attribute, not Hash#values
|
|
142
|
+
node.values&.each do |value_node|
|
|
143
|
+
value_key = process_body_node(value_node, scope_id, return_sources)
|
|
144
|
+
add_edge(value_key, node_key) if value_key
|
|
145
|
+
end
|
|
146
|
+
# rubocop:enable Style/HashEachMethods
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
node_key
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def add_node(node, scope_id = @scope_id)
|
|
153
|
+
node_key = node.node_key(scope_id)
|
|
154
|
+
return node_key if @nodes.key?(node_key)
|
|
155
|
+
|
|
156
|
+
@nodes[node_key] = serialize_node(node, node_key)
|
|
157
|
+
node_key
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_edge(from_key, to_key)
|
|
161
|
+
return unless from_key && to_key
|
|
162
|
+
|
|
163
|
+
edge = { from: from_key, to: to_key }
|
|
164
|
+
@edges << edge unless @edges.include?(edge)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def infer_type_str(node)
|
|
168
|
+
result = @runtime_adapter.infer_type(node)
|
|
169
|
+
result.type.to_s
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# BFS traversal for non-DefNode (fallback)
|
|
173
|
+
def traverse_dependencies(node, node_key)
|
|
174
|
+
visited = Set.new
|
|
175
|
+
queue = [[node, node_key]]
|
|
176
|
+
|
|
177
|
+
while (current, current_key = queue.shift)
|
|
178
|
+
next if visited.include?(current_key)
|
|
179
|
+
break if visited.size >= MAX_NODES
|
|
180
|
+
|
|
181
|
+
visited.add(current_key)
|
|
182
|
+
add_node(current, @scope_id)
|
|
183
|
+
|
|
184
|
+
current.dependencies.each do |dep_node|
|
|
185
|
+
next unless dep_node
|
|
186
|
+
|
|
187
|
+
dep_key = dep_node.node_key(@scope_id)
|
|
188
|
+
add_edge(current_key, dep_key)
|
|
189
|
+
queue << [dep_node, dep_key]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Extract scope_id from a node key
|
|
195
|
+
# Key format: {scope_id}:{type}:{name}:{line}
|
|
196
|
+
def extract_scope_id(node_key)
|
|
197
|
+
# For root key like "Class:def:name:line", scope is "Class"
|
|
198
|
+
# For "Class#method:type:name:line", scope is "Class#method"
|
|
199
|
+
# Find the last occurrence of known type prefixes
|
|
200
|
+
type_prefixes = %w[def: param: call: lit: local_write: local_read: ivar_write: ivar_read: cvar_write: cvar_read: merge: const: bparam: return: class:
|
|
201
|
+
self:]
|
|
202
|
+
last_type_pos = nil
|
|
203
|
+
|
|
204
|
+
type_prefixes.each do |prefix|
|
|
205
|
+
pos = node_key.rindex(":#{prefix}")
|
|
206
|
+
pos = node_key.index(prefix) if pos.nil? && node_key.start_with?(prefix)
|
|
207
|
+
next unless pos
|
|
208
|
+
|
|
209
|
+
pos += 1 if node_key[pos] == ":"
|
|
210
|
+
last_type_pos = pos if last_type_pos.nil? || pos > last_type_pos
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if last_type_pos
|
|
214
|
+
node_key[0...(last_type_pos - 1)]
|
|
215
|
+
else
|
|
216
|
+
# Fallback for unknown type prefixes: count colons (format: scope:type:name:line)
|
|
217
|
+
colon_positions = []
|
|
218
|
+
node_key.each_char.with_index { |c, i| colon_positions << i if c == ":" }
|
|
219
|
+
|
|
220
|
+
if colon_positions.size >= 3
|
|
221
|
+
node_key[0...colon_positions[-3]]
|
|
222
|
+
else
|
|
223
|
+
node_key.split(":").first || ""
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Serialize a node to hash format
|
|
229
|
+
def serialize_node(node, node_key)
|
|
230
|
+
warn("[GraphBuilder] serialize_node: #{node_key}") if Config.debug?
|
|
231
|
+
result = @runtime_adapter.infer_type(node)
|
|
232
|
+
warn("[GraphBuilder] infer_type done for: #{node_key}") if Config.debug?
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
key: node_key,
|
|
236
|
+
type: node_type_name(node),
|
|
237
|
+
line: node.loc&.line,
|
|
238
|
+
inferred_type: result.type.to_s,
|
|
239
|
+
details: extract_details(node, node_key)
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Get the short type name for a node
|
|
244
|
+
def node_type_name(node)
|
|
245
|
+
node.class.name.split("::").last
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Extract details based on node type
|
|
249
|
+
def extract_details(node, node_key)
|
|
250
|
+
case node
|
|
251
|
+
when ::TypeGuessr::Core::IR::DefNode
|
|
252
|
+
# Build full method signature with param types
|
|
253
|
+
param_signatures = (node.params || []).map do |p|
|
|
254
|
+
type_result = @runtime_adapter.infer_type(p)
|
|
255
|
+
type_str = type_result.type.to_s
|
|
256
|
+
"#{p.name}: #{type_str}"
|
|
257
|
+
end
|
|
258
|
+
{ name: node.name.to_s, param_signatures: param_signatures }
|
|
259
|
+
when ::TypeGuessr::Core::IR::CallNode
|
|
260
|
+
# Calculate arg keys for subgraph grouping
|
|
261
|
+
arg_keys = node.args.compact.map do |arg|
|
|
262
|
+
infer_dependency_key(arg, node_key, node)
|
|
263
|
+
end
|
|
264
|
+
# Get receiver description
|
|
265
|
+
receiver_str = format_receiver(node.receiver)
|
|
266
|
+
{ method: node.method.to_s, has_block: node.has_block, arg_keys: arg_keys, receiver: receiver_str }
|
|
267
|
+
when ::TypeGuessr::Core::IR::LocalWriteNode
|
|
268
|
+
{ name: node.name.to_s, kind: "local", called_methods: node.called_methods.map(&:to_s),
|
|
269
|
+
is_read: false }
|
|
270
|
+
when ::TypeGuessr::Core::IR::LocalReadNode
|
|
271
|
+
{ name: node.name.to_s, kind: "local", called_methods: node.called_methods.map(&:to_s),
|
|
272
|
+
is_read: true }
|
|
273
|
+
when ::TypeGuessr::Core::IR::InstanceVariableWriteNode
|
|
274
|
+
{ name: node.name.to_s, kind: "instance", class_name: node.class_name,
|
|
275
|
+
called_methods: node.called_methods.map(&:to_s), is_read: false }
|
|
276
|
+
when ::TypeGuessr::Core::IR::InstanceVariableReadNode
|
|
277
|
+
{ name: node.name.to_s, kind: "instance", class_name: node.class_name,
|
|
278
|
+
called_methods: node.called_methods.map(&:to_s), is_read: true }
|
|
279
|
+
when ::TypeGuessr::Core::IR::ClassVariableWriteNode
|
|
280
|
+
{ name: node.name.to_s, kind: "class", class_name: node.class_name,
|
|
281
|
+
called_methods: node.called_methods.map(&:to_s), is_read: false }
|
|
282
|
+
when ::TypeGuessr::Core::IR::ClassVariableReadNode
|
|
283
|
+
{ name: node.name.to_s, kind: "class", class_name: node.class_name,
|
|
284
|
+
called_methods: node.called_methods.map(&:to_s), is_read: true }
|
|
285
|
+
when ::TypeGuessr::Core::IR::ParamNode
|
|
286
|
+
{ name: node.name.to_s, kind: node.kind.to_s, called_methods: node.called_methods.map(&:to_s) }
|
|
287
|
+
when ::TypeGuessr::Core::IR::LiteralNode
|
|
288
|
+
{ literal_type: node.type.to_s }
|
|
289
|
+
when ::TypeGuessr::Core::IR::MergeNode
|
|
290
|
+
{ branches_count: node.branches.size }
|
|
291
|
+
when ::TypeGuessr::Core::IR::ConstantNode
|
|
292
|
+
{ name: node.name.to_s }
|
|
293
|
+
when ::TypeGuessr::Core::IR::BlockParamSlot
|
|
294
|
+
{ index: node.index }
|
|
295
|
+
when ::TypeGuessr::Core::IR::SelfNode
|
|
296
|
+
{ class_name: node.class_name.to_s }
|
|
297
|
+
when ::TypeGuessr::Core::IR::ClassModuleNode
|
|
298
|
+
{ name: node.name.to_s, methods_count: node.methods&.size || 0 }
|
|
299
|
+
else
|
|
300
|
+
{}
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Format receiver node for display
|
|
305
|
+
def format_receiver(receiver)
|
|
306
|
+
return nil unless receiver
|
|
307
|
+
|
|
308
|
+
case receiver
|
|
309
|
+
when ::TypeGuessr::Core::IR::SelfNode
|
|
310
|
+
"self"
|
|
311
|
+
when ::TypeGuessr::Core::IR::LocalWriteNode, ::TypeGuessr::Core::IR::LocalReadNode,
|
|
312
|
+
::TypeGuessr::Core::IR::InstanceVariableWriteNode, ::TypeGuessr::Core::IR::InstanceVariableReadNode,
|
|
313
|
+
::TypeGuessr::Core::IR::ClassVariableWriteNode, ::TypeGuessr::Core::IR::ClassVariableReadNode
|
|
314
|
+
receiver.name.to_s
|
|
315
|
+
when ::TypeGuessr::Core::IR::ConstantNode
|
|
316
|
+
receiver.name.to_s
|
|
317
|
+
when ::TypeGuessr::Core::IR::CallNode
|
|
318
|
+
".#{receiver.method}"
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Infer node key for a dependency
|
|
323
|
+
# Uses parent node info to determine correct scope for children
|
|
324
|
+
def infer_dependency_key(dep_node, parent_key, parent_node = nil)
|
|
325
|
+
# Extract scope_id from parent key
|
|
326
|
+
# Key format: {scope_id}:{type}:{name}:{line}
|
|
327
|
+
# scope_id can contain :: for namespaces and # for methods
|
|
328
|
+
# Find the scope by locating the last 3 colons (for type:name:line)
|
|
329
|
+
colon_positions = []
|
|
330
|
+
parent_key.each_char.with_index { |c, i| colon_positions << i if c == ":" }
|
|
331
|
+
|
|
332
|
+
scope_id = if colon_positions.size >= 3
|
|
333
|
+
# Take everything before the 3rd-to-last colon
|
|
334
|
+
parent_key[0...colon_positions[-3]]
|
|
335
|
+
else
|
|
336
|
+
parent_key.split(":").first
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# For DefNode parents, children have a deeper scope (Class#method)
|
|
340
|
+
if parent_node.is_a?(::TypeGuessr::Core::IR::DefNode)
|
|
341
|
+
method_name = parent_node.name
|
|
342
|
+
scope_id = scope_id.empty? ? "##{method_name}" : "#{scope_id}##{method_name}"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
dep_node.node_key(scope_id)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|