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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/lib/ruby_lsp/type_guessr/addon.rb +4 -5
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +17 -0
- data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +3 -3
- data/lib/ruby_lsp/type_guessr/debug_server.rb +2 -2
- data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
- data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
- data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +46 -40
- data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +90 -16
- data/lib/type-guessr.rb +2 -13
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +3 -2
- data/lib/type_guessr/core/cache.rb +5 -0
- data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +2 -2
- data/lib/type_guessr/core/converter/call_converter.rb +161 -0
- data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
- data/lib/type_guessr/core/converter/context.rb +144 -0
- data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
- data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
- data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +9 -1682
- data/lib/type_guessr/core/converter/rbs_converter.rb +15 -1
- data/lib/type_guessr/core/converter/registration.rb +100 -0
- data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
- data/lib/type_guessr/core/converter.rb +4 -0
- data/lib/type_guessr/core/index.rb +3 -0
- data/lib/type_guessr/core/inference/resolver.rb +206 -208
- data/lib/type_guessr/core/inference.rb +4 -0
- data/lib/type_guessr/core/ir.rb +3 -0
- data/lib/type_guessr/core/logger.rb +3 -5
- data/lib/type_guessr/core/registry/method_registry.rb +9 -0
- data/lib/type_guessr/core/registry/signature_registry.rb +73 -16
- data/lib/type_guessr/core/registry.rb +6 -0
- data/lib/type_guessr/core/type_serializer.rb +18 -14
- data/lib/type_guessr/core/type_simplifier.rb +5 -5
- data/lib/type_guessr/core/types.rb +64 -22
- data/lib/type_guessr/core.rb +29 -0
- data/lib/type_guessr/mcp/server.rb +55 -46
- data/lib/type_guessr/mcp/standalone_runtime.rb +70 -110
- data/lib/type_guessr/version.rb +1 -1
- metadata +25 -5
- data/.mcp.json +0 -9
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Converter
|
|
6
|
+
# Control flow (if/case/begin/or/and), variable merging, and rescue methods for PrismConverter
|
|
7
|
+
class PrismConverter
|
|
8
|
+
# Register exception variable from rescue clause (=> e)
|
|
9
|
+
# @param rescue_clause [Prism::RescueNode] The rescue clause
|
|
10
|
+
# @param context [Context] Conversion context
|
|
11
|
+
private def register_rescue_variable(rescue_clause, context)
|
|
12
|
+
var_name = rescue_clause.reference.name
|
|
13
|
+
exception_type = infer_rescue_exception_type(rescue_clause.exceptions)
|
|
14
|
+
loc = convert_loc(rescue_clause.reference.location)
|
|
15
|
+
|
|
16
|
+
value_node = IR::LiteralNode.new(exception_type, nil, nil, [], loc)
|
|
17
|
+
|
|
18
|
+
write_node = IR::LocalWriteNode.new(var_name, value_node, [], loc)
|
|
19
|
+
|
|
20
|
+
context.register_variable(var_name, write_node)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Infer exception type from rescue clause's exception list
|
|
24
|
+
# @param exceptions [Array<Prism::Node>] List of exception class nodes
|
|
25
|
+
# @return [Types::ClassInstance, Types::Union] Inferred exception type
|
|
26
|
+
private def infer_rescue_exception_type(exceptions)
|
|
27
|
+
# Default to StandardError if no exception class specified (rescue => e)
|
|
28
|
+
return Types::ClassInstance.new("StandardError") if exceptions.empty?
|
|
29
|
+
|
|
30
|
+
types = exceptions.map do |exc|
|
|
31
|
+
class_name = case exc
|
|
32
|
+
when Prism::ConstantReadNode
|
|
33
|
+
exc.name.to_s
|
|
34
|
+
when Prism::ConstantPathNode
|
|
35
|
+
# Handle namespaced constants like Net::HTTPError
|
|
36
|
+
begin
|
|
37
|
+
exc.full_name
|
|
38
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
39
|
+
"StandardError"
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
"StandardError"
|
|
43
|
+
end
|
|
44
|
+
Types::ClassInstance.new(class_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
types.size == 1 ? types.first : Types::Union.new(types)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private def convert_if(prism_node, context)
|
|
51
|
+
# Convert then branch
|
|
52
|
+
then_context = context.fork(:then)
|
|
53
|
+
then_node = convert(prism_node.statements, then_context) if prism_node.statements
|
|
54
|
+
|
|
55
|
+
# Convert else branch (could be IfNode, ElseNode, or nil)
|
|
56
|
+
else_context = context.fork(:else)
|
|
57
|
+
else_node = if prism_node.subsequent
|
|
58
|
+
case prism_node.subsequent
|
|
59
|
+
when Prism::IfNode
|
|
60
|
+
convert_if(prism_node.subsequent, else_context)
|
|
61
|
+
when Prism::ElseNode
|
|
62
|
+
convert(prism_node.subsequent.statements, else_context)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Create merge nodes for variables modified in branches
|
|
67
|
+
merge_modified_variables(context, then_context, else_context, then_node, else_node, prism_node.location)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def convert_unless(prism_node, context)
|
|
71
|
+
# Unless is like if with inverted condition
|
|
72
|
+
# We treat the unless body as the "else" branch and the consequent as "then"
|
|
73
|
+
|
|
74
|
+
unless_context = context.fork(:unless)
|
|
75
|
+
unless_node = convert(prism_node.statements, unless_context) if prism_node.statements
|
|
76
|
+
|
|
77
|
+
else_context = context.fork(:else)
|
|
78
|
+
else_node = (convert(prism_node.else_clause.statements, else_context) if prism_node.else_clause)
|
|
79
|
+
|
|
80
|
+
result = merge_modified_variables(context, unless_context, else_context, unless_node, else_node, prism_node.location)
|
|
81
|
+
|
|
82
|
+
# Guard clause narrowing: `return/raise unless x` → x is truthy after
|
|
83
|
+
narrow_guard_variable(prism_node.predicate, :truthy, context, prism_node.location) if guard_clause?(unless_node)
|
|
84
|
+
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private def convert_case(prism_node, context)
|
|
89
|
+
branches = []
|
|
90
|
+
branch_contexts = []
|
|
91
|
+
|
|
92
|
+
# Convert each when clause
|
|
93
|
+
prism_node.conditions&.each do |when_node|
|
|
94
|
+
when_context = context.fork(:when)
|
|
95
|
+
if when_node.statements
|
|
96
|
+
when_result = convert(when_node.statements, when_context)
|
|
97
|
+
# Skip non-returning branches (raise, fail, etc.)
|
|
98
|
+
unless non_returning?(when_result)
|
|
99
|
+
branches << (when_result || create_nil_literal(prism_node.location))
|
|
100
|
+
branch_contexts << when_context
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
# Empty when clause → nil
|
|
104
|
+
branches << create_nil_literal(prism_node.location)
|
|
105
|
+
branch_contexts << when_context
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Convert else clause
|
|
110
|
+
if prism_node.else_clause
|
|
111
|
+
else_context = context.fork(:else)
|
|
112
|
+
else_result = convert(prism_node.else_clause.statements, else_context)
|
|
113
|
+
# Skip non-returning else clause (raise, fail, etc.)
|
|
114
|
+
unless non_returning?(else_result)
|
|
115
|
+
branches << (else_result || create_nil_literal(prism_node.location))
|
|
116
|
+
branch_contexts << else_context
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
# If no else clause, nil is possible
|
|
120
|
+
branches << create_nil_literal(prism_node.location)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Merge modified variables across all branches
|
|
124
|
+
merge_case_variables(context, branch_contexts, branches, prism_node.location)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private def convert_case_match(prism_node, context)
|
|
128
|
+
# Pattern matching case (Ruby 3.0+)
|
|
129
|
+
# For now, treat it similarly to regular case
|
|
130
|
+
convert_case(prism_node, context)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private def convert_statements(prism_node, context)
|
|
134
|
+
last_node = nil
|
|
135
|
+
prism_node.body.each do |stmt|
|
|
136
|
+
last_node = convert(stmt, context)
|
|
137
|
+
end
|
|
138
|
+
last_node
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Helper to convert an array of statement bodies
|
|
142
|
+
# @param body [Array<Prism::Node>, nil] Array of statement nodes
|
|
143
|
+
# @param context [Context] Conversion context
|
|
144
|
+
# @return [Array<IR::Node>] Array of converted IR nodes
|
|
145
|
+
private def convert_statements_body(body, context)
|
|
146
|
+
return [] unless body
|
|
147
|
+
|
|
148
|
+
nodes = []
|
|
149
|
+
body.each do |stmt|
|
|
150
|
+
node = convert(stmt, context)
|
|
151
|
+
nodes << node if node
|
|
152
|
+
end
|
|
153
|
+
nodes
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Convert begin/rescue/ensure block
|
|
157
|
+
private def convert_begin(prism_node, context)
|
|
158
|
+
body_nodes = extract_begin_body_nodes(prism_node, context)
|
|
159
|
+
# Return the last node (represents the value of the begin block)
|
|
160
|
+
body_nodes.last
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Convert || (or) operator to OrNode
|
|
164
|
+
# a || b → LHS evaluated first, RHS only if LHS is falsy
|
|
165
|
+
private def convert_or_node(prism_node, context)
|
|
166
|
+
left_node = convert(prism_node.left, context)
|
|
167
|
+
right_node = convert(prism_node.right, context)
|
|
168
|
+
|
|
169
|
+
return nil if left_node.nil? && right_node.nil?
|
|
170
|
+
return left_node if right_node.nil?
|
|
171
|
+
return right_node if left_node.nil?
|
|
172
|
+
|
|
173
|
+
IR::OrNode.new(left_node, right_node, [], convert_loc(prism_node.location))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Convert && (and) operator to MergeNode
|
|
177
|
+
# a && b → result is either a or b (short-circuit evaluation)
|
|
178
|
+
private def convert_and_node(prism_node, context)
|
|
179
|
+
left_node = convert(prism_node.left, context)
|
|
180
|
+
right_node = convert(prism_node.right, context)
|
|
181
|
+
|
|
182
|
+
branches = [left_node, right_node].compact
|
|
183
|
+
return nil if branches.empty?
|
|
184
|
+
return branches.first if branches.size == 1
|
|
185
|
+
|
|
186
|
+
IR::MergeNode.new(branches, [], convert_loc(prism_node.location))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Convert h[:key] ||= value → OrNode(h.[](:key), value)
|
|
190
|
+
private def convert_index_or_write(prism_node, context)
|
|
191
|
+
receiver_node = convert(prism_node.receiver, context)
|
|
192
|
+
args = prism_node.arguments&.arguments&.map { |arg| convert(arg, context) } || []
|
|
193
|
+
value_node = convert(prism_node.value, context)
|
|
194
|
+
|
|
195
|
+
read_call = IR::CallNode.new(:[], receiver_node, args, [], nil, false, [], convert_loc(prism_node.opening_loc))
|
|
196
|
+
|
|
197
|
+
IR::OrNode.new(read_call, value_node, [], convert_loc(prism_node.location))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Convert multiple assignment (a, b, c = expr)
|
|
201
|
+
# Creates synthetic value[index] calls for each target variable
|
|
202
|
+
private def convert_multi_write(prism_node, context)
|
|
203
|
+
value_node = convert(prism_node.value, context)
|
|
204
|
+
|
|
205
|
+
# lefts: variables before splat → value[0], value[1], ...
|
|
206
|
+
prism_node.lefts.each_with_index do |target, index|
|
|
207
|
+
assign_multi_write_target(target, value_node, index, context)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# rest: splat variable → ArrayType(Unknown)
|
|
211
|
+
if prism_node.rest.is_a?(Prism::SplatNode) && prism_node.rest.expression
|
|
212
|
+
splat_target = prism_node.rest.expression
|
|
213
|
+
splat_value = IR::LiteralNode.new(
|
|
214
|
+
Types::ArrayType.new, nil, nil, [], convert_loc(splat_target.location)
|
|
215
|
+
)
|
|
216
|
+
register_multi_write_variable(splat_target, splat_value, context)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# rights: variables after splat → value[-n], value[-(n-1)], ...
|
|
220
|
+
prism_node.rights.each_with_index do |target, index|
|
|
221
|
+
negative_index = -(prism_node.rights.size - index)
|
|
222
|
+
assign_multi_write_target(target, value_node, negative_index, context)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
value_node
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Create synthetic value[index] call and register the target variable
|
|
229
|
+
private def assign_multi_write_target(target, value_node, index, context)
|
|
230
|
+
loc = convert_loc(target.location)
|
|
231
|
+
index_literal = IR::LiteralNode.new(
|
|
232
|
+
Types::ClassInstance.for("Integer"), index, nil, [], loc
|
|
233
|
+
)
|
|
234
|
+
call_node = IR::CallNode.new(:[], value_node, [index_literal], [], nil, false, [], loc)
|
|
235
|
+
register_multi_write_variable(target, call_node, context)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Register a multi-write target variable (local or instance variable)
|
|
239
|
+
private def register_multi_write_variable(target, value_node, context)
|
|
240
|
+
loc = convert_loc(target.location)
|
|
241
|
+
case target
|
|
242
|
+
when Prism::LocalVariableTargetNode
|
|
243
|
+
write_node = IR::LocalWriteNode.new(target.name, value_node, [], loc)
|
|
244
|
+
context.register_variable(target.name, write_node)
|
|
245
|
+
when Prism::InstanceVariableTargetNode
|
|
246
|
+
write_node = IR::InstanceVariableWriteNode.new(
|
|
247
|
+
target.name, context.current_class_name, value_node, [], loc
|
|
248
|
+
)
|
|
249
|
+
context.register_instance_variable(target.name, write_node)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Extract all body nodes from a BeginNode (for DefNode bodies with rescue/ensure)
|
|
254
|
+
# @param begin_node [Prism::BeginNode] The begin node
|
|
255
|
+
# @param context [Context] Conversion context
|
|
256
|
+
# @return [Array<IR::Node>] Array of all body nodes
|
|
257
|
+
private def extract_begin_body_nodes(begin_node, context)
|
|
258
|
+
body_nodes = []
|
|
259
|
+
|
|
260
|
+
# Convert main body statements
|
|
261
|
+
body_nodes.concat(convert_statements_body(begin_node.statements.body, context)) if begin_node.statements
|
|
262
|
+
|
|
263
|
+
# Convert rescue clause(s)
|
|
264
|
+
rescue_clause = begin_node.rescue_clause
|
|
265
|
+
while rescue_clause
|
|
266
|
+
# Register exception variable (=> e) if present
|
|
267
|
+
register_rescue_variable(rescue_clause, context) if rescue_clause.reference.is_a?(Prism::LocalVariableTargetNode)
|
|
268
|
+
|
|
269
|
+
rescue_nodes = convert_statements_body(rescue_clause.statements&.body, context)
|
|
270
|
+
body_nodes.concat(rescue_nodes)
|
|
271
|
+
rescue_clause = rescue_clause.subsequent
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Convert else clause
|
|
275
|
+
if begin_node.else_clause
|
|
276
|
+
else_nodes = convert_statements_body(begin_node.else_clause.statements&.body, context)
|
|
277
|
+
body_nodes.concat(else_nodes)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Convert ensure clause
|
|
281
|
+
if begin_node.ensure_clause
|
|
282
|
+
ensure_nodes = convert_statements_body(begin_node.ensure_clause.statements&.body, context)
|
|
283
|
+
body_nodes.concat(ensure_nodes)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
body_nodes
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private def merge_modified_variables(parent_context, then_context, else_context, then_node, else_node, location)
|
|
290
|
+
# Skip non-returning branches (raise, fail, etc.)
|
|
291
|
+
then_node = nil if non_returning?(then_node)
|
|
292
|
+
else_node = nil if non_returning?(else_node)
|
|
293
|
+
|
|
294
|
+
# Track which variables were modified in each branch
|
|
295
|
+
then_vars = then_context&.local_variables || []
|
|
296
|
+
else_vars = else_context&.local_variables || []
|
|
297
|
+
|
|
298
|
+
# All variables modified in either branch
|
|
299
|
+
modified_vars = (then_vars + else_vars).uniq
|
|
300
|
+
|
|
301
|
+
# Create MergeNode for each modified variable
|
|
302
|
+
modified_vars.each do |var_name|
|
|
303
|
+
then_val = then_context&.variables&.[](var_name)
|
|
304
|
+
else_val = else_context&.variables&.[](var_name)
|
|
305
|
+
|
|
306
|
+
# Get the original value from parent context (before if statement)
|
|
307
|
+
original_val = parent_context.lookup_variable(var_name)
|
|
308
|
+
|
|
309
|
+
# Determine branches for merge
|
|
310
|
+
branches = []
|
|
311
|
+
if then_val
|
|
312
|
+
branches << then_val
|
|
313
|
+
elsif original_val
|
|
314
|
+
# Variable not modified in then branch, use original
|
|
315
|
+
branches << original_val
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
if else_val
|
|
319
|
+
branches << else_val
|
|
320
|
+
elsif original_val
|
|
321
|
+
# Variable not modified in else branch, use original
|
|
322
|
+
branches << original_val
|
|
323
|
+
elsif then_val
|
|
324
|
+
# Inline if/unless: no else branch and no original value
|
|
325
|
+
# Add nil to represent "variable may not be assigned"
|
|
326
|
+
nil_node = IR::LiteralNode.new(
|
|
327
|
+
Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location)
|
|
328
|
+
)
|
|
329
|
+
branches << nil_node
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Create MergeNode only if we have multiple branches
|
|
333
|
+
if branches.size > 1
|
|
334
|
+
merge_node = IR::MergeNode.new(branches.uniq, [], convert_loc(location))
|
|
335
|
+
parent_context.register_variable(var_name, merge_node)
|
|
336
|
+
elsif branches.size == 1
|
|
337
|
+
# Only one branch has a value, use it directly
|
|
338
|
+
parent_context.register_variable(var_name, branches.first)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Return MergeNode for the if expression value
|
|
343
|
+
if then_node && else_node
|
|
344
|
+
IR::MergeNode.new([then_node, else_node].compact, [], convert_loc(location))
|
|
345
|
+
elsif then_node || else_node
|
|
346
|
+
# Modifier form: one branch only → value or nil
|
|
347
|
+
branch_node = then_node || else_node
|
|
348
|
+
nil_node = IR::LiteralNode.new(
|
|
349
|
+
Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location)
|
|
350
|
+
)
|
|
351
|
+
IR::MergeNode.new([branch_node, nil_node], [], convert_loc(location))
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
private def merge_case_variables(parent_context, branch_contexts, branches, location)
|
|
356
|
+
# Collect all variables modified in any branch
|
|
357
|
+
all_modified_vars = branch_contexts.flat_map { |ctx| ctx&.local_variables || [] }.uniq
|
|
358
|
+
|
|
359
|
+
# Create MergeNode for each modified variable
|
|
360
|
+
all_modified_vars.each do |var_name|
|
|
361
|
+
# Get original value from parent context
|
|
362
|
+
original_val = parent_context.lookup_variable(var_name)
|
|
363
|
+
|
|
364
|
+
# Build branches array
|
|
365
|
+
merge_branches = branch_contexts.map.with_index do |ctx, _idx|
|
|
366
|
+
ctx&.variables&.[](var_name) || original_val
|
|
367
|
+
end.compact.uniq
|
|
368
|
+
|
|
369
|
+
# Create MergeNode if we have multiple different values
|
|
370
|
+
if merge_branches.size > 1
|
|
371
|
+
merge_node = IR::MergeNode.new(merge_branches, [], convert_loc(location))
|
|
372
|
+
parent_context.register_variable(var_name, merge_node)
|
|
373
|
+
elsif merge_branches.size == 1
|
|
374
|
+
parent_context.register_variable(var_name, merge_branches.first)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Return MergeNode for the case expression value
|
|
379
|
+
if branches.size > 1
|
|
380
|
+
IR::MergeNode.new(branches.compact.uniq, [], convert_loc(location))
|
|
381
|
+
elsif branches.size == 1
|
|
382
|
+
branches.first
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
private def create_nil_literal(location)
|
|
387
|
+
IR::LiteralNode.new(Types::ClassInstance.for("NilClass"), nil, nil, [], convert_loc(location))
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Check if a node represents a non-returning expression (raise, fail, exit, abort)
|
|
391
|
+
# These should be excluded from branch type inference
|
|
392
|
+
private def non_returning?(node)
|
|
393
|
+
return false unless node.is_a?(IR::CallNode)
|
|
394
|
+
|
|
395
|
+
node.receiver.nil? && %i[raise fail exit abort].include?(node.method)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Check if a node represents a guard clause body (exits the method)
|
|
399
|
+
# Includes both non-returning expressions (raise/fail) and explicit returns
|
|
400
|
+
private def guard_clause?(node)
|
|
401
|
+
node.is_a?(IR::ReturnNode) || non_returning?(node)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# After a guard clause (`return/raise unless x`), narrow the guarded variable
|
|
405
|
+
# to remove falsy types (NilClass, FalseClass)
|
|
406
|
+
private def narrow_guard_variable(predicate, kind, context, location)
|
|
407
|
+
case predicate
|
|
408
|
+
when Prism::LocalVariableReadNode
|
|
409
|
+
write_node = context.lookup_variable(predicate.name)
|
|
410
|
+
return unless write_node
|
|
411
|
+
|
|
412
|
+
narrow = IR::NarrowNode.new(write_node, kind, write_node.called_methods, convert_loc(location))
|
|
413
|
+
context.register_variable(predicate.name, narrow)
|
|
414
|
+
when Prism::InstanceVariableReadNode
|
|
415
|
+
write_node = context.lookup_instance_variable(predicate.name)
|
|
416
|
+
return unless write_node
|
|
417
|
+
|
|
418
|
+
narrow = IR::NarrowNode.new(write_node, kind, write_node.called_methods, convert_loc(location))
|
|
419
|
+
context.narrow_instance_variable(predicate.name, narrow)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Converter
|
|
6
|
+
# Method/class/module/constant definition methods for PrismConverter
|
|
7
|
+
class PrismConverter
|
|
8
|
+
private def convert_def(prism_node, context, module_function: false)
|
|
9
|
+
def_context = context.fork(:method)
|
|
10
|
+
def_context.current_method = prism_node.name.to_s
|
|
11
|
+
def_context.in_singleton_method = prism_node.receiver.is_a?(Prism::SelfNode)
|
|
12
|
+
|
|
13
|
+
# Convert parameters
|
|
14
|
+
params = []
|
|
15
|
+
if prism_node.parameters
|
|
16
|
+
parameters_node = prism_node.parameters
|
|
17
|
+
|
|
18
|
+
# Required parameters
|
|
19
|
+
parameters_node.requireds&.each do |param|
|
|
20
|
+
extract_param_nodes(param, :required, def_context).each do |param_node|
|
|
21
|
+
params << param_node
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Optional parameters
|
|
26
|
+
parameters_node.optionals&.each do |param|
|
|
27
|
+
default_node = convert(param.value, def_context)
|
|
28
|
+
param_node = IR::ParamNode.new(param.name, :optional, default_node, [], convert_loc(param.location))
|
|
29
|
+
params << param_node
|
|
30
|
+
def_context.register_variable(param.name, param_node)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Rest parameter (*args)
|
|
34
|
+
if parameters_node.rest.is_a?(Prism::RestParameterNode)
|
|
35
|
+
rest = parameters_node.rest
|
|
36
|
+
param_node = IR::ParamNode.new(rest.name || :*, :rest, nil, [], convert_loc(rest.location))
|
|
37
|
+
params << param_node
|
|
38
|
+
def_context.register_variable(rest.name, param_node) if rest.name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Required keyword parameters (name:)
|
|
42
|
+
parameters_node.keywords&.each do |kw|
|
|
43
|
+
case kw
|
|
44
|
+
when Prism::RequiredKeywordParameterNode
|
|
45
|
+
param_node = IR::ParamNode.new(kw.name, :keyword_required, nil, [], convert_loc(kw.location))
|
|
46
|
+
params << param_node
|
|
47
|
+
def_context.register_variable(kw.name, param_node)
|
|
48
|
+
when Prism::OptionalKeywordParameterNode
|
|
49
|
+
default_node = convert(kw.value, def_context)
|
|
50
|
+
param_node = IR::ParamNode.new(kw.name, :keyword_optional, default_node, [], convert_loc(kw.location))
|
|
51
|
+
params << param_node
|
|
52
|
+
def_context.register_variable(kw.name, param_node)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Keyword rest parameter (**kwargs)
|
|
57
|
+
if parameters_node.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
|
|
58
|
+
kwrest = parameters_node.keyword_rest
|
|
59
|
+
param_node = IR::ParamNode.new(kwrest.name || :**, :keyword_rest, nil, [], convert_loc(kwrest.location))
|
|
60
|
+
params << param_node
|
|
61
|
+
def_context.register_variable(kwrest.name, param_node) if kwrest.name
|
|
62
|
+
elsif parameters_node.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
63
|
+
# Forwarding parameter (...)
|
|
64
|
+
fwd = parameters_node.keyword_rest
|
|
65
|
+
param_node = IR::ParamNode.new(:"...", :forwarding, nil, [], convert_loc(fwd.location))
|
|
66
|
+
params << param_node
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Block parameter (&block)
|
|
70
|
+
if parameters_node.block
|
|
71
|
+
block = parameters_node.block
|
|
72
|
+
param_node = IR::ParamNode.new(block.name || :&, :block, nil, [], convert_loc(block.location))
|
|
73
|
+
params << param_node
|
|
74
|
+
def_context.register_variable(block.name, param_node) if block.name
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Convert method body - collect all body nodes
|
|
79
|
+
body_nodes = []
|
|
80
|
+
|
|
81
|
+
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
82
|
+
prism_node.body.body.each do |stmt|
|
|
83
|
+
node = convert(stmt, def_context)
|
|
84
|
+
body_nodes << node if node
|
|
85
|
+
end
|
|
86
|
+
elsif prism_node.body.is_a?(Prism::BeginNode)
|
|
87
|
+
# Method with rescue/ensure block
|
|
88
|
+
begin_node = prism_node.body
|
|
89
|
+
body_nodes = extract_begin_body_nodes(begin_node, def_context)
|
|
90
|
+
elsif prism_node.body
|
|
91
|
+
node = convert(prism_node.body, def_context)
|
|
92
|
+
body_nodes << node if node
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Collect all return points: explicit returns + implicit last expression
|
|
96
|
+
return_node = compute_return_node(body_nodes, prism_node.name_loc)
|
|
97
|
+
|
|
98
|
+
IR::DefNode.new(
|
|
99
|
+
prism_node.name,
|
|
100
|
+
def_context.current_class_name,
|
|
101
|
+
params,
|
|
102
|
+
return_node,
|
|
103
|
+
body_nodes,
|
|
104
|
+
[],
|
|
105
|
+
convert_loc(prism_node.name_loc),
|
|
106
|
+
prism_node.receiver.is_a?(Prism::SelfNode),
|
|
107
|
+
module_function: module_function
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Compute the return node for a method by collecting all return points
|
|
112
|
+
# @param body_nodes [Array<IR::Node>] All nodes in the method body
|
|
113
|
+
# @param loc [Prism::Location] Location for the MergeNode if needed
|
|
114
|
+
# @return [IR::Node, nil] The return node (MergeNode if multiple returns)
|
|
115
|
+
private def compute_return_node(body_nodes, loc)
|
|
116
|
+
return nil if body_nodes.empty?
|
|
117
|
+
|
|
118
|
+
# Collect all explicit returns from the body
|
|
119
|
+
explicit_returns = collect_returns(body_nodes)
|
|
120
|
+
|
|
121
|
+
# The implicit return is the last non-ReturnNode in body
|
|
122
|
+
implicit_return = body_nodes.grep_v(IR::ReturnNode).last
|
|
123
|
+
|
|
124
|
+
# Determine all return points
|
|
125
|
+
return_points = explicit_returns.dup
|
|
126
|
+
return_points << implicit_return if implicit_return && !last_node_returns?(body_nodes)
|
|
127
|
+
|
|
128
|
+
case return_points.size
|
|
129
|
+
when 0
|
|
130
|
+
nil
|
|
131
|
+
when 1
|
|
132
|
+
return_points.first
|
|
133
|
+
else
|
|
134
|
+
IR::MergeNode.new(return_points, [], convert_loc(loc))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Collect all ReturnNode instances from body nodes (recursive)
|
|
139
|
+
# Searches inside MergeNode branches to find nested returns from if/case
|
|
140
|
+
# @param nodes [Array<IR::Node>] Nodes to search
|
|
141
|
+
# @return [Array<IR::ReturnNode>] All explicit return nodes
|
|
142
|
+
private def collect_returns(nodes)
|
|
143
|
+
returns = []
|
|
144
|
+
nodes.each do |node|
|
|
145
|
+
case node
|
|
146
|
+
when IR::ReturnNode
|
|
147
|
+
returns << node
|
|
148
|
+
when IR::MergeNode
|
|
149
|
+
returns.concat(collect_returns(node.branches))
|
|
150
|
+
when IR::OrNode
|
|
151
|
+
returns.concat(collect_returns([node.lhs, node.rhs]))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
returns
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if the last node in body is a ReturnNode
|
|
158
|
+
# @param body_nodes [Array<IR::Node>] Body nodes
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
private def last_node_returns?(body_nodes)
|
|
161
|
+
body_nodes.last.is_a?(IR::ReturnNode)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private def convert_constant_read(prism_node, context)
|
|
165
|
+
name = case prism_node
|
|
166
|
+
when Prism::ConstantReadNode
|
|
167
|
+
prism_node.name.to_s
|
|
168
|
+
when Prism::ConstantPathNode
|
|
169
|
+
prism_node.slice
|
|
170
|
+
else
|
|
171
|
+
prism_node.to_s
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
IR::ConstantNode.new(name, context.lookup_constant(name), [], convert_loc(prism_node.location))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private def convert_constant_write(prism_node, context)
|
|
178
|
+
value_node = convert(prism_node.value, context)
|
|
179
|
+
context.register_constant(prism_node.name.to_s, value_node)
|
|
180
|
+
IR::ConstantNode.new(prism_node.name.to_s, value_node, [], convert_loc(prism_node.location))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private def convert_class_or_module(prism_node, context)
|
|
184
|
+
# Get class/module name first
|
|
185
|
+
name = case prism_node.constant_path
|
|
186
|
+
when Prism::ConstantReadNode
|
|
187
|
+
prism_node.constant_path.name.to_s
|
|
188
|
+
when Prism::ConstantPathNode
|
|
189
|
+
prism_node.constant_path.slice
|
|
190
|
+
else
|
|
191
|
+
"Anonymous"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Create a new context for class/module scope with the full class path
|
|
195
|
+
class_context = context.fork(:class)
|
|
196
|
+
parent_path = context.current_class_name
|
|
197
|
+
full_name = parent_path ? "#{parent_path}::#{name}" : name
|
|
198
|
+
class_context.current_class = full_name
|
|
199
|
+
|
|
200
|
+
# Collect all method definitions and nested classes from the body
|
|
201
|
+
methods = []
|
|
202
|
+
nested_classes = []
|
|
203
|
+
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
204
|
+
prism_node.body.body.each do |stmt|
|
|
205
|
+
node = convert(stmt, class_context)
|
|
206
|
+
if node.is_a?(IR::DefNode)
|
|
207
|
+
methods << node
|
|
208
|
+
elsif node.is_a?(IR::ClassModuleNode)
|
|
209
|
+
# Store nested class/module for separate indexing with proper scope
|
|
210
|
+
nested_classes << node
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
# Store nested classes in methods array (RuntimeAdapter handles both types)
|
|
215
|
+
methods.concat(nested_classes)
|
|
216
|
+
|
|
217
|
+
IR::ClassModuleNode.new(name, methods, [], convert_loc(prism_node.constant_path&.location || prism_node.location))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private def convert_singleton_class(prism_node, context)
|
|
221
|
+
# Create a new context for singleton class scope
|
|
222
|
+
singleton_context = context.fork(:class)
|
|
223
|
+
|
|
224
|
+
# Generate singleton class name in format: Parent::<Class:ParentName>
|
|
225
|
+
# This matches the scope convention used by RuntimeAdapter and RubyIndexer
|
|
226
|
+
parent_path = context.current_class_name || ""
|
|
227
|
+
parent_name = IR.extract_last_name(parent_path) || "Object"
|
|
228
|
+
singleton_suffix = "<Class:#{parent_name}>"
|
|
229
|
+
singleton_name = parent_path.empty? ? singleton_suffix : "#{parent_path}::#{singleton_suffix}"
|
|
230
|
+
singleton_context.current_class = singleton_name
|
|
231
|
+
|
|
232
|
+
# Collect all method definitions from the body
|
|
233
|
+
methods = []
|
|
234
|
+
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
235
|
+
prism_node.body.body.each do |stmt|
|
|
236
|
+
node = convert(stmt, singleton_context)
|
|
237
|
+
methods << node if node.is_a?(IR::DefNode)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
IR::ClassModuleNode.new(singleton_name, methods, [], convert_loc(prism_node.location))
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|