type-guessr 0.0.1 → 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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -0
  3. data/exe/type-guessr +30 -0
  4. data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
  5. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
  6. data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
  7. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
  8. data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
  9. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  10. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  11. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  12. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  13. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  14. data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
  15. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  16. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
  17. data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
  18. data/lib/type-guessr.rb +3 -11
  19. data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
  20. data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
  21. data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
  22. data/lib/type_guessr/core/cache.rb +5 -0
  23. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
  24. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  25. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  26. data/lib/type_guessr/core/converter/context.rb +144 -0
  27. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  28. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  29. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  30. data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
  31. data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
  32. data/lib/type_guessr/core/converter/registration.rb +100 -0
  33. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  34. data/lib/type_guessr/core/converter.rb +4 -0
  35. data/lib/type_guessr/core/index/location_index.rb +32 -0
  36. data/lib/type_guessr/core/index.rb +3 -0
  37. data/lib/type_guessr/core/inference/resolver.rb +516 -349
  38. data/lib/type_guessr/core/inference.rb +4 -0
  39. data/lib/type_guessr/core/ir/nodes.rb +362 -103
  40. data/lib/type_guessr/core/ir.rb +3 -0
  41. data/lib/type_guessr/core/logger.rb +6 -13
  42. data/lib/type_guessr/core/node_context_helper.rb +126 -0
  43. data/lib/type_guessr/core/node_key_generator.rb +31 -0
  44. data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
  45. data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
  46. data/lib/type_guessr/core/registry/method_registry.rb +65 -38
  47. data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
  48. data/lib/type_guessr/core/registry.rb +6 -0
  49. data/lib/type_guessr/core/signature_builder.rb +39 -0
  50. data/lib/type_guessr/core/type_serializer.rb +96 -0
  51. data/lib/type_guessr/core/type_simplifier.rb +15 -12
  52. data/lib/type_guessr/core/types.rb +250 -32
  53. data/lib/type_guessr/core.rb +29 -0
  54. data/lib/type_guessr/mcp/file_watcher.rb +87 -0
  55. data/lib/type_guessr/mcp/server.rb +463 -0
  56. data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
  57. data/lib/type_guessr/version.rb +1 -1
  58. metadata +57 -8
  59. data/lib/type_guessr/core/rbs_provider.rb +0 -304
  60. data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
  61. data/lib/type_guessr/core/signature_provider.rb +0 -101
@@ -12,6 +12,12 @@ module TypeGuessr
12
12
  # This class only handles conversion (RBS → internal types).
13
13
  # Type variable substitution is handled separately by Type#substitute.
14
14
  class RBSConverter
15
+ # @param class_type_params [Hash{String => Array<Symbol>}] class-level type parameter names
16
+ # e.g., { "Set" => [:A], "Enumerator" => [:Elem, :Return] }
17
+ def initialize(class_type_params = {})
18
+ @class_type_params = class_type_params
19
+ end
20
+
15
21
  # Convert RBS type to internal type system
16
22
  # @param rbs_type [RBS::Types::t] the RBS type
17
23
  # @return [Types::Type] internal type representation (TypeVariables preserved)
@@ -31,11 +37,11 @@ module TypeGuessr
31
37
  convert_tuple(rbs_type)
32
38
  when RBS::Types::Bases::Bool
33
39
  # bool is a type alias for TrueClass | FalseClass
34
- Types::ClassInstance.new("bool")
40
+ Types::ClassInstance.for("bool")
35
41
  when RBS::Types::Bases::Void
36
- Types::ClassInstance.new("void")
42
+ Types::ClassInstance.for("void")
37
43
  when RBS::Types::Bases::Nil
38
- Types::ClassInstance.new("NilClass")
44
+ Types::ClassInstance.for("NilClass")
39
45
  when RBS::Types::Bases::Self
40
46
  Types::SelfType.instance
41
47
  when RBS::Types::Bases::Instance
@@ -47,12 +53,7 @@ module TypeGuessr
47
53
  end
48
54
  end
49
55
 
50
- private
51
-
52
- # Convert RBS ClassInstance to internal type
53
- # @param rbs_type [RBS::Types::ClassInstance] RBS class instance
54
- # @return [Types::Type] internal type
55
- def convert_class_instance(rbs_type)
56
+ private def convert_class_instance(rbs_type)
56
57
  class_name = rbs_type.name.to_s.delete_prefix("::")
