type-guessr 0.0.2 → 0.0.4
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 +18 -1
- 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 +312 -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 +24 -4
- 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
|