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.
- checksums.yaml +4 -4
- data/README.md +41 -0
- data/exe/type-guessr +30 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
- data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
- data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
- data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
- 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 +129 -261
- data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
- data/lib/type-guessr.rb +3 -11
- data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
- data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
- data/lib/type_guessr/core/cache.rb +5 -0
- data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
- 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 +246 -0
- data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
- data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
- 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/location_index.rb +32 -0
- data/lib/type_guessr/core/index.rb +3 -0
- data/lib/type_guessr/core/inference/resolver.rb +516 -349
- data/lib/type_guessr/core/inference.rb +4 -0
- data/lib/type_guessr/core/ir/nodes.rb +362 -103
- data/lib/type_guessr/core/ir.rb +3 -0
- data/lib/type_guessr/core/logger.rb +6 -13
- data/lib/type_guessr/core/node_context_helper.rb +126 -0
- data/lib/type_guessr/core/node_key_generator.rb +31 -0
- data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
- data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
- data/lib/type_guessr/core/registry/method_registry.rb +65 -38
- data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
- data/lib/type_guessr/core/registry.rb +6 -0
- data/lib/type_guessr/core/signature_builder.rb +39 -0
- data/lib/type_guessr/core/type_serializer.rb +96 -0
- data/lib/type_guessr/core/type_simplifier.rb +15 -12
- data/lib/type_guessr/core/types.rb +250 -32
- data/lib/type_guessr/core.rb +29 -0
- data/lib/type_guessr/mcp/file_watcher.rb +87 -0
- data/lib/type_guessr/mcp/server.rb +463 -0
- data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
- data/lib/type_guessr/version.rb +1 -1
- metadata +57 -8
- data/lib/type_guessr/core/rbs_provider.rb +0 -304
- data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
- data/lib/type_guessr/core/signature_provider.rb +0 -101
|
@@ -7,9 +7,12 @@ module TypeGuessr
|
|
|
7
7
|
# Simplifies types by unwrapping single-element unions and
|
|
8
8
|
# unifying parent/child class relationships
|
|
9
9
|
class TypeSimplifier
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# Maximum number of types in a Union before considering it too ambiguous
|
|
11
|
+
MAX_ELEMENT_IN_UNION = 3
|
|
12
|
+
|
|
13
|
+
# @param code_index [#ancestors_of, nil] Adapter for inheritance lookup
|
|
14
|
+
def initialize(code_index: nil)
|
|
15
|
+
@code_index = code_index
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
# Simplify a type
|
|
@@ -24,28 +27,28 @@ module TypeGuessr
|
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def simplify_union(union)
|
|
30
|
+
private def simplify_union(union)
|
|
30
31
|
types = union.types
|
|
31
32
|
|
|
32
33
|
# 1. Single element: unwrap
|
|
33
34
|
return types.first if types.size == 1
|
|
34
35
|
|
|
35
|
-
# 2.
|
|
36
|
-
types =
|
|
36
|
+
# 2. Remove subclass types when parent class is also present
|
|
37
|
+
types = remove_subclass_types(types) if @code_index
|
|
37
38
|
|
|
38
39
|
# 3. Check again after filtering
|
|
39
40
|
return types.first if types.size == 1
|
|
40
41
|
|
|
42
|
+
return Types::Unknown.instance if types.size > MAX_ELEMENT_IN_UNION
|
|
43
|
+
|
|
41
44
|
# 4. Multiple elements remain: create new Union
|
|
42
45
|
Types::Union.new(types)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
#
|
|
48
|
+
# Remove types whose ancestor is also in the list (e.g., remove Integer when Numeric is present)
|
|
46
49
|
# @param types [Array<Types::Type>] List of types
|
|
47
|
-
# @return [Array<Types::Type>] Filtered list with
|
|
48
|
-
def
|
|
50
|
+
# @return [Array<Types::Type>] Filtered list with subclass types removed
|
|
51
|
+
private def remove_subclass_types(types)
|
|
49
52
|
# Extract class names from ClassInstance types
|
|
50
53
|
class_names = types.filter_map do |t|
|
|
51
54
|
t.name if t.is_a?(Types::ClassInstance)
|
|
@@ -54,7 +57,7 @@ module TypeGuessr
|
|
|
54
57
|
types.reject do |type|
|
|
55
58
|
next false unless type.is_a?(Types::ClassInstance)
|
|
56
59
|
|
|
57
|
-
ancestors = @
|
|
60
|
+
ancestors = @code_index.ancestors_of(type.name)
|
|
58
61
|
# Check if any ancestor (excluding self) is also in the list
|
|
59
62
|
ancestors.any? { |ancestor| ancestor != type.name && class_names.include?(ancestor) }
|
|
60
63
|
end
|
|
@@ -36,9 +36,15 @@ module TypeGuessr
|
|
|
36
36
|
# Get type variable substitutions for this type
|
|
37
37
|
# Used for substituting type variables in block parameters
|
|
38
38
|
# @return [Hash{Symbol => Type}] type variable substitutions (e.g., { Elem: Integer, K: Symbol, V: String })
|
|
39
|
-
def
|
|
39
|
+
def type_parameter_bindings
|
|
40
40
|
{}
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Readable inspect output for debugging
|
|
44
|
+
# @return [String]
|
|
45
|
+
def inspect
|
|
46
|
+
"#<#{self.class.name.split("::").last}>"
|
|
47
|
+
end
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
# Unknown type - no information available
|
|
@@ -50,32 +56,87 @@ module TypeGuessr
|
|
|
50
56
|
end
|
|
51
57
|
end
|
|
52
58
|
|
|
59
|
+
# Unguessed type - type exists but has not been inferred yet
|
|
60
|
+
# Used for lazy gem inference: method signatures are cached with
|
|
61
|
+
# Unguessed return/param types until background inference completes.
|
|
62
|
+
class Unguessed < Type
|
|
63
|
+
include Singleton
|
|
64
|
+
|
|
65
|
+
def to_s
|
|
66
|
+
"unguessed"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
53
70
|
# ClassInstance - instance of a class
|
|
71
|
+
# @param type_params [Hash{Symbol => Type}, nil] type parameters (e.g., { A: String } for Set[String])
|
|
54
72
|
class ClassInstance < Type
|
|
55
|
-
|
|
73
|
+
CACHE = {} # rubocop:disable Style/MutableConstant
|
|
74
|
+
GENERIC_CACHE = {} # rubocop:disable Style/MutableConstant
|
|
75
|
+
|
|
76
|
+
# Factory method that caches instances for reuse
|
|
77
|
+
# @param name [String] The class name
|
|
78
|
+
# @param type_params [Hash{Symbol => Type}, nil] type parameters
|
|
79
|
+
# @return [ClassInstance] Cached or new instance
|
|
80
|
+
def self.for(name, type_params = nil)
|
|
81
|
+
if type_params.nil?
|
|
82
|
+
CACHE[name] ||= new(name).freeze
|
|
83
|
+
else
|
|
84
|
+
key = [name, type_params]
|
|
85
|
+
GENERIC_CACHE[key] ||= new(name, type_params).freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
56
88
|
|
|
57
|
-
|
|
89
|
+
attr_reader :name, :type_params
|
|
90
|
+
|
|
91
|
+
def initialize(name, type_params = nil)
|
|
58
92
|
super()
|
|
59
93
|
@name = name
|
|
94
|
+
@type_params = type_params&.freeze
|
|
60
95
|
end
|
|
61
96
|
|
|
62
97
|
def eql?(other)
|
|
63
|
-
super && @name == other.name
|
|
98
|
+
super && @name == other.name && @type_params == other.type_params
|
|
64
99
|
end
|
|
65
100
|
|
|
66
101
|
def hash
|
|
67
|
-
[self.class, @name].hash
|
|
102
|
+
[self.class, @name, @type_params].hash
|
|
68
103
|
end
|
|
69
104
|
|
|
70
105
|
def to_s
|
|
71
|
-
case @name
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
base = case @name
|
|
107
|
+
when "NilClass" then "nil"
|
|
108
|
+
when "TrueClass" then "true"
|
|
109
|
+
when "FalseClass" then "false"
|
|
110
|
+
else @name
|
|
111
|
+
end
|
|
112
|
+
if @type_params&.any?
|
|
113
|
+
"#{base}[#{@type_params.values.join(", ")}]"
|
|
114
|
+
else
|
|
115
|
+
base
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def inspect
|
|
120
|
+
if @type_params&.any?
|
|
121
|
+
"#<ClassInstance:#{@name}[#{@type_params.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")}]>"
|
|
122
|
+
else
|
|
123
|
+
"#<ClassInstance:#{@name}>"
|
|
76
124
|
end
|
|
77
125
|
end
|
|
78
126
|
|
|
127
|
+
def type_parameter_bindings
|
|
128
|
+
@type_params || {}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def substitute(substitutions)
|
|
132
|
+
return self if @type_params.nil? || @type_params.empty?
|
|
133
|
+
|
|
134
|
+
new_params = @type_params.transform_values { |t| t.substitute(substitutions) }
|
|
135
|
+
return self if new_params == @type_params
|
|
136
|
+
|
|
137
|
+
ClassInstance.new(@name, new_params)
|
|
138
|
+
end
|
|
139
|
+
|
|
79
140
|
def rbs_class_name
|
|
80
141
|
@name
|
|
81
142
|
end
|
|
@@ -102,6 +163,10 @@ module TypeGuessr
|
|
|
102
163
|
"singleton(#{@name})"
|
|
103
164
|
end
|
|
104
165
|
|
|
166
|
+
def inspect
|
|
167
|
+
"#<SingletonType:#{@name}>"
|
|
168
|
+
end
|
|
169
|
+
|
|
105
170
|
def rbs_class_name
|
|
106
171
|
@name
|
|
107
172
|
end
|
|
@@ -111,7 +176,7 @@ module TypeGuessr
|
|
|
111
176
|
class Union < Type
|
|
112
177
|
attr_reader :types
|
|
113
178
|
|
|
114
|
-
def initialize(types, cutoff:
|
|
179
|
+
def initialize(types, cutoff: 10)
|
|
115
180
|
super()
|
|
116
181
|
@types = normalize(types, cutoff)
|
|
117
182
|
end
|
|
@@ -130,13 +195,19 @@ module TypeGuessr
|
|
|
130
195
|
def to_s
|
|
131
196
|
if bool_type?
|
|
132
197
|
"bool"
|
|
198
|
+
elsif nullable_bool_type?
|
|
199
|
+
"bool?"
|
|
133
200
|
elsif optional_type?
|
|
134
|
-
"
|
|
201
|
+
"#{non_nil_type}?"
|
|
135
202
|
else
|
|
136
203
|
@types.map(&:to_s).sort.join(" | ")
|
|
137
204
|
end
|
|
138
205
|
end
|
|
139
206
|
|
|
207
|
+
def inspect
|
|
208
|
+
"#<Union:#{@types.join("|")}>"
|
|
209
|
+
end
|
|
210
|
+
|
|
140
211
|
def substitute(substitutions)
|
|
141
212
|
new_types = @types.map { |t| t.substitute(substitutions) }
|
|
142
213
|
return self if new_types.zip(@types).all? { |new_t, old_t| new_t.equal?(old_t) }
|
|
@@ -144,9 +215,7 @@ module TypeGuessr
|
|
|
144
215
|
Union.new(new_types)
|
|
145
216
|
end
|
|
146
217
|
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
def bool_type?
|
|
218
|
+
private def bool_type?
|
|
150
219
|
return false unless @types.size == 2
|
|
151
220
|
|
|
152
221
|
has_true = @types.any? { |t| t.is_a?(ClassInstance) && t.name == "TrueClass" }
|
|
@@ -154,19 +223,28 @@ module TypeGuessr
|
|
|
154
223
|
has_true && has_false
|
|
155
224
|
end
|
|
156
225
|
|
|
157
|
-
def
|
|
226
|
+
private def nullable_bool_type?
|
|
227
|
+
return false unless @types.size == 3
|
|
228
|
+
|
|
229
|
+
has_true = @types.any? { |t| t.is_a?(ClassInstance) && t.name == "TrueClass" }
|
|
230
|
+
has_false = @types.any? { |t| t.is_a?(ClassInstance) && t.name == "FalseClass" }
|
|
231
|
+
has_nil = @types.any? { |t| nil_type?(t) }
|
|
232
|
+
has_true && has_false && has_nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private def optional_type?
|
|
158
236
|
@types.size == 2 && @types.any? { |t| nil_type?(t) }
|
|
159
237
|
end
|
|
160
238
|
|
|
161
|
-
def nil_type?(type)
|
|
239
|
+
private def nil_type?(type)
|
|
162
240
|
type.is_a?(ClassInstance) && type.name == "NilClass"
|
|
163
241
|
end
|
|
164
242
|
|
|
165
|
-
def non_nil_type
|
|
243
|
+
private def non_nil_type
|
|
166
244
|
@types.find { |t| !nil_type?(t) }
|
|
167
245
|
end
|
|
168
246
|
|
|
169
|
-
def normalize(types, cutoff)
|
|
247
|
+
private def normalize(types, cutoff)
|
|
170
248
|
# Flatten nested unions
|
|
171
249
|
flattened = flatten_unions(types)
|
|
172
250
|
|
|
@@ -180,20 +258,20 @@ module TypeGuessr
|
|
|
180
258
|
apply_cutoff(filtered, cutoff)
|
|
181
259
|
end
|
|
182
260
|
|
|
183
|
-
def flatten_unions(types)
|
|
261
|
+
private def flatten_unions(types)
|
|
184
262
|
types.flat_map do |type|
|
|
185
263
|
type.is_a?(Union) ? type.types : type
|
|
186
264
|
end
|
|
187
265
|
end
|
|
188
266
|
|
|
189
|
-
def simplify_if_unknown_present(types)
|
|
267
|
+
private def simplify_if_unknown_present(types)
|
|
190
268
|
return types if types.size <= 1
|
|
191
269
|
|
|
192
|
-
has_unknown = types.any?
|
|
270
|
+
has_unknown = types.any?(Unknown)
|
|
193
271
|
has_unknown ? [Unknown.instance] : types
|
|
194
272
|
end
|
|
195
273
|
|
|
196
|
-
def apply_cutoff(types, cutoff)
|
|
274
|
+
private def apply_cutoff(types, cutoff)
|
|
197
275
|
types.take(cutoff)
|
|
198
276
|
end
|
|
199
277
|
end
|
|
@@ -219,6 +297,10 @@ module TypeGuessr
|
|
|
219
297
|
"Array[#{@element_type}]"
|
|
220
298
|
end
|
|
221
299
|
|
|
300
|
+
def inspect
|
|
301
|
+
"#<ArrayType:#{@element_type}>"
|
|
302
|
+
end
|
|
303
|
+
|
|
222
304
|
def substitute(substitutions)
|
|
223
305
|
new_element = @element_type.substitute(substitutions)
|
|
224
306
|
return self if new_element.equal?(@element_type)
|
|
@@ -230,11 +312,66 @@ module TypeGuessr
|
|
|
230
312
|
"Array"
|
|
231
313
|
end
|
|
232
314
|
|
|
233
|
-
def
|
|
315
|
+
def type_parameter_bindings
|
|
234
316
|
{ Elem: @element_type }
|
|
235
317
|
end
|
|
236
318
|
end
|
|
237
319
|
|
|
320
|
+
# TupleType - array with per-position element types
|
|
321
|
+
# Preserves positional type information for mixed-type array literals
|
|
322
|
+
# Falls back to ArrayType when element count exceeds MAX_ELEMENTS
|
|
323
|
+
class TupleType < Type
|
|
324
|
+
attr_reader :element_types
|
|
325
|
+
|
|
326
|
+
MAX_ELEMENTS = 8
|
|
327
|
+
|
|
328
|
+
def self.new(element_types)
|
|
329
|
+
return ArrayType.new(Union.new(element_types)) if element_types.size > MAX_ELEMENTS
|
|
330
|
+
|
|
331
|
+
super
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def initialize(element_types)
|
|
335
|
+
super()
|
|
336
|
+
@element_types = element_types
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def eql?(other)
|
|
340
|
+
super && @element_types == other.element_types
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def hash
|
|
344
|
+
[self.class, @element_types].hash
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def to_s
|
|
348
|
+
"[#{@element_types.join(", ")}]"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def inspect
|
|
352
|
+
"#<TupleType:#{self}>"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def substitute(substitutions)
|
|
356
|
+
new_types = @element_types.map { |t| t.substitute(substitutions) }
|
|
357
|
+
return self if new_types.zip(@element_types).all? { |n, o| n.equal?(o) }
|
|
358
|
+
|
|
359
|
+
TupleType.new(new_types)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def rbs_class_name
|
|
363
|
+
"Array"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def type_parameter_bindings
|
|
367
|
+
return { Elem: Unknown.instance } if @element_types.empty?
|
|
368
|
+
|
|
369
|
+
unique = @element_types.uniq
|
|
370
|
+
elem = unique.size == 1 ? unique.first : Union.new(unique)
|
|
371
|
+
{ Elem: elem }
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
238
375
|
# HashType - hash with key and value types
|
|
239
376
|
class HashType < Type
|
|
240
377
|
attr_reader :key_type, :value_type
|
|
@@ -257,6 +394,10 @@ module TypeGuessr
|
|
|
257
394
|
"Hash[#{@key_type}, #{@value_type}]"
|
|
258
395
|
end
|
|
259
396
|
|
|
397
|
+
def inspect
|
|
398
|
+
"#<HashType:#{@key_type},#{@value_type}>"
|
|
399
|
+
end
|
|
400
|
+
|
|
260
401
|
def substitute(substitutions)
|
|
261
402
|
new_key = @key_type.substitute(substitutions)
|
|
262
403
|
new_value = @value_type.substitute(substitutions)
|
|
@@ -269,7 +410,7 @@ module TypeGuessr
|
|
|
269
410
|
"Hash"
|
|
270
411
|
end
|
|
271
412
|
|
|
272
|
-
def
|
|
413
|
+
def type_parameter_bindings
|
|
273
414
|
{ K: @key_type, V: @value_type }
|
|
274
415
|
end
|
|
275
416
|
end
|
|
@@ -295,6 +436,10 @@ module TypeGuessr
|
|
|
295
436
|
"Range[#{@element_type}]"
|
|
296
437
|
end
|
|
297
438
|
|
|
439
|
+
def inspect
|
|
440
|
+
"#<RangeType:#{@element_type}>"
|
|
441
|
+
end
|
|
442
|
+
|
|
298
443
|
def substitute(substitutions)
|
|
299
444
|
new_element = @element_type.substitute(substitutions)
|
|
300
445
|
return self if new_element.equal?(@element_type)
|
|
@@ -306,7 +451,7 @@ module TypeGuessr
|
|
|
306
451
|
"Range"
|
|
307
452
|
end
|
|
308
453
|
|
|
309
|
-
def
|
|
454
|
+
def type_parameter_bindings
|
|
310
455
|
{ Elem: @element_type }
|
|
311
456
|
end
|
|
312
457
|
end
|
|
@@ -315,9 +460,13 @@ module TypeGuessr
|
|
|
315
460
|
class HashShape < Type
|
|
316
461
|
attr_reader :fields
|
|
317
462
|
|
|
318
|
-
def self.new(fields, max_fields:
|
|
319
|
-
# Widen to generic
|
|
320
|
-
|
|
463
|
+
def self.new(fields, max_fields: 15)
|
|
464
|
+
# Widen to generic HashType when too many fields
|
|
465
|
+
if fields.size > max_fields
|
|
466
|
+
value_types = fields.values.uniq
|
|
467
|
+
value_type = value_types.size == 1 ? value_types.first : Union.new(value_types)
|
|
468
|
+
return HashType.new(ClassInstance.for("Symbol"), value_type)
|
|
469
|
+
end
|
|
321
470
|
|
|
322
471
|
super(fields)
|
|
323
472
|
end
|
|
@@ -342,7 +491,11 @@ module TypeGuessr
|
|
|
342
491
|
"{ #{fields_str} }"
|
|
343
492
|
end
|
|
344
493
|
|
|
345
|
-
def
|
|
494
|
+
def inspect
|
|
495
|
+
"#<HashShape:#{self}>"
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def merge_field(key, value_type, max_fields: 15)
|
|
346
499
|
new_fields = @fields.merge(key => value_type)
|
|
347
500
|
HashShape.new(new_fields, max_fields: max_fields)
|
|
348
501
|
end
|
|
@@ -358,8 +511,8 @@ module TypeGuessr
|
|
|
358
511
|
"Hash"
|
|
359
512
|
end
|
|
360
513
|
|
|
361
|
-
def
|
|
362
|
-
key_type = ClassInstance.
|
|
514
|
+
def type_parameter_bindings
|
|
515
|
+
key_type = ClassInstance.for("Symbol")
|
|
363
516
|
value_types = @fields.values.uniq
|
|
364
517
|
value_type = if value_types.empty?
|
|
365
518
|
Unknown.instance
|
|
@@ -393,6 +546,10 @@ module TypeGuessr
|
|
|
393
546
|
@name.to_s
|
|
394
547
|
end
|
|
395
548
|
|
|
549
|
+
def inspect
|
|
550
|
+
"#<TypeVariable:#{@name}>"
|
|
551
|
+
end
|
|
552
|
+
|
|
396
553
|
def substitute(substitutions)
|
|
397
554
|
substitutions[@name] || self
|
|
398
555
|
end
|
|
@@ -420,6 +577,67 @@ module TypeGuessr
|
|
|
420
577
|
"..."
|
|
421
578
|
end
|
|
422
579
|
end
|
|
580
|
+
|
|
581
|
+
# ParamSignature - structural component of MethodSignature (not a Type)
|
|
582
|
+
# Represents a single parameter with its name, kind, and inferred type
|
|
583
|
+
ParamSignature = Data.define(:name, :kind, :type) do
|
|
584
|
+
def to_s
|
|
585
|
+
type_str = type.to_s
|
|
586
|
+
case kind
|
|
587
|
+
when :required then "#{type_str} #{name}"
|
|
588
|
+
when :optional then "?#{type_str} #{name}"
|
|
589
|
+
when :rest then "*#{type_str} #{name}"
|
|
590
|
+
when :keyword_required then "#{name}: #{type_str}"
|
|
591
|
+
when :keyword_optional then "#{name}: ?#{type_str}"
|
|
592
|
+
when :keyword_rest then "**#{type_str} #{name}"
|
|
593
|
+
when :block then "&#{type_str} #{name}"
|
|
594
|
+
when :forwarding then "..."
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# MethodSignature - first-class type for Proc/Lambda/Method signatures
|
|
600
|
+
# Follows the same pattern as ArrayType: holds inner types and delegates substitute
|
|
601
|
+
class MethodSignature < Type
|
|
602
|
+
attr_reader :params, :return_type
|
|
603
|
+
|
|
604
|
+
def initialize(params, return_type)
|
|
605
|
+
super()
|
|
606
|
+
@params = params
|
|
607
|
+
@return_type = return_type
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def eql?(other)
|
|
611
|
+
super && @params == other.params && @return_type == other.return_type
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def hash
|
|
615
|
+
[self.class, @params, @return_type].hash
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def to_s
|
|
619
|
+
params_str = @params.join(", ")
|
|
620
|
+
"(#{params_str}) -> #{@return_type}"
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def inspect
|
|
624
|
+
"#<MethodSignature:#{self}>"
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def substitute(substitutions)
|
|
628
|
+
new_params = @params.map do |p|
|
|
629
|
+
ParamSignature.new(name: p.name, kind: p.kind, type: p.type.substitute(substitutions))
|
|
630
|
+
end
|
|
631
|
+
new_return_type = @return_type.substitute(substitutions)
|
|
632
|
+
return self if new_params == @params && new_return_type.equal?(@return_type)
|
|
633
|
+
|
|
634
|
+
MethodSignature.new(new_params, new_return_type)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def rbs_class_name
|
|
638
|
+
"Proc"
|
|
639
|
+
end
|
|
640
|
+
end
|
|
423
641
|
end
|
|
424
642
|
end
|
|
425
643
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Configuration
|
|
4
|
+
require_relative "core/config"
|
|
5
|
+
|
|
6
|
+
# Core type system
|
|
7
|
+
require_relative "core/types"
|
|
8
|
+
|
|
9
|
+
# Node infrastructure
|
|
10
|
+
require_relative "core/node_key_generator"
|
|
11
|
+
require_relative "core/node_context_helper"
|
|
12
|
+
require_relative "core/ir"
|
|
13
|
+
require_relative "core/index"
|
|
14
|
+
|
|
15
|
+
# Registries
|
|
16
|
+
require_relative "core/registry"
|
|
17
|
+
|
|
18
|
+
# Converters
|
|
19
|
+
require_relative "core/converter"
|
|
20
|
+
|
|
21
|
+
# Inference
|
|
22
|
+
require_relative "core/inference"
|
|
23
|
+
|
|
24
|
+
# Utilities
|
|
25
|
+
require_relative "core/signature_builder"
|
|
26
|
+
require_relative "core/type_simplifier"
|
|
27
|
+
require_relative "core/type_serializer"
|
|
28
|
+
require_relative "core/cache"
|
|
29
|
+
require_relative "core/logger"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module MCP
|
|
5
|
+
# Watches a project directory for .rb file changes using mtime polling.
|
|
6
|
+
# Detects modified, added, and deleted files and invokes a callback.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# watcher = FileWatcher.new(project_path: "/path/to/project", interval: 2) do |modified, added, removed|
|
|
10
|
+
# modified.each { |f| reindex(f) }
|
|
11
|
+
# removed.each { |f| remove(f) }
|
|
12
|
+
# end
|
|
13
|
+
# watcher.start
|
|
14
|
+
class FileWatcher
|
|
15
|
+
# @param project_path [String] Root directory to watch
|
|
16
|
+
# @param interval [Numeric] Polling interval in seconds (default: 2)
|
|
17
|
+
# @param on_change [Proc] Callback receiving (modified, added, removed) arrays
|
|
18
|
+
def initialize(project_path:, on_change:, interval: 2)
|
|
19
|
+
@project_path = project_path
|
|
20
|
+
@interval = interval
|
|
21
|
+
@on_change = on_change
|
|
22
|
+
@thread = nil
|
|
23
|
+
@running = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
@running = true
|
|
28
|
+
@snapshot = take_snapshot
|
|
29
|
+
@thread = Thread.new { poll_loop }
|
|
30
|
+
@thread.abort_on_exception = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop
|
|
34
|
+
@running = false
|
|
35
|
+
@thread&.join(5)
|
|
36
|
+
@thread = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def running?
|
|
40
|
+
@running && @thread&.alive?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private def poll_loop
|
|
44
|
+
while @running
|
|
45
|
+
sleep(@interval)
|
|
46
|
+
check_changes
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private def check_changes
|
|
51
|
+
current = take_snapshot
|
|
52
|
+
previous = @snapshot
|
|
53
|
+
|
|
54
|
+
modified = []
|
|
55
|
+
added = []
|
|
56
|
+
removed = []
|
|
57
|
+
|
|
58
|
+
current.each do |path, mtime|
|
|
59
|
+
if previous.key?(path)
|
|
60
|
+
modified << path if mtime > previous[path]
|
|
61
|
+
else
|
|
62
|
+
added << path
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
previous.each_key do |path|
|
|
67
|
+
removed << path unless current.key?(path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@snapshot = current
|
|
71
|
+
|
|
72
|
+
return if modified.empty? && added.empty? && removed.empty?
|
|
73
|
+
|
|
74
|
+
@on_change.call(modified, added, removed)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private def take_snapshot
|
|
78
|
+
pattern = File.join(@project_path, "**", "*.rb")
|
|
79
|
+
Dir.glob(pattern).each_with_object({}) do |path, hash|
|
|
80
|
+
hash[path] = File.mtime(path)
|
|
81
|
+
rescue Errno::ENOENT
|
|
82
|
+
# File deleted between glob and mtime - skip
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|