57
58
 
58
59
  # Handle Array with type parameter
@@ -61,15 +62,35 @@ module TypeGuessr
61
62
  return Types::ArrayType.new(element_type)
62
63
  end
63
64
 
64
- # For other generic types, return ClassInstance (ignore type args for now)
65
- # TODO: Add HashType with key/value types in the future
66
- Types::ClassInstance.new(class_name)
65
+ # Handle Hash with key/value type parameters
66
+ if class_name == "Hash" && rbs_type.args.size == 2
67
+ key_type = convert(rbs_type.args[0])
68
+ value_type = convert(rbs_type.args[1])
69
+ return Types::HashType.new(key_type, value_type)
70
+ end
71
+
72
+ # Handle Range with element type parameter
73
+ if class_name == "Range" && rbs_type.args.size == 1
74
+ element_type = convert(rbs_type.args.first)
75
+ return Types::RangeType.new(element_type)
76
+ end
77
+
78
+ # For other generic types, build type_params hash from class definition
79
+ param_names = @class_type_params[class_name]
80
+ if param_names && rbs_type.args.any?
81
+ type_params = param_names.zip(rbs_type.args).to_h do |name, arg|
82
+ [name, arg ? convert(arg) : Types::Unknown.instance]
83
+ end
84
+ return Types::ClassInstance.for(class_name, type_params)
85
+ end
86
+
87
+ Types::ClassInstance.for(class_name)
67
88
  end
68
89
 
69
90
  # Convert RBS Union to internal Union type
70
91
  # @param rbs_type [RBS::Types::Union] RBS union type
71
92
  # @return [Types::Union] internal union type
72
- def convert_union(rbs_type)
93
+ private def convert_union(rbs_type)
73
94
  types = rbs_type.types.map { |t| convert(t) }
74
95
  Types::Union.new(types)
75
96
  end
@@ -78,7 +99,7 @@ module TypeGuessr
78
99
  # Tuples like [K, V] are converted to Array[K | V]
79
100
  # @param rbs_type [RBS::Types::Tuple] RBS tuple type
80
101
  # @return [Types::ArrayType] internal array type with union element type
81
- def convert_tuple(rbs_type)
102
+ private def convert_tuple(rbs_type)
82
103
  element_types = rbs_type.types.map { |t| convert(t) }
83
104
  Types::ArrayType.new(Types::Union.new(element_types))
