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,312 @@
|
|
|
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
|
+
if attr_accessor_call?(stmt)
|
|
206
|
+
# attr_reader/attr_accessor/attr_writer DSL → 合成DefNodeを生成
|
|
207
|
+
methods.concat(synthesize_attr_defs(stmt, class_context))
|
|
208
|
+
next
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
node = convert(stmt, class_context)
|
|
212
|
+
if node.is_a?(IR::DefNode)
|
|
213
|
+
methods << node
|
|
214
|
+
elsif node.is_a?(IR::ClassModuleNode)
|
|
215
|
+
# Store nested class/module for separate indexing with proper scope
|
|
216
|
+
nested_classes << node
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
# Store nested classes in methods array (RuntimeAdapter handles both types)
|
|
221
|
+
methods.concat(nested_classes)
|
|
222
|
+
|
|
223
|
+
IR::ClassModuleNode.new(name, methods, [], convert_loc(prism_node.constant_path&.location || prism_node.location))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
ATTR_DSL_NAMES = %i[attr_reader attr_writer attr_accessor].freeze
|
|
227
|
+
private_constant :ATTR_DSL_NAMES
|
|
228
|
+
|
|
229
|
+
private def attr_accessor_call?(prism_node)
|
|
230
|
+
return false unless prism_node.is_a?(Prism::CallNode)
|
|
231
|
+
return false unless prism_node.receiver.nil?
|
|
232
|
+
|
|
233
|
+
ATTR_DSL_NAMES.include?(prism_node.name)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# attr_reader/attr_writer/attr_accessor呼び出しから合成DefNodeを生成する
|
|
237
|
+
# @return [Array<IR::DefNode>]
|
|
238
|
+
private def synthesize_attr_defs(prism_node, context)
|
|
239
|
+
dsl_name = prism_node.name
|
|
240
|
+
args = prism_node.arguments&.arguments || []
|
|
241
|
+
|
|
242
|
+
args.flat_map do |arg|
|
|
243
|
+
attr_name = extract_attr_name(arg)
|
|
244
|
+
next [] unless attr_name
|
|
245
|
+
|
|
246
|
+
loc = convert_loc(arg.location)
|
|
247
|
+
case dsl_name
|
|
248
|
+
when :attr_reader
|
|
249
|
+
[build_reader_def(attr_name, context.current_class_name, loc)]
|
|
250
|
+
when :attr_writer
|
|
251
|
+
[build_writer_def(attr_name, context.current_class_name, loc)]
|
|
252
|
+
when :attr_accessor
|
|
253
|
+
[
|
|
254
|
+
build_reader_def(attr_name, context.current_class_name, loc),
|
|
255
|
+
build_writer_def(attr_name, context.current_class_name, loc),
|
|
256
|
+
]
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# 記号/文字列リテラル引数から属性名を取り出す
|
|
262
|
+
# @return [Symbol, nil] シンボルで返す。リテラルでない場合はnil
|
|
263
|
+
private def extract_attr_name(arg)
|
|
264
|
+
case arg
|
|
265
|
+
when Prism::SymbolNode
|
|
266
|
+
arg.value&.to_sym
|
|
267
|
+
when Prism::StringNode
|
|
268
|
+
arg.unescaped.to_sym
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# attr_readerに対応する合成DefNode (引数なし、@ivarを返す)
|
|
273
|
+
private def build_reader_def(attr_name, class_name, loc)
|
|
274
|
+
ivar_name = :"@#{attr_name}"
|
|
275
|
+
ivar_read = IR::InstanceVariableReadNode.new(ivar_name, class_name, nil, [], loc)
|
|
276
|
+
IR::DefNode.new(attr_name, class_name, [], ivar_read, [ivar_read], [], loc, false)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# attr_writerに対応する合成DefNode (name= :引数を返す)
|
|
280
|
+
private def build_writer_def(attr_name, class_name, loc)
|
|
281
|
+
setter_name = :"#{attr_name}="
|
|
282
|
+
param = IR::ParamNode.new(:value, :required, nil, [], loc)
|
|
283
|
+
IR::DefNode.new(setter_name, class_name, [param], param, [param], [], loc, false)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private def convert_singleton_class(prism_node, context)
|
|
287
|
+
# Create a new context for singleton class scope
|
|
288
|
+
singleton_context = context.fork(:class)
|
|
289
|
+
|
|
290
|
+
# Generate singleton class name in format: Parent::<Class:ParentName>
|
|
291
|
+
# This matches the scope convention used by RuntimeAdapter and RubyIndexer
|
|
292
|
+
parent_path = context.current_class_name || ""
|
|
293
|
+
parent_name = IR.extract_last_name(parent_path) || "Object"
|
|
294
|
+
singleton_suffix = "<Class:#{parent_name}>"
|
|
295
|
+
singleton_name = parent_path.empty? ? singleton_suffix : "#{parent_path}::#{singleton_suffix}"
|
|
296
|
+
singleton_context.current_class = singleton_name
|
|
297
|
+
|
|
298
|
+
# Collect all method definitions from the body
|
|
299
|
+
methods = []
|
|
300
|
+
if prism_node.body.is_a?(Prism::StatementsNode)
|
|
301
|
+
prism_node.body.body.each do |stmt|
|
|
302
|
+
node = convert(stmt, singleton_context)
|
|
303
|
+
methods << node if node.is_a?(IR::DefNode)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
IR::ClassModuleNode.new(singleton_name, methods, [], convert_loc(prism_node.location))
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Converter
|
|
6
|
+
# Literal and type inference helpers for PrismConverter
|
|
7
|
+
class PrismConverter
|
|
8
|
+
private def convert_literal(prism_node)
|
|
9
|
+
type = literal_type_for(prism_node)
|
|
10
|
+
literal_value = extract_literal_value(prism_node)
|
|
11
|
+
IR::LiteralNode.new(type, literal_value, nil, [], convert_loc(prism_node.location))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Extract the actual value from a literal node (for Symbol, Integer, String)
|
|
15
|
+
private def extract_literal_value(prism_node)
|
|
16
|
+
case prism_node
|
|
17
|
+
when Prism::SymbolNode
|
|
18
|
+
prism_node.value.to_sym
|
|
19
|
+
when Prism::IntegerNode
|
|
20
|
+
prism_node.value
|
|
21
|
+
when Prism::StringNode
|
|
22
|
+
prism_node.content
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private def convert_array_literal(prism_node, context)
|
|
27
|
+
type = array_element_type_for(prism_node)
|
|
28
|
+
|
|
29
|
+
# Convert each element to an IR node
|
|
30
|
+
value_nodes = prism_node.elements.filter_map do |elem|
|
|
31
|
+
next if elem.nil?
|
|
32
|
+
|
|
33
|
+
case elem
|
|
34
|
+
when Prism::SplatNode
|
|
35
|
+
# *arr → convert to CallNode for to_a
|
|
36
|
+
splat_expr = convert(elem.expression, context)
|
|
37
|
+
IR::CallNode.new(:to_a, splat_expr, [], [], nil, false, [], convert_loc(elem.location))
|
|
38
|
+
else
|
|
39
|
+
convert(elem, context)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
IR::LiteralNode.new(type, nil, value_nodes.empty? ? nil : value_nodes, [], convert_loc(prism_node.location))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private def convert_hash_literal(prism_node, context)
|
|
47
|
+
type = hash_element_types_for(prism_node)
|
|
48
|
+
build_hash_literal_node(prism_node, type, context)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Convert KeywordHashNode (keyword arguments in method calls like `foo(a: 1, b: x)`)
|
|
52
|
+
private def convert_keyword_hash(prism_node, context)
|
|
53
|
+
type = infer_keyword_hash_type(prism_node)
|
|
54
|
+
build_hash_literal_node(prism_node, type, context)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Shared helper for hash-like nodes (HashNode, KeywordHashNode)
|
|
58
|
+
private def build_hash_literal_node(prism_node, type, context)
|
|
59
|
+
value_nodes = prism_node.elements.filter_map do |elem|
|
|
60
|
+
case elem
|
|
61
|
+
when Prism::AssocNode
|
|
62
|
+
convert(elem.value, context)
|
|
63
|
+
when Prism::AssocSplatNode
|
|
64
|
+
convert(elem.value, context)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
IR::LiteralNode.new(type, nil, value_nodes.empty? ? nil : value_nodes, [], convert_loc(prism_node.location))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Infer type for KeywordHashNode (always has symbol keys)
|
|
72
|
+
private def infer_keyword_hash_type(keyword_hash_node)
|
|
73
|
+
return Types::HashShape.new({}) if keyword_hash_node.elements.empty?
|
|
74
|
+
|
|
75
|
+
fields = keyword_hash_node.elements.each_with_object({}) do |elem, hash|
|
|
76
|
+
next unless elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
|
|
77
|
+
|
|
78
|
+
hash[elem.key.value.to_sym] = literal_type_for(elem.value)
|
|
79
|
+
end
|
|
80
|
+
Types::HashShape.new(fields)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private def literal_type_for(prism_node)
|
|
84
|
+
case prism_node
|
|
85
|
+
when Prism::IntegerNode
|
|
86
|
+
Types::ClassInstance.for("Integer")
|
|
87
|
+
when Prism::FloatNode
|
|
88
|
+
Types::ClassInstance.for("Float")
|
|
89
|
+
when Prism::StringNode, Prism::InterpolatedStringNode
|
|
90
|
+
Types::ClassInstance.for("String")
|
|
91
|
+
when Prism::SymbolNode
|
|
92
|
+
Types::ClassInstance.for("Symbol")
|
|
93
|
+
when Prism::TrueNode
|
|
94
|
+
Types::ClassInstance.for("TrueClass")
|
|
95
|
+
when Prism::FalseNode
|
|
96
|
+
Types::ClassInstance.for("FalseClass")
|
|
97
|
+
when Prism::NilNode
|
|
98
|
+
Types::ClassInstance.for("NilClass")
|
|
99
|
+
when Prism::ArrayNode
|
|
100
|
+
# Infer element type from array contents
|
|
101
|
+
array_element_type_for(prism_node)
|
|
102
|
+
when Prism::HashNode
|
|
103
|
+
hash_element_types_for(prism_node)
|
|
104
|
+
when Prism::RangeNode
|
|
105
|
+
range_element_type_for(prism_node)
|
|
106
|
+
when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
|
|
107
|
+
Types::ClassInstance.for("Regexp")
|
|
108
|
+
when Prism::ImaginaryNode
|
|
109
|
+
Types::ClassInstance.for("Complex")
|
|
110
|
+
when Prism::RationalNode
|
|
111
|
+
Types::ClassInstance.for("Rational")
|
|
112
|
+
when Prism::XStringNode, Prism::InterpolatedXStringNode
|
|
113
|
+
Types::ClassInstance.for("String")
|
|
114
|
+
else
|
|
115
|
+
Types::Unknown.instance
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private def range_element_type_for(range_node)
|
|
120
|
+
left_type = range_node.left ? literal_type_for(range_node.left) : nil
|
|
121
|
+
right_type = range_node.right ? literal_type_for(range_node.right) : nil
|
|
122
|
+
|
|
123
|
+
types = [left_type, right_type].compact
|
|
124
|
+
|
|
125
|
+
# No bounds at all (shouldn't happen in valid Ruby, but handle gracefully)
|
|
126
|
+
return Types::RangeType.new if types.empty?
|
|
127
|
+
|
|
128
|
+
unique_types = types.uniq
|
|
129
|
+
|
|
130
|
+
element_type = if unique_types.size == 1
|
|
131
|
+
unique_types.first
|
|
132
|
+
else
|
|
133
|
+
Types::Union.new(unique_types)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Types::RangeType.new(element_type)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private def array_element_type_for(array_node)
|
|
140
|
+
return Types::TupleType.new([]) if array_node.elements.empty?
|
|
141
|
+
|
|
142
|
+
element_types = array_node.elements.filter_map do |elem|
|
|
143
|
+
literal_type_for(elem) unless elem.nil?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return Types::ArrayType.new if element_types.empty?
|
|
147
|
+
|
|
148
|
+
if element_types.any?(Types::Unknown)
|
|
149
|
+
# Splat or unknown elements → widen to ArrayType(Union)
|
|
150
|
+
unique_types = element_types.uniq
|
|
151
|
+
Types::ArrayType.new(Types::Union.new(unique_types))
|
|
152
|
+
else
|
|
153
|
+
Types::TupleType.new(element_types)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private def hash_element_types_for(hash_node)
|
|
158
|
+
return Types::HashShape.new({}) if hash_node.elements.empty?
|
|
159
|
+
|
|
160
|
+
# Check if all keys are symbols for HashShape
|
|
161
|
+
all_symbol_keys = hash_node.elements.all? do |elem|
|
|
162
|
+
elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if all_symbol_keys
|
|
166
|
+
# Build HashShape with field types
|
|
167
|
+
fields = {}
|
|
168
|
+
hash_node.elements.each do |elem|
|
|
169
|
+
next unless elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
|
|
170
|
+
|
|
171
|
+
field_name = elem.key.value.to_sym
|
|
172
|
+
field_type = literal_type_for(elem.value)
|
|
173
|
+
fields[field_name] = field_type
|
|
174
|
+
end
|
|
175
|
+
Types::HashShape.new(fields)
|
|
176
|
+
else
|
|
177
|
+
# Non-symbol keys or mixed keys - return HashType
|
|
178
|
+
key_types = []
|
|
179
|
+
value_types = []
|
|
180
|
+
|
|
181
|
+
hash_node.elements.each do |elem|
|
|
182
|
+
case elem
|
|
183
|
+
when Prism::AssocNode
|
|
184
|
+
key_types << literal_type_for(elem.key) if elem.key
|
|
185
|
+
value_types << literal_type_for(elem.value) if elem.value
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return Types::HashType.new if key_types.empty? && value_types.empty?
|
|
190
|
+
|
|
191
|
+
# Deduplicate types
|
|
192
|
+
unique_key_types = key_types.uniq
|
|
193
|
+
unique_value_types = value_types.uniq
|
|
194
|
+
|
|
195
|
+
key_type = if unique_key_types.size == 1
|
|
196
|
+
unique_key_types.first
|
|
197
|
+
elsif unique_key_types.empty?
|
|
198
|
+
Types::Unknown.instance
|
|
199
|
+
else
|
|
200
|
+
Types::Union.new(unique_key_types)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
value_type = if unique_value_types.size == 1
|
|
204
|
+
unique_value_types.first
|
|
205
|
+
elsif unique_value_types.empty?
|
|
206
|
+
Types::Unknown.instance
|
|
207
|
+
else
|
|
208
|
+
Types::Union.new(unique_value_types)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
Types::HashType.new(key_type, value_type)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|