Skip to content

Commit 3be75f5

Browse files
committed
Change InstanceType structure
Actual object type was represented by InstanceType.new(object.singleton_class). This will unnecessarily creates many singleton classes. Actual array type was represented by InstanceType.new(Array, Elem: ElementType). Information of singleton methods will be lost. Instead of using singleton_class, InstanceType will retain actual class and actual instances. Params will be expanded lazily.
1 parent 5524524 commit 3be75f5

6 files changed

Lines changed: 154 additions & 64 deletions

File tree

lib/repl_type_completor/methods.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
module ReplTypeCompletor
44
module Methods
5-
OBJECT_SINGLETON_CLASS_METHOD = Object.instance_method(:singleton_class)
5+
OBJECT_SINGLETON_METHODS_METHOD = Object.instance_method(:singleton_methods)
6+
OBJECT_PRIVATE_METHODS_METHOD = Object.instance_method(:private_methods)
67
OBJECT_INSTANCE_VARIABLES_METHOD = Object.instance_method(:instance_variables)
78
OBJECT_INSTANCE_VARIABLE_GET_METHOD = Object.instance_method(:instance_variable_get)
89
OBJECT_CLASS_METHOD = Object.instance_method(:class)

lib/repl_type_completor/scope.rb

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -284,36 +284,19 @@ def table_instance_variables
284284
end
285285

286286
def instance_variables
287-
self_singleton_types = self_type.types.grep(Types::SingletonType)
288-
singleton_classes = self_type.types.grep(Types::InstanceType).map(&:klass).select(&:singleton_class?)
289-
base_self = base_scope.self_object
290-
self_instance_variables = singleton_classes.flat_map do |singleton_class|
291-
if singleton_class.respond_to? :attached_object
292-
Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(singleton_class.attached_object).map(&:to_s)
293-
elsif singleton_class == Methods::OBJECT_SINGLETON_CLASS_METHOD.bind_call(base_self)
294-
Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(base_self).map(&:to_s)
295-
else
296-
[]
297-
end
298-
end
287+
modules = self_type.types.grep(Types::SingletonType).map(&:module_or_class)
288+
instances = self_type.types.grep(Types::InstanceType).filter_map(&:instances).flatten(1)
289+
self_objects = modules + instances
299290
[
300-
self_singleton_types.flat_map { _1.module_or_class.instance_variables.map(&:to_s) },
301-
self_instance_variables || [],
291+
self_objects.flat_map { Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(_1).map(&:to_s) },
302292
table_instance_variables
303-
].inject(:|)
293+
].inject([], :|)
304294
end
305295

306296
def self_instance_variable_get(name)
307-
self_objects = self_type.types.grep(Types::SingletonType).map(&:module_or_class)
308-
singleton_classes = self_type.types.grep(Types::InstanceType).map(&:klass).select(&:singleton_class?)
309-
base_self = base_scope.self_object
310-
singleton_classes.each do |singleton_class|
311-
if singleton_class.respond_to? :attached_object
312-
self_objects << singleton_class.attached_object
313-
elsif singleton_class == base_self.singleton_class
314-
self_objects << base_self
315-
end
316-
end
297+
modules = self_type.types.grep(Types::SingletonType).map(&:module_or_class)
298+
instances = self_type.types.grep(Types::InstanceType).filter_map(&:instances).flatten(1)
299+
self_objects = modules + instances
317300
types = self_objects.map do |object|
318301
value = begin
319302
Methods::OBJECT_INSTANCE_VARIABLE_GET_METHOD.bind_call(object, name)

lib/repl_type_completor/types.rb

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,20 @@ def self.intersect?(a, b)
177177
def self.type_from_object(object)
178178
case object
179179
when Array
180-
InstanceType.new Array, { Elem: union_type_from_objects(object) }
180+
InstanceType.new Array, nil, [object]
181181
when Hash
182-
InstanceType.new Hash, { K: union_type_from_objects(object.keys), V: union_type_from_objects(object.values) }
182+
InstanceType.new Hash, nil, [object]
183183
when Module
184184
SingletonType.new object
185185
else
186-
klass = Methods::OBJECT_SINGLETON_CLASS_METHOD.bind_call(object) rescue Methods::OBJECT_CLASS_METHOD.bind_call(object)
187-
InstanceType.new klass
186+
InstanceType.new Methods::OBJECT_CLASS_METHOD.bind_call(object), nil, [object]
188187
end
189188
end
190189

