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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/ruby_lsp/type_guessr/addon.rb +4 -5
  4. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +17 -0
  5. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +3 -3
  6. data/lib/ruby_lsp/type_guessr/debug_server.rb +2 -2
  7. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  8. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  9. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  10. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  11. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  12. data/lib/ruby_lsp/type_guessr/hover.rb +46 -40
  13. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  14. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +90 -16
  15. data/lib/type-guessr.rb +2 -13
  16. data/lib/type_guessr/core/cache/gem_signature_cache.rb +3 -2
  17. data/lib/type_guessr/core/cache.rb +5 -0
  18. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +2 -2
  19. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  20. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  21. data/lib/type_guessr/core/converter/context.rb +144 -0
  22. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  23. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  24. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  25. data/lib/type_guessr/core/converter/prism_converter.rb +9 -1682
  26. data/lib/type_guessr/core/converter/rbs_converter.rb +15 -1
  27. data/lib/type_guessr/core/converter/registration.rb +100 -0
  28. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  29. data/lib/type_guessr/core/converter.rb +4 -0
  30. data/lib/type_guessr/core/index.rb +3 -0
  31. data/lib/type_guessr/core/inference/resolver.rb +206 -208
  32. data/lib/type_guessr/core/inference.rb +4 -0
  33. data/lib/type_guessr/core/ir.rb +3 -0
  34. data/lib/type_guessr/core/logger.rb +3 -5
  35. data/lib/type_guessr/core/registry/method_registry.rb +9 -0
  36. data/lib/type_guessr/core/registry/signature_registry.rb +73 -16
  37. data/lib/type_guessr/core/registry.rb +6 -0
  38. data/lib/type_guessr/core/type_serializer.rb +18 -14
  39. data/lib/type_guessr/core/type_simplifier.rb +5 -5
  40. data/lib/type_guessr/core/types.rb +64 -22
  41. data/lib/type_guessr/core.rb +29 -0
  42. data/lib/type_guessr/mcp/server.rb +55 -46
  43. data/lib/type_guessr/mcp/standalone_runtime.rb +70 -110
  44. data/lib/type_guessr/version.rb +1 -1
  45. metadata +25 -5
  46. 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