84
105
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeGuessr
4
+ module Core
5
+ module Converter
6
+ # Node registration and location conversion methods for PrismConverter
7
+ class PrismConverter
8
+ private def convert_loc(prism_location)
9
+ prism_location.start_offset
10
+ end
11
+
12
+ # Register node in location_index and registries during conversion
13
+ # This eliminates the need for a separate tree traversal after conversion
14
+ private def register_node(node, context)
15
+ return unless context.location_index
16
+
17
+ case node
18
+ when IR::DefNode
19
+ # DefNode uses singleton-adjusted method_scope for registration
20
+ method_scope = singleton_scope_for(context.current_class_name || "", singleton: node.singleton)
21
+ context.location_index.add(context.file_path, node, method_scope)
22
+ register_method(node, context)
23
+
24
+ # Register params (created directly, not via convert)
25
+ # Use method scope with method name for params
26
+ param_scope = method_scope.empty? ? "##{node.name}" : "#{method_scope}##{node.name}"
27
+ node.params&.each do |param|
28
+ context.location_index.add(context.file_path, param, param_scope)
29
+ end
30
+ when IR::ClassModuleNode
31
+ # ClassModuleNode uses parent scope for registration
32
+ context.location_index.add(context.file_path, node, context.scope_id)
33
+ register_class_module(node, context)
34
+ when IR::CallNode
35
+ context.location_index.add(context.file_path, node, context.scope_id)
36
+ # Register block params (created directly, not via convert)
37
+ node.block_params&.each do |param|
38
+ context.location_index.add(context.file_path, param, context.scope_id)
39
+ end
40
+ when IR::InstanceVariableWriteNode
41
+ context.location_index.add(context.file_path, node, context.scope_id)
42
+ context.ivar_registry&.register(node.class_name, node.name, node, file_path: context.file_path)
43
+ when IR::ClassVariableWriteNode
44
+ context.location_index.add(context.file_path, node, context.scope_id)
45
+ context.cvar_registry&.register(node.class_name, node.name, node, file_path: context.file_path)
46
+ else
47
+ # All other nodes (MergeNode, LiteralNode, etc.)
48
+ context.location_index.add(context.file_path, node, context.scope_id)
49
+ end
50
+ end
51
+
52
+ # Register method in method_registry
53
+ # Only registers top-level methods; class methods are handled by register_class_module
54
+ private def register_method(node, context)
55
+ return unless context.method_registry
56
+
57
+ # Only register top-level methods (no class context)
58
+ return unless (context.current_class_name || "").empty?
59
+
60
+ context.method_registry.register("", node.name.to_s, node, file_path: context.file_path)
61
+ end
62
+
63
+ # Register methods from a class/module in method_registry
64
+ private def register_class_module(node, context)
65
+ return unless context.method_registry
66
+
67
+ # Build the full class path from parent context + node name
68
+ parent_path = context.current_class_name || ""
69
+ class_path = parent_path.empty? ? node.name : "#{parent_path}::#{node.name}"
70
+
71
+ # Register each method in the class (nested classes are handled recursively via convert)
72
+ node.methods&.each do |method|
73
+ next if method.is_a?(IR::ClassModuleNode)
74
+
75
+ method_scope = singleton_scope_for(class_path, singleton: method.singleton)
76
+ context.method_registry.register(method_scope, method.name.to_s, method, file_path: context.file_path)
77
+
78
+ # module_function: also register as singleton method
79
+ if method.module_function
80
+ singleton_scope = singleton_scope_for(class_path, singleton: true)
81
+ context.method_registry.register(singleton_scope, method.name.to_s, method, file_path: context.file_path)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Build singleton class scope for method registration/lookup
87
+ # Singleton methods use "<Class:ClassName>" suffix to match RubyIndexer convention
88
+ # @param scope [String] Base scope (e.g., "RBS::Environment")
89
+ # @param singleton [Boolean] Whether the method is a singleton method
90
+ # @return [String] Scope with singleton class suffix if applicable
91
+ private def singleton_scope_for(scope, singleton:)
92
+ return scope unless singleton
93
+
94
+ parent_name = IR.extract_last_name(scope) || "Object"
95
+ scope.empty? ? "<Class:Object>" : "#{scope}::<Class:#{parent_name}>"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeGuessr
4
+ module Core
5
+ module Converter
6
+ # Variable read/write and compound assignment methods for PrismConverter
7
+ class PrismConverter
8
+ private def convert_local_variable_write(prism_node, context)
9
+ value_node = convert(prism_node.value, context)
10
+ write_node = IR::LocalWriteNode.new(prism_node.name, value_node, [], convert_loc(prism_node.location))
11
+ context.register_variable(prism_node.name, write_node)
12
+ write_node
13
+ end
14
+
15
+ private def convert_local_variable_read(prism_node, context)
16
+ # Look up the most recent assignment
17
+ write_node = context.lookup_variable(prism_node.name)
18
+
19
+ IR::LocalReadNode.new(
20
+ prism_node.name,
21
+ write_node,
22
+ # Share called_methods array for method-based inference
23
+ # nil case: rescue binding (=> e), pattern matching binding, etc. (not yet implemented)
24
+ write_node&.called_methods || [],
25
+ convert_loc(prism_node.location)
26
+ )
27
+ end
28
+
29
+ private def convert_instance_variable_write(prism_node, context)
30
+ value_node = convert(prism_node.value, context)
31
+ class_name = context.current_class_name
32
+
33
+ write_node = IR::InstanceVariableWriteNode.new(
34
+ prism_node.name,
35
+ class_name,
36
+ value_node,
37
+ # Share called_methods with value node for type propagation
38
+ # nil case: value is an unhandled node type (convert() returns nil)
39
+ value_node&.called_methods || [],
40
+ convert_loc(prism_node.location)
41
+ )
42
+ # Register at class level so it's visible across methods
43
+ context.register_instance_variable(prism_node.name, write_node)
44
+ write_node
45
+ end
46
+
47
+ private def convert_instance_variable_read(prism_node, context)
48
+ # Look up from class level first
49
+ write_node = context.lookup_instance_variable(prism_node.name)
50
+
51
+ IR::InstanceVariableReadNode.new(
52
+ prism_node.name,
53
+ context.current_class_name,
54
+ write_node,
55
+ # Share called_methods array for method-based inference
56
+ # nil case: instance variable read before any assignment in current file
57
+ write_node&.called_methods || [],
58
+ convert_loc(prism_node.location)
59
+ )
60
+ end
61
+
62
+ private def convert_class_variable_write(prism_node, context)
63
+ value_node = convert(prism_node.value, context)
64
+
65
+ write_node = IR::ClassVariableWriteNode.new(
66
+ prism_node.name,
67
+ context.current_class_name,
68
+ value_node,
69
+ # Share called_methods with value node for type propagation
70
+ # nil case: value is an unhandled node type (e.g., LambdaNode)
71
+ value_node&.called_methods || [],
72
+ convert_loc(prism_node.location)
73
+ )
74
+ context.register_variable(prism_node.name, write_node)
75
+ write_node
76
+ end
77
+
78
+ private def convert_class_variable_read(prism_node, context)
79
+ write_node = context.lookup_variable(prism_node.name)
80
+
81
+ IR::ClassVariableReadNode.new(
82
+ prism_node.name,
83
+ context.current_class_name,
84
+ write_node,
85
+ # Share called_methods array for method-based inference
86
+ # nil case: class variable read before any assignment in current file
87
+ write_node&.called_methods || [],
88
+ convert_loc(prism_node.location)
89
+ )
90
+ end
91
+
92
+ # Compound assignment: x ||= value
93
+ # Result type is union of original and new value type
94
+ private def convert_local_variable_or_write(prism_node, context)
95
+ convert_or_write(prism_node, context, :local)
96
+ end
97
+
98
+ # Compound assignment: x &&= value
99
+ # Result type is union of original and new value type
100
+ private def convert_local_variable_and_write(prism_node, context)
101
+ convert_and_write(prism_node, context, :local)
102
+ end
103
+
104
+ # Compound assignment: x += value, x -= value, etc.
105
+ # Result type depends on the operator method return type
106
+ private def convert_local_variable_operator_write(prism_node, context)
107
+ convert_operator_write(prism_node, context, :local)
108
+ end
109
+
110
+ private def convert_instance_variable_or_write(prism_node, context)
111
+ convert_or_write(prism_node, context, :instance)
112
+ end
113
+
114
+ private def convert_instance_variable_and_write(prism_node, context)
115
+ convert_and_write(prism_node, context, :instance)
116
+ end
117
+
118
+ private def convert_instance_variable_operator_write(prism_node, context)
119
+ convert_operator_write(prism_node, context, :instance)
120
+ end
121
+
122
+ # Generic ||= handler
123
+ # x ||= value means: if x is nil/false, x = value, else keep x
124
+ # Uses OrNode to apply truthiness filtering (removes nil/false from LHS)
125
+ private def convert_or_write(prism_node, context, kind)
126
+ original_node = lookup_by_kind(prism_node.name, kind, context)
127
+ value_node = convert(prism_node.value, context)
128
+
129
+ or_node = if original_node
130
+ IR::OrNode.new(
131
+ original_node,
132
+ value_node,
133
+ [],
134
+ convert_loc(prism_node.location)
135
+ )
136
+ else
137
+ value_node
138
+ end
139
+
140
+ write_node = create_write_node(prism_node.name, kind, or_node, context, prism_node.location)
141
+ register_by_kind(prism_node.name, write_node, kind, context)
142
+ write_node
143
+ end
144
+
145
+ # Generic &&= handler
146
+ # x &&= value means: if x is truthy, x = value, else keep x
147
+ # Type is union of original type and value type
148
+ private def convert_and_write(prism_node, context, kind)
149
+ original_node = lookup_by_kind(prism_node.name, kind, context)
150
+ value_node = convert(prism_node.value, context)
151
+
152
+ # Create merge node for union type (original | value)
153
+ branches = []
154
+ branches << original_node if original_node
155
+ branches << value_node
156
+
157
+ merge_node = if branches.size == 1
158
+ branches.first
159
+ else
160
+ IR::MergeNode.new(
161
+ branches,
162
+ [],
163
+ convert_loc(prism_node.location)
164
+ )
165
+ end
166
+
167
+ write_node = create_write_node(prism_node.name, kind, merge_node, context, prism_node.location)
168
+ register_by_kind(prism_node.name, write_node, kind, context)
169
+ write_node
170
+ end
171
+
172
+ # Generic operator write handler (+=, -=, *=, etc.)
173
+ # x += value is equivalent to x = x.+(value)
174
+ # Type is the return type of the operator method
175
+ private def convert_operator_write(prism_node, context, kind)
176
+ original_node = lookup_by_kind(prism_node.name, kind, context)
177
+ value_node = convert(prism_node.value, context)
178
+
179
+ # Create a call node representing x.operator(value)
180
+ call_node = IR::CallNode.new(
181
+ prism_node.binary_operator, original_node, [value_node], [], nil, false, [], convert_loc(prism_node.location)
182
+ )
183
+
184
+ # Create write node with call result as value
185
+ write_node = create_write_node(prism_node.name, kind, call_node, context, prism_node.location)
186
+ register_by_kind(prism_node.name, write_node, kind, context)
187
+ write_node
188
+ end
189
+
190
+ # Helper to create the appropriate write node type based on kind
191
+ private def create_write_node(name, kind, value, context, location)
192
+ loc = convert_loc(location)
193
+ case kind
194
+ when :local
195
+ IR::LocalWriteNode.new(name, value, [], loc)
196
+ when :instance
197
+ IR::InstanceVariableWriteNode.new(name, context.current_class_name, value, [], loc)
198
+ when :class
199
+ IR::ClassVariableWriteNode.new(name, context.current_class_name, value, [], loc)
200
+ end
201
+ end
202
+
203
+ # Helper to lookup variable by kind
204
+ private def lookup_by_kind(name, kind, context)
205
+ case kind
206
+ when :instance
207
+ context.lookup_instance_variable(name)
208
+ else
209
+ context.lookup_variable(name)
210
+ end
211
+ end
212
+
213
+ # Helper to register variable by kind
214
+ private def register_by_kind(name, node, kind, context)
215
+ case kind
216
+ when :instance
217
+ context.register_instance_variable(name, node)
218
+ else
219
+ context.register_variable(name, node)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "converter/prism_converter"
4
+ require_relative "converter/rbs_converter"
@@ -66,6 +66,38 @@ module TypeGuessr
66
66
  total_nodes: @key_index.size