191190
def self.union_type_from_objects(objects)
192-
values = objects.size <= OBJECT_TO_TYPE_SAMPLE_SIZE ? objects : objects.sample(OBJECT_TO_TYPE_SAMPLE_SIZE)
193-
klasses = values.map { Methods::OBJECT_CLASS_METHOD.bind_call(_1) }
194-
UnionType[*klasses.uniq.map { InstanceType.new _1 }]
191+
instanes = objects.size <= OBJECT_TO_TYPE_SAMPLE_SIZE ? objects : objects.sample(OBJECT_TO_TYPE_SAMPLE_SIZE)
192+
class_instanes = instanes.group_by { Methods::OBJECT_CLASS_METHOD.bind_call(_1) }
193+
UnionType[*class_instanes.map { InstanceType.new _1, nil, _2 }]
195194
end
196195

197196
class SingletonType
@@ -212,25 +211,67 @@ def inspect
212211
end
213212

214213
class InstanceType
215-
attr_reader :klass, :params
216-
def initialize(klass, params = {})
214+
attr_reader :klass, :raw_params, :instances
215+
def initialize(klass, params = nil, instances = nil)
217216
@klass = klass
218-
@params = params
217+
@raw_params = params if params && !params.empty?
218+
@instances = instances if instances && !instances.empty?
219219
end
220+
220221
def transform() = yield(self)
221-
def methods() = rbs_methods.select { _2.public? }.keys | @klass.instance_methods
222-
def all_methods() = rbs_methods.keys | @klass.instance_methods | @klass.private_instance_methods
222+
def methods() = rbs_methods.select { _2.public? }.keys | @klass.instance_methods | singleton_methods
223+
def all_methods() = rbs_methods.keys | @klass.instance_methods | @klass.private_instance_methods | singleton_methods | instances_private_methods
224+
225+
def singleton_methods
226+
return [] unless @instances
227+
@singleton_methods ||= @instances.map do |instance|
228+
Methods::OBJECT_SINGLETON_METHODS_METHOD.bind_call(instance)
229+
end.inject(:|)
230+
end
231+
232+
def instances_private_methods
233+
return [] unless @instances
234+
@private_instances_methods ||= @instances.map do |instance|
235+
Methods::OBJECT_PRIVATE_METHODS_METHOD.bind_call(instance, false)
236+
end.inject(:|)
237+
end
238+
239+
def params
240+
@params ||= expand_params
241+
end
242+
243+
def expand_params
244+
params = @raw_params || {}
245+
return params unless @instances
246+
247+
if @klass == Array
248+
type = Types.union_type_from_objects(@instances.flatten(1))
249+
{ Elem: UnionType[*params[:Elem], *type] }
250+
elsif @klass == Hash
251+
key = Types.union_type_from_objects(@instances.map(&:keys).flatten(1))
252+
value = Types.union_type_from_objects(@instances.map(&:values).flatten(1))
253+
{
254+
K: UnionType[*params[:K], key],
255+
V: UnionType[*params[:V], value]
256+
}
257+
else
258+
params
259+
end
260+
end
261+
223262
def constants() = []
224263
def types() = [self]
225264
def nillable?() = (@klass == NilClass)
226265
def nonnillable() = self
266+
227267
def rbs_methods
228268
return {} unless Types.rbs_builder
229269

230270
name = Types.class_name_of(@klass)
231271
type_name = Types.rbs_absolute_type_name(name)
232272
Types.rbs_builder.build_instance(type_name).methods rescue {}
233273
end
274+
234275
def inspect
235276
if params.empty?
236277
inspect_without_params
@@ -239,6 +280,7 @@ def inspect
239280
"#{inspect_without_params}#{params_string}"
240281
end
241282
end
283+
242284
def inspect_without_params
243285
if klass == NilClass
244286
'nil'
@@ -247,7 +289,7 @@ def inspect_without_params
247289
elsif klass == FalseClass
248290
'false'
249291
else
250-
klass.singleton_class? ? klass.superclass.to_s : klass.to_s
292+
klass.to_s
251293
end
252294
end
253295
end
@@ -275,24 +317,26 @@ class UnionType
275317

276318
def initialize(*types)
277319
@types = []
278-
singletons = []
279-
instances = {}
320+
singleton_types = []
321+
instance_types = {}
280322
collect = -> type do
281323
case type
282324
in UnionType
283325
type.types.each(&collect)
284326
in InstanceType
285-
params = (instances[type.klass] ||= {})
286-
type.params.each do |k, v|
327+
params, instances = (instance_types[type.klass] ||= [{}, []])
328+
type.instances&.each { instances << _1 }
329+
type.raw_params&.each do |k, v|
287330
(params[k] ||= []) << v
288331
end
289332
in SingletonType
290-
singletons << type
333+
singleton_types << type
291334
end
292335
end
293336
types.each(&collect)
294-
@types = singletons.uniq + instances.map do |klass, params|
295-
InstanceType.new(klass, params.transform_values { |v| UnionType[*v] })
337+
@types = singleton_types.uniq + instance_types.map do |klass, (params, instances)|
338+
params = params.transform_values { |v| UnionType[*v] }
339+
InstanceType.new(klass, params, instances)
296340
end
297341
end
298342

