Skip to content

Commit 92d9144

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 92d9144

4 files changed

Lines changed: 77 additions & 3 deletions

File tree

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: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ 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+
ivar_name = :"@#{method_name.to_s.gsub(/[!?]\z/, '')}"
110+
instances = []
111+
type.types.each do |t|
112+
case t
113+
in SingletonType
114+
instances << t.module_or_class
115+
in InstanceType
116+
instances.concat(t.instances) if t.instances
117+
end
118+
end
119+
instances = instances.sample(OBJECT_TO_TYPE_SAMPLE_SIZE) if instances.size > OBJECT_TO_TYPE_SAMPLE_SIZE
120+
objects = []
121+
instances.map do |instance|
122+
objects << instance.instance_variable_get(ivar_name) if instance.instance_variable_defined?(ivar_name)
123+
end
124+
union_type_from_objects(objects) if objects
125+
end
126+
108127
def self.rbs_methods(type, method_name, args_types, kwargs_type, has_block)
109128
return [] unless rbs_builder
110129

@@ -188,9 +207,10 @@ def self.type_from_object(object)
188207
end
189208

190209
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 }]
210+
instances = objects.size <= OBJECT_TO_TYPE_SAMPLE_SIZE ? objects : objects.sample(OBJECT_TO_TYPE_SAMPLE_SIZE)
211+
modules, instances = instances.partition { Module === _1 }
212+
class_instances = instances.group_by { Methods::OBJECT_CLASS_METHOD.bind_call(_1) }
213+
UnionType[*class_instances.map { InstanceType.new _1, nil, _2 }, *modules.uniq.map { SingletonType.new _1 }]
194214
end
195215

196216
class SingletonType

test/repl_type_completor/test_type_analyze.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,53 @@ 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 d.bar!; end
300+
def d.baz?; end
301+
def bo.foo; @foo ||= {}; end
302+
bo.foo
303+
a.instance_variable_set(:@foo, 1)
304+
b.instance_variable_set(:@foo, 'a')
305+
c.instance_variable_set(:@foo, nil)
306+
d.instance_variable_set(:@bar, :a)
307+
d.instance_variable_set(:@baz, [])
308+
d.instance_variable_set(:@blah, 1)
309+
assert_call('a.foo.', include: Integer, binding:)
310+
assert_call('b.foo.', include: String, binding:)
311+
assert_call('bo.foo.', include: Hash, binding:)
312+
assert_call('[a, b, bo].sample.foo.', include: [Integer, String, Hash], binding:)
313+
assert_call('[a, b, c].sample.foo.', include: [Integer, String, NilClass], binding:)
314+
assert_call('[a, b, d].sample.foo.', include: [Integer, String], exclude: NilClass, binding:)
315+
assert_call('d.bar!.', include: Symbol, binding:)
316+
assert_call('d.baz?.', include: Array, binding:)
317+
# Even if method does not exist, it might return @blah in method_missing
318+
assert_call('d.blah.', include: Integer, binding:)
319+
end
320+
321+
def test_accessor_method_with_class_module
322+
AnalyzeTest.instance_variable_set(:@foo, 1)
323+
AnalyzeTest.instance_variable_set(:@bar, Symbol)
324+
o = Object.new
325+
o.instance_variable_set(:@foo, String)
326+
o.instance_variable_set(:@bar, Math)
327+
assert_call('AnalyzeTest.foo.', include: Integer, binding:)
328+
assert_call('AnalyzeTest.bar.all_symbols.', include: Array, binding:)
329+
assert_call('o.foo.new.', include: String, binding:)
330+
assert_call('o.bar.sin(1).', include: Float, binding:)
331+
ensure
332+
AnalyzeTest.remove_instance_variable(:@foo)
333+
AnalyzeTest.remove_instance_variable(:@bar)
334+
end
335+
289336
def test_interface_match_var
290337
assert_call('([1]+[:a]+["a"]).sample.', include: [Integer, String, Symbol])
291338
end

0 commit comments

Comments
 (0)