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,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Converter
|
|
6
|
+
# Method call, block, and call-related helper methods for PrismConverter
|
|
7
|
+
class PrismConverter
|
|
8
|
+
private def convert_call(prism_node, context)
|
|
9
|
+
# Convert receiver - if nil and inside a class, create implicit SelfNode
|
|
10
|
+
receiver_node = if prism_node.receiver
|
|
11
|
+
convert(prism_node.receiver, context)
|
|
12
|
+
elsif context.current_class_name
|
|
13
|
+
IR::SelfNode.new(
|
|
14
|
+
context.current_class_name,
|
|
15
|
+
context.in_singleton_method,
|
|
16
|
+
[],
|
|
17
|
+
convert_loc(prism_node.location)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
args = prism_node.arguments&.arguments&.map { |arg| convert(arg, context) } || []
|
|
22
|
+
|
|
23
|
+
has_block = !prism_node.block.nil?
|
|
24
|
+
|
|
25
|
+
# Track method call on receiver for method-based type inference
|
|
26
|
+
if variable_node?(receiver_node) && receiver_node.called_methods.none? { |cm| cm.name == prism_node.name }
|
|
27
|
+
receiver_node.called_methods << build_called_method(prism_node)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Handle container mutating methods (Hash#[]=, Array#[]=, Array#<<)
|
|
31
|
+
receiver_node = handle_container_mutation(prism_node, receiver_node, args, context) if container_mutating_method?(prism_node.name, receiver_node)
|
|
32
|
+
|
|
33
|
+
# Use message_loc for method name position to match hover lookup
|
|
34
|
+
call_loc = convert_loc(prism_node.message_loc || prism_node.location)
|
|
35
|
+
call_node = IR::CallNode.new(
|
|
36
|
+
prism_node.name, receiver_node, args, [], nil, has_block, [], call_loc
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Handle block if present (but not block arguments like &block)
|
|
40
|
+
if prism_node.block.is_a?(Prism::BlockNode)
|
|
41
|
+
block_body = convert_block(prism_node.block, call_node, context)
|
|
42
|
+
# Update block_body and has_block on mutable Struct
|
|
43
|
+
call_node.block_body = block_body
|
|
44
|
+
call_node.has_block = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
call_node
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if node is any variable node (for method call tracking)
|
|
51
|
+
private def variable_node?(node)
|
|
52
|
+
node.is_a?(IR::LocalWriteNode) ||
|
|
53
|
+
node.is_a?(IR::LocalReadNode) ||
|
|
54
|
+
node.is_a?(IR::InstanceVariableWriteNode) ||
|
|
55
|
+
node.is_a?(IR::InstanceVariableReadNode) ||
|
|
56
|
+
node.is_a?(IR::ClassVariableWriteNode) ||
|
|
57
|
+
node.is_a?(IR::ClassVariableReadNode) ||
|
|
58
|
+
node.is_a?(IR::ParamNode) ||
|
|
59
|
+
node.is_a?(IR::BlockParamSlot)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Build CalledMethod with signature information from Prism CallNode
|
|
63
|
+
private def build_called_method(prism_node)
|
|
64
|
+
positional_count, has_splat, keywords = extract_call_signature(prism_node)
|
|
65
|
+
|
|
66
|
+
IR::CalledMethod.new(
|
|
67
|
+
name: prism_node.name,
|
|
68
|
+
positional_count: has_splat ? nil : positional_count,
|
|
69
|
+
keywords: keywords
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Extract positional count, splat presence, and keywords from call arguments
|
|
74
|
+
# @return [Array(Integer, Boolean, Array<Symbol>)] [positional_count, has_splat, keywords]
|
|
75
|
+
private def extract_call_signature(prism_node)
|
|
76
|
+
arguments = prism_node.arguments&.arguments || []
|
|
77
|
+
positional_count = 0
|
|
78
|
+
has_splat = false
|
|
79
|
+
keywords = []
|
|
80
|
+
|
|
81
|
+
arguments.each do |arg|
|
|
82
|
+
case arg
|
|
83
|
+
when Prism::SplatNode
|
|
84
|
+
has_splat = true
|
|
85
|
+
when Prism::KeywordHashNode
|
|
86
|
+
extract_keywords_from_hash(arg, keywords)
|
|
87
|
+
else
|
|
88
|
+
positional_count += 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
[positional_count, has_splat, keywords]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Extract keyword argument names from KeywordHashNode
|
|
96
|
+
private def extract_keywords_from_hash(hash_node, keywords)
|
|
97
|
+
hash_node.elements.each do |element|
|
|
98
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
99
|
+
|
|
100
|
+
key = element.key
|
|
101
|
+
keywords << key.value.to_sym if key.is_a?(Prism::SymbolNode)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Extract IR param nodes from a Prism parameter node
|
|
106
|
+
# Handles destructuring (MultiTargetNode) by flattening nested params
|
|
107
|
+
private def extract_param_nodes(param, kind, context, default_value: nil)
|
|
108
|
+
case param
|
|
109
|
+
when Prism::MultiTargetNode
|
|
110
|
+
# Destructuring parameter like (a, b) - extract all nested params
|
|
111
|
+
param.lefts.flat_map { |p| extract_param_nodes(p, kind, context) } +
|
|
112
|
+
param.rights.flat_map { |p| extract_param_nodes(p, kind, context) }
|
|
113
|
+
when Prism::RequiredParameterNode, Prism::OptionalParameterNode
|
|
114
|
+
param_node = IR::ParamNode.new(param.name, kind, default_value, [], convert_loc(param.location))
|
|
115
|
+
context.register_variable(param.name, param_node)
|
|
116
|
+
[param_node]
|
|
117
|
+
else
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private def convert_block(block_node, call_node, context)
|
|
123
|
+
# Create block parameter slots and register them in context
|
|
124
|
+
block_context = context.fork(:block)
|
|
125
|
+
|
|
126
|
+
if block_node.parameters.is_a?(Prism::BlockParametersNode)
|
|
127
|
+
parameters_node = block_node.parameters.parameters
|
|
128
|
+
if parameters_node
|
|
129
|
+
# Collect all parameters in order
|
|
130
|
+
params = []
|
|
131
|
+
params.concat(parameters_node.requireds) if parameters_node.requireds
|
|
132
|
+
params.concat(parameters_node.optionals) if parameters_node.optionals
|
|
133
|
+
|
|
134
|
+
params.each_with_index do |param, index|
|
|
135
|
+
param_name, param_loc = case param
|
|
136
|
+
when Prism::RequiredParameterNode
|
|
137
|
+
[param.name, param.location]
|
|
138
|
+
when Prism::OptionalParameterNode
|
|
139
|
+
[param.name, param.location]
|
|
140
|
+
when Prism::MultiTargetNode
|
|
141
|
+
# Destructuring parameters like |a, (b, c)|
|
|
142
|
+
# For now, skip complex cases
|
|
143
|
+
next
|
|
144
|
+
else
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
slot = IR::BlockParamSlot.new(index, call_node, [], convert_loc(param_loc))
|
|
149
|
+
call_node.block_params << slot
|
|
150
|
+
block_context.register_variable(param_name, slot)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Convert block body and return it for block return type inference
|
|
156
|
+
block_node.body ? convert(block_node.body, block_context) : nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Converter
|
|
6
|
+
# Container mutation methods (Hash#[]=, Array#[]=, Array#<<) for PrismConverter
|
|
7
|
+
class PrismConverter
|
|
8
|
+
# Check if node is a local variable node (for indexed assignment)
|
|
9
|
+
private def local_variable_node?(node)
|
|
10
|
+
node.is_a?(IR::LocalWriteNode) || node.is_a?(IR::LocalReadNode)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private def extract_literal_type(ir_node)
|
|
14
|
+
case ir_node
|
|
15
|
+
when IR::LiteralNode
|
|
16
|
+
ir_node.type
|
|
17
|
+
else
|
|
18
|
+
Types::Unknown.instance
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private def widen_to_hash_type(original_type, key_arg, value_type)
|
|
23
|
+
# When mixing key types, widen to generic HashType
|
|
24
|
+
new_key_type = infer_key_type(key_arg)
|
|
25
|
+
|
|
26
|
+
case original_type
|
|
27
|
+
when Types::HashShape
|
|
28
|
+
# HashShape with symbol keys + non-symbol key -> widen to Hash[Symbol | NewKeyType, ValueUnion]
|
|
29
|
+
original_key_type = Types::ClassInstance.for("Symbol")
|
|
30
|
+
original_value_types = original_type.fields.values.uniq
|
|
31
|
+
all_value_types = (original_value_types + [value_type]).uniq
|
|
32
|
+
|
|
33
|
+
combined_key_type = Types::Union.new([original_key_type, new_key_type].uniq)
|
|
34
|
+
combined_value_type = all_value_types.size == 1 ? all_value_types.first : Types::Union.new(all_value_types)
|
|
35
|
+
|
|
36
|
+
Types::HashType.new(combined_key_type, combined_value_type)
|
|
37
|
+
when Types::HashType
|
|
38
|
+
# Already a HashType, just union the key and value types
|
|
39
|
+
combined_key_type = union_types(original_type.key_type, new_key_type)
|
|
40
|
+
combined_value_type = union_types(original_type.value_type, value_type)
|
|
41
|
+
Types::HashType.new(combined_key_type, combined_value_type)
|
|
42
|
+
else
|
|
43
|
+
Types::HashType.new(new_key_type, value_type)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private def union_types(type1, type2)
|
|
48
|
+
return type2 if type1.nil? || type1.is_a?(Types::Unknown)
|
|
49
|
+
return type1 if type2.nil? || type2.is_a?(Types::Unknown)
|
|
50
|
+
return type1 if type1 == type2
|
|
51
|
+
|
|
52
|
+
types = []
|
|
53
|
+
types += type1.is_a?(Types::Union) ? type1.types : [type1]
|
|
54
|
+
types += type2.is_a?(Types::Union) ? type2.types : [type2]
|
|
55
|
+
Types::Union.new(types.uniq)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private def infer_key_type(key_arg)
|
|
59
|
+
case key_arg
|
|
60
|
+
when Prism::SymbolNode
|
|
61
|
+
Types::ClassInstance.for("Symbol")
|
|
62
|
+
when Prism::StringNode
|
|
63
|
+
Types::ClassInstance.for("String")
|
|
64
|
+
when Prism::IntegerNode
|
|
65
|
+
Types::ClassInstance.for("Integer")
|
|
66
|
+
else
|
|
67
|
+
Types::Unknown.instance
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if method is a container mutating method
|
|
72
|
+
private def container_mutating_method?(method, receiver_node)
|
|
73
|
+
return false unless local_variable_node?(receiver_node)
|
|
74
|
+
|
|
75
|
+
receiver_type = get_receiver_type(receiver_node)
|
|
76
|
+
return false unless receiver_type
|
|
77
|
+
|
|
78
|
+
case method
|
|
79
|
+
when :[]= then hash_like?(receiver_type) || array_like?(receiver_type)
|
|
80
|
+
when :<< then array_like?(receiver_type)
|
|
81
|
+
else false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get receiver's current type
|
|
86
|
+
private def get_receiver_type(receiver_node)
|
|
87
|
+
return nil unless receiver_node.respond_to?(:write_node)
|
|
88
|
+
|
|
89
|
+
write_node = receiver_node.write_node
|
|
90
|
+
return nil unless write_node
|
|
91
|
+
return nil unless write_node.respond_to?(:value)
|
|
92
|
+
|
|
93
|
+
value = write_node.value
|
|
94
|
+
return nil unless value.respond_to?(:type)
|
|
95
|
+
|
|
96
|
+
value.type
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if type is hash-like
|
|
100
|
+
private def hash_like?(type)
|
|
101
|
+
type.is_a?(Types::HashShape) || type.is_a?(Types::HashType)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if type is array-like
|
|
105
|
+
private def array_like?(type)
|
|
106
|
+
type.is_a?(Types::ArrayType) || type.is_a?(Types::TupleType)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Handle container mutation by creating new LocalWriteNode with merged type
|
|
110
|
+
private def handle_container_mutation(prism_node, receiver_node, args, context)
|
|
111
|
+
merged_type = compute_merged_type(receiver_node, prism_node.name, args, prism_node)
|
|
112
|
+
return receiver_node unless merged_type
|
|
113
|
+
|
|
114
|
+
# Block scope + outer variable → widen TupleType to ArrayType
|
|
115
|
+
is_outer_var = context.scope_type == :block && !context.owns_variable?(receiver_node.name)
|
|
116
|
+
merged_type = widen_tuple_to_array(merged_type) if is_outer_var
|
|
117
|
+
|
|
118
|
+
# Create new LiteralNode with merged type
|
|
119
|
+
value_node = IR::LiteralNode.new(merged_type, nil, nil, [], receiver_node.loc)
|
|
120
|
+
|
|
121
|
+
# Create new LocalWriteNode with merged type
|
|
122
|
+
new_write = IR::LocalWriteNode.new(
|
|
123
|
+
receiver_node.name, value_node, receiver_node.called_methods, convert_loc(prism_node.location)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Register for next line references
|
|
127
|
+
context.register_variable(receiver_node.name, new_write)
|
|
128
|
+
|
|
129
|
+
# Propagate widened type to parent context (so it's visible after block)
|
|
130
|
+
context.register_variable_in_parent(receiver_node.name, new_write) if is_outer_var
|
|
131
|
+
|
|
132
|
+
# Create new LocalReadNode pointing to new write_node
|
|
133
|
+
new_read = IR::LocalReadNode.new(
|
|
134
|
+
receiver_node.name, new_write, receiver_node.called_methods, receiver_node.loc
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Register the newly created nodes in location_index
|
|
138
|
+
if context.location_index
|
|
139
|
+
context.location_index.add(context.file_path, value_node, context.scope_id)
|
|
140
|
+
context.location_index.add(context.file_path, new_write, context.scope_id)
|
|
141
|
+
context.location_index.add(context.file_path, new_read, context.scope_id)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
new_read
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Compute merged type for container mutation
|
|
148
|
+
private def compute_merged_type(receiver_node, method, args, prism_node)
|
|
149
|
+
original_type = get_receiver_type(receiver_node)
|
|
150
|
+
return nil unless original_type
|
|
151
|
+
|
|
152
|
+
case method
|
|
153
|
+
when :[]=
|
|
154
|
+
if hash_like?(original_type)
|
|
155
|
+
compute_hash_assignment_type(original_type, args, prism_node)
|
|
156
|
+
elsif array_like?(original_type)
|
|
157
|
+
compute_array_assignment_type(original_type, args)
|
|
158
|
+
end
|
|
159
|
+
when :<<
|
|
160
|
+
compute_array_append_type(original_type, args) if array_like?(original_type)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Compute Hash type after indexed assignment
|
|
165
|
+
private def compute_hash_assignment_type(original_type, args, prism_node)
|
|
166
|
+
return nil unless args.size == 2
|
|
167
|
+
|
|
168
|
+
key_arg = prism_node.arguments.arguments[0]
|
|
169
|
+
value_type = extract_literal_type(args[1])
|
|
170
|
+
|
|
171
|
+
case original_type
|
|
172
|
+
when Types::HashShape
|
|
173
|
+
if key_arg.is_a?(Prism::SymbolNode)
|
|
174
|
+
# Symbol key → keep HashShape, add field
|
|
175
|
+
key_name = key_arg.value.to_sym
|
|
176
|
+
Types::HashShape.new(original_type.fields.merge(key_name => value_type))
|
|
177
|
+
else
|
|
178
|
+
# Non-symbol key → widen to HashType
|
|
179
|
+
widen_to_hash_type(original_type, key_arg, value_type)
|
|
180
|
+
end
|
|
181
|
+
when Types::HashType
|
|
182
|
+
# Empty hash (Unknown types) + symbol key → becomes HashShape with one field
|
|
183
|
+
if empty_hash_type?(original_type) && key_arg.is_a?(Prism::SymbolNode)
|
|
184
|
+
key_name = key_arg.value.to_sym
|
|
185
|
+
Types::HashShape.new({ key_name => value_type })
|
|
186
|
+
else
|
|
187
|
+
key_type = infer_key_type(key_arg)
|
|
188
|
+
Types::HashType.new(
|
|
189
|
+
union_types(original_type.key_type, key_type),
|
|
190
|
+
union_types(original_type.value_type, value_type)
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check if HashType is empty (has Unknown types)
|
|
197
|
+
private def empty_hash_type?(hash_type)
|
|
198
|
+
(hash_type.key_type.nil? || hash_type.key_type.is_a?(Types::Unknown)) &&
|
|
199
|
+
(hash_type.value_type.nil? || hash_type.value_type.is_a?(Types::Unknown))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Compute Array type after indexed assignment
|
|
203
|
+
private def compute_array_assignment_type(original_type, args)
|
|
204
|
+
return nil unless args.size == 2
|
|
205
|
+
|
|
206
|
+
value_type = extract_literal_type(args[1])
|
|
207
|
+
case original_type
|
|
208
|
+
when Types::TupleType
|
|
209
|
+
Types::TupleType.new(original_type.element_types + [value_type])
|
|
210
|
+
else
|
|
211
|
+
combined = union_types(original_type.element_type, value_type)
|
|
212
|
+
Types::ArrayType.new(combined)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Compute Array type after << operator
|
|
217
|
+
private def compute_array_append_type(original_type, args)
|
|
218
|
+
return nil unless args.size == 1
|
|
219
|
+
|
|
220
|
+
value_type = extract_literal_type(args[0])
|
|
221
|
+
case original_type
|
|
222
|
+
when Types::TupleType
|
|
223
|
+
Types::TupleType.new(original_type.element_types + [value_type])
|
|
224
|
+
else
|
|
225
|
+
combined = union_types(original_type.element_type, value_type)
|
|
226
|
+
Types::ArrayType.new(combined)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Widen TupleType to ArrayType (for block mutations where position info is meaningless)
|
|
231
|
+
private def widen_tuple_to_array(type)
|
|
232
|
+
return type unless type.is_a?(Types::TupleType)
|
|
233
|
+
|
|
234
|
+
unique = type.element_types.uniq
|
|
235
|
+
elem = unique.size == 1 ? unique.first : Types::Union.new(unique)
|
|
236
|
+
Types::ArrayType.new(elem)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ir"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
module Converter
|
|
8
|
+
class PrismConverter
|
|
9
|
+
# Context for tracking variable bindings during conversion
|
|
10
|
+
class Context
|
|
11
|
+
attr_reader :variables, :file_path, :location_index, :method_registry, :ivar_registry, :cvar_registry
|
|
12
|
+
attr_accessor :current_class, :current_method, :in_singleton_method
|
|
13
|
+
|
|
14
|
+
def initialize(parent = nil, file_path: nil, location_index: nil,
|
|
15
|
+
method_registry: nil, ivar_registry: nil, cvar_registry: nil)
|
|
16
|
+
@parent = parent
|
|
17
|
+
@variables = {} # name => node
|
|
18
|
+
@instance_variables = {} # @name => node (only for class-level context)
|
|
19
|
+
@narrowed_ivars = {} # @name => narrowed node (method-level, does not pollute class-level)
|
|
20
|
+
@constants = {} # name => dependency node (for constant alias tracking)
|
|
21
|
+
@scope_type = nil # :class, :method, :block, :top_level
|
|
22
|
+
@current_class = nil
|
|
23
|
+
@current_method = nil
|
|
24
|
+
@in_singleton_method = false
|
|
25
|
+
|
|
26
|
+
# Index/registry references (inherited from parent or set directly)
|
|
27
|
+
@file_path = file_path || parent&.file_path
|
|
28
|
+
@location_index = location_index || parent&.location_index
|
|
29
|
+
@method_registry = method_registry || parent&.method_registry
|
|
30
|
+
@ivar_registry = ivar_registry || parent&.ivar_registry
|
|
31
|
+
@cvar_registry = cvar_registry || parent&.cvar_registry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def register_variable(name, node)
|
|
35
|
+
@variables[name] = node
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lookup_variable(name)
|
|
39
|
+
@variables[name] || @parent&.lookup_variable(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Register an instance variable at the class level
|
|
43
|
+
# Instance variables are shared across all methods in a class
|
|
44
|
+
def register_instance_variable(name, node)
|
|
45
|
+
if @scope_type == :class
|
|
46
|
+
@instance_variables[name] = node
|
|
47
|
+
elsif @parent
|
|
48
|
+
@parent.register_instance_variable(name, node)
|
|
49
|
+
else
|
|
50
|
+
# Top-level instance variable, store locally
|
|
51
|
+
@instance_variables[name] = node
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Narrow an instance variable's type within the current method scope
|
|
56
|
+
# Does not pollute the class-level ivar definition
|
|
57
|
+
def narrow_instance_variable(name, node)
|
|
58
|
+
@narrowed_ivars[name] = node
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Lookup an instance variable, checking narrowed ivars first
|
|
62
|
+
def lookup_instance_variable(name)
|
|
63
|
+
return @narrowed_ivars[name] if @narrowed_ivars.key?(name)
|
|
64
|
+
|
|
65
|
+
if @scope_type == :class
|
|
66
|
+
@instance_variables[name]
|
|
67
|
+
elsif @parent
|
|
68
|
+
@parent.lookup_instance_variable(name)
|
|
69
|
+
else
|
|
70
|
+
@instance_variables[name]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Register a constant's dependency node for alias tracking
|
|
75
|
+
def register_constant(name, dependency_node)
|
|
76
|
+
@constants[name] = dependency_node
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Lookup a constant's dependency node (for alias resolution)
|
|
80
|
+
def lookup_constant(name)
|
|
81
|
+
@constants[name] || @parent&.lookup_constant(name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fork(scope_type)
|
|
85
|
+
child = Context.new(self)
|
|
86
|
+
child.instance_variable_set(:@scope_type, scope_type)
|
|
87
|
+
child.current_class = current_class_name
|
|
88
|
+
child.current_method = current_method_name
|
|
89
|
+
child.in_singleton_method = @in_singleton_method
|
|
90
|
+
child
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def scope_type
|
|
94
|
+
@scope_type || @parent&.scope_type
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get the current class name (from this context or parent)
|
|
98
|
+
def current_class_name
|
|
99
|
+
@current_class || @parent&.current_class_name
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get the current method name (from this context or parent)
|
|
103
|
+
def current_method_name
|
|
104
|
+
@current_method || @parent&.current_method_name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Generate scope_id for node lookup (e.g., "User#save" or "User" or "")
|
|
108
|
+
# For singleton methods, uses "<Class:ClassName>" format to match RubyIndexer convention
|
|
109
|
+
def scope_id
|
|
110
|
+
base_class_path = current_class_name || ""
|
|
111
|
+
class_path = if @in_singleton_method
|
|
112
|
+
# Singleton methods use "<Class:ClassName>" suffix
|
|
113
|
+
parent_name = IR.extract_last_name(base_class_path) || "Object"
|
|
114
|
+
base_class_path.empty? ? "<Class:Object>" : "#{base_class_path}::<Class:#{parent_name}>"
|
|
115
|
+
else
|
|
116
|
+
base_class_path
|
|
117
|
+
end
|
|
118
|
+
method_name = current_method_name
|
|
119
|
+
if method_name
|
|
120
|
+
"#{class_path}##{method_name}"
|
|
121
|
+
else
|
|
122
|
+
class_path
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if a variable is defined in this context (not inherited from parent)
|
|
127
|
+
def owns_variable?(name)
|
|
128
|
+
@variables.key?(name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Register a variable in the parent context (for block mutation propagation)
|
|
132
|
+
def register_variable_in_parent(name, node)
|
|
133
|
+
@parent&.register_variable(name, node)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get variables that were defined/modified in this context (not from parent)
|
|
137
|
+
def local_variables
|
|
138
|
+
@variables.keys
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|