@@ -322,7 +366,7 @@ def self.[](*types)
322366
def methods() = @types.flat_map(&:methods).uniq
323367
def all_methods() = @types.flat_map(&:all_methods).uniq
324368
def constants() = @types.flat_map(&:constants).uniq
325-
def inspect() = @types.map(&:inspect).join(' | ')
369+
def inspect() = @types.map(&:inspect).sort.join(' | ')
326370
end
327371

328372
BOOLEAN = UnionType[TRUE, FALSE]

test/repl_type_completor/test_repl_type_completor.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,26 @@ def test_anonymous_module
197197
assert_doc_namespace('m.ancestors', 'Module.ancestors', binding: bind)
198198
end
199199

200+
def test_array_singleton_method
201+
assert_completion('$LOAD_PATH.', include: 'resolve_feature_path')
202+
assert_completion('$LOAD_PATH.itself.', include: 'resolve_feature_path')
203+
assert_completion('[$LOAD_PATH].first.', include: 'resolve_feature_path')
204+
assert_completion('{ x: $LOAD_PATH }.each_value { _1.', include: 'resolve_feature_path')
205+
end
206+
207+
def test_recursive_array
208+
a = Object.new
209+
b = Object.new
210+
def a.foobar; end
211+
def b.foobaz; end
212+
arr = [a, [b]]
213+
arr[1] << arr
214+
bind = binding
215+
assert_completion('arr.sample.foo', binding: bind, include: 'bar', exclude: 'baz')
216+
assert_completion('arr.sample.sample.foo', binding: bind, include: 'baz', exclude: 'bar')
217+
assert_completion('arr.sample.sample.sample.foo', binding: bind, include: 'bar', exclude: 'baz')
218+
end
219+
200220
DEPRECATED_CONST = 1
201221
deprecate_constant :DEPRECATED_CONST
202222
def test_deprecated_const_without_warning

test/repl_type_completor/test_type_analyze.rb

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,40 @@ def assert_analyze_type(code, type, token = nil, binding: empty_binding)
3333
assert_equal token, result_token if token
3434
end
3535

36+
ActualInstance = Struct.new(:instance) do
37+
def to_s() = instance.inspect
38+
undef to_a # to suppress [*obj] being splat to array
39+
end
40+
41+
def instance(instance)
42+
ActualInstance.new(instance)
43+
end
44+
3645
def assert_call(code, include: nil, exclude: nil, binding: nil)
3746
raise ArgumentError if include.nil? && exclude.nil?
3847

3948
result = analyze(code.strip, binding: binding)
4049
type = result[2] if result[0] == :call
41-
klasses = type.types.flat_map do
42-
_1.klass.singleton_class? ? [_1.klass.superclass, _1.klass] : _1.klass
50+
instance_types = type.types.grep(ReplTypeCompletor::Types::InstanceType)
51+
singleton_types = type.types.grep(ReplTypeCompletor::Types::SingletonType)
52+
instances = {}.compare_by_identity
53+
singleton_types.each { instances[_1.class_or_module] = true }
54+
instance_types.each do |t|
55+
t.instances&.each { instances[_1] = true }
4356
end
44-
assert ([*include] - klasses).empty?, "Expected #{klasses} to include #{include}" if include
45-
assert (klasses & [*exclude]).empty?, "Expected #{klasses} not to include #{exclude}" if exclude
57+
classes = instance_types.to_h { [_1.klass, true] }
58+
59+
match = ->(query) do
60+
case query
61+
in ActualInstance
62+
instances.key?(query.instance)
63+
in Class
64+
classes.key?(query)
65+
end
66+
end
67+
68+
assert [*include].all?(&match), "Expected #{classes.keys} #{instances.keys} to include #{include}"
69+
refute [*exclude].any?(&match), "Expected #{classes.keys} #{instances.keys} not to include #{exclude}" if exclude
4670
end
4771

4872
def test_lvar_ivar_gvar_cvar
@@ -89,8 +113,8 @@ class (Module.new)::A
89113
binding
90114
end
91115
RUBY
92-
assert_call('STDIN.', include: STDIN.singleton_class)
93-
assert_call('$stdin.', include: $stdin.singleton_class)
116+
assert_call('STDIN.', include: instance(STDIN))
117+
assert_call('$stdin.', include: instance($stdin))
94118
assert_call('@ivar.', include: Symbol, binding: bind)
95119
assert_call('@@cvar.', include: String, binding: bind)
96120
lbind = eval('lvar = 1; binding')
@@ -127,15 +151,15 @@ def test_module_cvar_ref
127151

