Skip to content

Commit 2f9c861

Browse files
committed
Completion for untyped accessor methods that has corresponding instance variables
Completes `untyped.foo.` if untyped has an instance variable `@foo`
1 parent aaf65d4 commit 2f9c861

File tree

4 files changed

+75
-3
lines changed

4 files changed

+75
-3
lines changed

lib/repl_type_completor/methods.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module Methods
55
OBJECT_SINGLETON_METHODS_METHOD = Object.instance_method(:singleton_methods)
66
OBJECT_PRIVATE_METHODS_METHOD = Object.instance_method(:private_methods)
77
OBJECT_INSTANCE_VARIABLES_METHOD = Object.instance_method(:instance_variables)
8+
OBJECT_INSTANCE_VARIABLE_DEFINED_METHOD = Object.instance_method(:instance_variable_defined?)
89
OBJECT_INSTANCE_VARIABLE_GET_METHOD = Object.instance_method(:instance_variable_get)
910
OBJECT_CLASS_METHOD = Object.instance_method(:class)
1011
MODULE_NAME_METHOD = Module.instance_method(:name)

lib/repl_type_completor/type_analyzer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,12 @@ def method_call(receiver, method_name, args, kwargs, block, scope, name_match: t
11791179
end
11801180
end
11811181
end
1182+
1183+
if types.empty? && args.empty? && !kwargs && !block
1184+
t = Types.accessor_method_return_type(receiver, method_name)
1185+
types << t if t
1186+
end
1187+
11821188
scope&.terminate if terminates && breaks.empty?
11831189
Types::UnionType[*types, *breaks]
11841190
end

lib/repl_type_completor/types.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,28 @@ def self.method_return_type(type, method_name)
105105
UnionType[*types]
106106
end
107107

108+
def self.accessor_method_return_type(type, method_name)
109+
return unless method_name.match?(/\A[a-z_][a-z_0-9]*\z/)
110+
111+
ivar_name = :"@#{method_name}"
112+
instances = type.types.filter_map do |t|
113+
case t
114+
in SingletonType
115+
t.module_or_class
116+
in InstanceType
117+
t.instances
118+
end
119+
end.flatten
120+
instances = instances.sample(OBJECT_TO_TYPE_SAMPLE_SIZE) if instances.size > OBJECT_TO_TYPE_SAMPLE_SIZE
121+
objects = []
122+
instances.each do |instance|
123+
if Methods::OBJECT_INSTANCE_VARIABLE_DEFINED_METHOD.bind_call(instance, ivar_name)
124+
objects << Methods::OBJECT_INSTANCE_VARIABLE_GET_METHOD.bind_call(instance, ivar_name)
125+
end
126+
end
127+
union_type_from_objects(objects) unless objects.empty?
128+
end
129+
108130
def self.rbs_methods(type, method_name, args_types, kwargs_type, has_block)
109131
return [] unless rbs_builder
110132

@@ -188,9 +210,10 @@ def self.type_from_object(object)
188210
end
189211

190212
def self.union_type_from_objects(objects)
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 }]
213+
instances = objects.size <= OBJECT_TO_TYPE_SAMPLE_SIZE ? objects : objects.sample(OBJECT_TO_TYPE_SAMPLE_SIZE)
214+
modules, instances = instances.partition { Module === _1 }
215+
class_instances = instances.group_by { Methods::OBJECT_CLASS_METHOD.bind_call(_1) }
216+
UnionType[*class_instances.map { InstanceType.new _1, nil, _2 }, *modules.uniq.map { SingletonType.new _1 }]
194217
end
195218

196219
class SingletonType

test/repl_type_completor/test_type_analyze.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,48 @@ def test_method_select
286286
assert_call('2.times{}.', include: Integer, exclude: Enumerator)
287287
end
288288

289+
def test_accessor_method
290+
a = Object.new
291+
b = Object.new
292+
c = Object.new
293+
d = Object.new
294+
bo = Object.new
295+
def a.foo; end
296+
def b.foo; end
297+
def c.foo; end
298+
def d.foo; end
299+
def bo.foo; @foo ||= {}; end
300+
bo.foo
301+
a.instance_variable_set(:@foo, 1)
302+
b.instance_variable_set(:@foo, 'a')
303+
c.instance_variable_set(:@foo, nil)
304+
d.instance_variable_set(:@bar, :a)
305+
assert_call('a.foo.', include: Integer, binding: binding)
306+
assert_call('b.foo.', include: String, binding: binding)
307+
assert_call('c.foo.', include: NilClass, binding: binding)
308+
assert_call('bo.foo.', include: Hash, binding: binding)
309+
assert_call('[a, b, bo].sample.foo.', include: [Integer, String, Hash], binding: binding)
310+
assert_call('[a, b, c].sample.foo.', include: [Integer, String, NilClass], binding: binding)
311+
assert_call('[a, b, d].sample.foo.', include: [Integer, String], exclude: NilClass, binding: binding)
312+
# Not sure if this is a good idea, but method_missing(:bar) might return @bar in this case.
313+
assert_call('d.bar.', include: Symbol, binding: binding)
314+
end
315+
316+
def test_accessor_method_with_class_module
317+
AnalyzeTest.instance_variable_set(:@foo, 1)
318+
AnalyzeTest.instance_variable_set(:@bar, Symbol)
319+
o = Object.new
320+
o.instance_variable_set(:@foo, String)
321+
o.instance_variable_set(:@bar, Math)
322+
assert_call('AnalyzeTest.foo.', include: Integer, binding: binding)
323+
assert_call('AnalyzeTest.bar.all_symbols.', include: Array, binding: binding)
324+
assert_call('o.foo.new.', include: String, binding: binding)
325+
assert_call('o.bar.sin(1).', include: Float, binding: binding)
326+
ensure
327+
AnalyzeTest.remove_instance_variable(:@foo)
328+
AnalyzeTest.remove_instance_variable(:@bar)
329+
end
330+
289331
def test_interface_match_var
290332
assert_call('([1]+[:a]+["a"]).sample.', include: [Integer, String, Symbol])
291333
end

0 commit comments

Comments
 (0)