67
67
  }
68
68
  end
69
+
70
+ # Get all indexed file paths
71
+ # @return [Array<String>] List of file paths
72
+ def all_files
73
+ @file_keys.keys
74
+ end
75
+
76
+ # Iterate over all indexed nodes with their scope IDs
77
+ # @yield [node, scope_id] Block to execute for each node
78
+ def each_node(&block)
79
+ return enum_for(:each_node) unless block
80
+
81
+ @key_index.each do |key, node|
82
+ # Extract scope_id from "scope_id:node_hash" format
83
+ scope_id = key.sub(/:#{Regexp.escape(node.node_hash)}$/, "")
84
+ block.call(node, scope_id)
85
+ end
86
+ end
87
+
88
+ # Find the scope ID for a node within a file
89
+ # @param file_path [String] Absolute file path
90
+ # @param node [TypeGuessr::Core::IR::Node] IR node to find
91
+ # @return [String, nil] Scope ID or nil if not found
92
+ def scope_for_node(file_path, node)
93
+ keys = @file_keys[file_path] || []
94
+ node_hash = node.node_hash
95
+ matching_key = keys.find { |k| k.end_with?(":#{node_hash}") }
96
+ return nil unless matching_key
97
+
98
+ # Extract scope_id from "scope_id:node_hash" format
99
+ matching_key.sub(/:#{Regexp.escape(node_hash)}$/, "")
100
+ end
69
101
  end
70
102
  end
71
103
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "index/location_index"