128152
def test_lvar_singleton_method
129153
a = 1
130-
b = +''
154+
b = ''
131155
c = Object.new
132156
d = [a, b, c]
133157
binding = Kernel.binding
134158
assert_call('a.', include: Integer, exclude: String, binding: binding)
135-
assert_call('b.', include: b.singleton_class, exclude: [Integer, Object], binding: binding)
136-
assert_call('c.', include: c.singleton_class, exclude: [Integer, String], binding: binding)
159+
assert_call('b.', include: instance(b), exclude: [Integer, Object], binding: binding)
160+
assert_call('c.', include: instance(c), exclude: [Integer, String], binding: binding)
137161
assert_call('d.', include: d.class, exclude: [Integer, String, Object], binding: binding)
138-
assert_call('d.sample.', include: [Integer, String, Object], exclude: [b.singleton_class, c.singleton_class], binding: binding)
162+
assert_call('d.sample.', include: [Integer, instance(b), instance(c)], binding: binding)
139163
end
140164

141165
def test_local_variable_assign
@@ -401,11 +425,11 @@ def test_self
401425
assert_call('self.', include: [Integer], binding: integer_binding)
402426
string = +''
403427
string_binding = string.instance_eval { Kernel.binding }
404-
assert_call('self.', include: [string.singleton_class], binding: string_binding)
428+
assert_call('self.', include: instance(string), binding: string_binding)
405429
object = Object.new
406430
object.instance_eval { @int = 1; @string = string }
407431
object_binding = object.instance_eval { Kernel.binding }
408-
assert_call('self.', include: [object.singleton_class], binding: object_binding)
432+
assert_call('self.', include: instance(object), binding: object_binding)
409433
assert_call('@int.', include: [Integer], binding: object_binding)
410434
assert_call('@string.', include: [String], binding: object_binding)
411435
end

test/repl_type_completor/test_types.rb

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ def test_type_inspect
1717
assert_equal 'false', false_type.inspect
1818
assert_equal 'String', string_type.inspect
1919
assert_equal 'Array', ReplTypeCompletor::Types::InstanceType.new(Array).inspect
20-
assert_equal 'true | false', true_or_false.inspect
21-
assert_equal 'Array[Elem: true | false]', array_type.inspect
20+
assert_equal 'false | true', true_or_false.inspect
21+
assert_equal 'Array[Elem: false | true]', array_type.inspect
2222
assert_equal 'Array', array_type.inspect_without_params
2323
assert_equal 'Proc', ReplTypeCompletor::Types::PROC.inspect
2424
assert_equal 'Array.itself', ReplTypeCompletor::Types::SingletonType.new(Array).inspect
@@ -40,9 +40,11 @@ def bo.hash; 42; end # Needed to use this object as a hash key
4040
bo_value_hash_type = ReplTypeCompletor::Types.type_from_object({ x: bo })
4141

4242
assert_equal Integer, int_type.klass
43-
# Use singleton_class to autocomplete singleton methods
44-
assert_equal obj.singleton_class, obj_type.klass
45-
assert_equal Object.instance_method(:singleton_class).bind_call(bo), bo_type.klass
43+
# Type contains actual instances to autocomplete singleton methods
44+
assert_equal Object, obj_type.klass
45+
assert_equal [obj], obj_type.instances
46+
assert_equal BasicObject, bo_type.klass
47+
assert_equal [bo], bo_type.instances
4648
# Array and Hash are special
4749
assert_equal Array, arr_type.klass
4850
assert_equal Array, bo_arr_type.klass
@@ -96,5 +98,21 @@ def bo.foobar; end
9698
type = ReplTypeCompletor::Types.type_from_object bo
9799
assert type.all_methods.include?(:foobar)
98100
end
101+
102+
def test_params_lazily_expanded_on_recursive_type
103+
deepest = [{ 1 => 2.0 }]
104+
a = deepest
105+
5.times { a = ['even', [:odd, a]] }
106+
deepest << a
107+
type = ReplTypeCompletor::Types.type_from_object a
108+
assert_equal Array, type.klass
109+
10.times do |i|
110+
elem_type = type.params[:Elem]
111+
expected = i.even? ? [Array, String] : [Array, Symbol]
112+
assert_equal expected, elem_type.types.map(&:klass).sort_by(&:name)
113+
type = elem_type.types.find { _1.klass == Array }
114+
end
115+
assert_equal 'Hash[K: Integer, V: Float]', type.params[:Elem].types.find { _1.klass == Hash }.inspect
116+
end
99117
end
100118
end

0 commit comments

Comments
 (0)