Skip to content

Commit f16166b

Browse files
authored
Merge pull request #79 from ruby/interface_type
Resolve rbs type aliases and interfaces from their definitions
2 parents 4bfb5c6 + 729cab9 commit f16166b

4 files changed

Lines changed: 192 additions & 30 deletions

File tree

lib/repl_type_completor/type_analyzer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,7 @@ def method_call(receiver, method_name, args, kwargs, block, scope, name_match: t
11521152
methods = Types.rbs_methods receiver, method_name.to_sym, args, kwargs, !!block
11531153
block_called = false
11541154
type_breaks = methods.map do |method, given_params, method_params|
1155-
receiver_vars = receiver.is_a?(Types::InstanceType) ? receiver.named_params : {}
1155+
receiver_vars = receiver.is_a?(Types::InstanceType) || receiver.is_a?(Types::InterfaceType) ? receiver.named_params : {}
11561156
free_vars = method.type.free_variables - receiver_vars.keys.to_set
11571157
vars = receiver_vars.merge Types.match_free_variables(free_vars, method_params, given_params)
11581158
if block && method.block && method.block.type.respond_to?(:required_positionals)

lib/repl_type_completor/types.rb

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
module ReplTypeCompletor
99
module Types
1010
OBJECT_TO_TYPE_SAMPLE_SIZE = 50
11+
EXPANSION_NESTING_LIMIT = 3
1112

1213
singleton_class.attr_reader :rbs_builder, :rbs_load_error
1314

@@ -75,6 +76,11 @@ def self.rbs_absolute_type_name(name)
7576
def self.rbs_search_method(klass, method_name, singleton)
7677
return unless rbs_builder
7778

79+
if klass.is_a? InterfaceType
80+
definition = klass.definition
81+
return definition && definition.methods[method_name]
82+
end
83+
7884
klass.ancestors.each do |ancestor|
7985
next unless (name = Methods::MODULE_NAME_METHOD.bind_call(ancestor))
8086

@@ -93,6 +99,8 @@ def self.method_return_type(type, method_name)
9399
[t, t.module_or_class, true]
94100
in InstanceType
95101
[t, t.klass, false]
102+
in InterfaceType
103+
[t, t, false]
96104
end
97105
end
98106
types = receivers.flat_map do |receiver_type, klass, singleton|
@@ -115,6 +123,8 @@ def self.accessor_method_return_type(type, method_name)
115123
t.module_or_class
116124
in InstanceType
117125
t.instances
126+
in InterfaceType
127+
nil
118128
end
119129
end.flatten
120130
instances = instances.sample(OBJECT_TO_TYPE_SAMPLE_SIZE) if instances.size > OBJECT_TO_TYPE_SAMPLE_SIZE
@@ -136,6 +146,8 @@ def self.rbs_methods(type, method_name, args_types, kwargs_type, has_block)
136146
[t, t.module_or_class, true]
137147
in InstanceType
138148
[t, t.klass, false]
149+
in InterfaceType
150+
[t, t, false]
139151
end
140152
end
141153
has_splat = args_types.include?(nil)
@@ -193,7 +205,14 @@ def self.intersect?(a, b)
193205
end
194206

195207
aa, bb = [atypes, btypes].map {|types| (types[InstanceType] || []).map(&:klass) }
196-
(aa.flat_map(&:ancestors) & bb).any?
208+
return true if (aa.flat_map(&:ancestors) & bb).any?
209+
210+
[[atypes[InterfaceType], b], [btypes[InterfaceType], a]].any? do |interfaces, other|
211+
interfaces&.any? do |interface|
212+
methods = interface.methods
213+
!methods.empty? && other.types.any? { |t| (methods - t.methods).empty? }
214+
end
215+
end
197216
end
198217

199218
def self.type_from_object(object)
@@ -340,6 +359,38 @@ def inspect_without_params
340359
end
341360
end
342361

362+
class InterfaceType
363+
attr_reader :name, :args
364+
365+
def initialize(name, args = [])
366+
@name = name
367+
@args = args
368+
end
369+
370+
def definition
371+
return @definition if defined?(@definition)
372+
373+
@definition = (Types.rbs_builder&.build_interface(@name) rescue nil)
374+
end
375+
376+
def transform() = yield(self)
377+
def methods() = definition ? definition.methods.keys : []
378+
def all_methods() = methods
379+
380+
def named_params
381+
definition ? definition.type_params.zip(@args).to_h.compact : {}
382+
end
383+
384+
def constants() = []
385+
def types() = [self]
386+
def nillable?() = false
387+
def nonnillable() = self
388+
389+
def inspect
390+
args.empty? ? name.name.to_s : "#{name.name}[#{args.map(&:inspect).join(', ')}]"
391+
end
392+
end
393+
343394
NIL = InstanceType.new NilClass
344395
OBJECT = InstanceType.new Object
345396
TRUE = InstanceType.new TrueClass
@@ -364,6 +415,7 @@ class UnionType
364415
def initialize(*types)
365416
@types = []
366417
singleton_types = []
418+
interface_types = {}
367419
instance_types = {}
368420
collect = -> type do
369421
case type
@@ -377,10 +429,12 @@ def initialize(*types)
377429
end
378430
in SingletonType
379431
singleton_types << type
432+
in InterfaceType
433+
interface_types[[type.name, type.args]] ||= type
380434
end
381435
end
382436
types.each(&collect)
383-
@types = singleton_types.uniq + instance_types.map do |klass, (params, instances)|
437+
@types = singleton_types.uniq + interface_types.values + instance_types.map do |klass, (params, instances)|
384438
params = params.map { |v| UnionType[*v] }
385439
InstanceType.new(klass, params, instances)
386440
end
@@ -422,7 +476,15 @@ def self.array_of(*types)
422476
InstanceType.new(Array, [type])
423477
end
424478

425-
def self.from_rbs_type(return_type, self_type, extra_vars = {})
479+
# Alias may validly recurse through type args (`type json = Integer | Array[json]`)
480+
# and can expand exponentially (`type b = a | [a]` chains), so expansion is depth limited.
481+
def self.expand_alias_type(rbs_type, nesting)
482+
return if nesting >= EXPANSION_NESTING_LIMIT
483+
484+
rbs_builder.expand_alias2 rbs_type.name, rbs_type.args rescue nil
485+
end
486+
487+
def self.from_rbs_type(return_type, self_type, extra_vars = {}, nesting = 0)
426488
case return_type
427489
when RBS::Types::Bases::Self
428490
self_type
@@ -434,12 +496,13 @@ def self.from_rbs_type(return_type, self_type, extra_vars = {})
434496
self_type.transform do |type|
435497
case type
436498
in SingletonType
437-
InstanceType.new(self_type.module_or_class.is_a?(Class) ? Class : Module)
499+
InstanceType.new(type.module_or_class.is_a?(Class) ? Class : Module)
438500
in InstanceType
439501
SingletonType.new type.klass
502+
in InterfaceType
503+
CLASS
440504
end
441505
end
442-
UnionType[*types]
443506
when RBS::Types::Bases::Bool
444507
BOOLEAN
445508
when RBS::Types::Bases::Instance
@@ -454,11 +517,11 @@ def self.from_rbs_type(return_type, self_type, extra_vars = {})
454517
end
455518
when RBS::Types::Union, RBS::Types::Intersection
456519
# Intersection is unsupported. fallback to union type
457-
UnionType[*return_type.types.map { from_rbs_type _1, self_type, extra_vars }]
520+
UnionType[*return_type.types.map { from_rbs_type _1, self_type, extra_vars, nesting }]
458521
when RBS::Types::Proc
459522
PROC
460523
when RBS::Types::Tuple
461-
elem = UnionType[*return_type.types.map { from_rbs_type _1, self_type, extra_vars }]
524+
elem = UnionType[*return_type.types.map { from_rbs_type _1, self_type, extra_vars, nesting }]
462525
InstanceType.new(Array, [elem])
463526
when RBS::Types::Record
464527
InstanceType.new(Hash, [SYMBOL, OBJECT])
@@ -467,37 +530,28 @@ def self.from_rbs_type(return_type, self_type, extra_vars = {})
467530
when RBS::Types::Variable
468531
if extra_vars.key? return_type.name
469532
extra_vars[return_type.name]
470-
elsif self_type.is_a? InstanceType
533+
elsif self_type.is_a?(InstanceType) || self_type.is_a?(InterfaceType)
471534
self_type.named_params[return_type.name] || OBJECT
472535
elsif self_type.is_a? UnionType
473536
types = self_type.types.filter_map do |t|
474-
t.named_params[return_type.name] if t.is_a? InstanceType
537+
t.named_params[return_type.name] if t.is_a?(InstanceType) || t.is_a?(InterfaceType)
475538
end
476539
UnionType[*types]
477540
else
478541
OBJECT
479542
end
480543
when RBS::Types::Optional
481-
UnionType[from_rbs_type(return_type.type, self_type, extra_vars), NIL]
544+
UnionType[from_rbs_type(return_type.type, self_type, extra_vars, nesting), NIL]
482545
when RBS::Types::Alias
483-
case return_type.name.name
484-
when :int
485-
INTEGER
486-
when :boolish
487-
BOOLEAN
488-
when :string
489-
STRING
490-
else
491-
# TODO: ???
492-
OBJECT
493-
end
546+
expanded = expand_alias_type(return_type, nesting)
547+
expanded ? from_rbs_type(expanded, self_type, extra_vars, nesting + 1) : OBJECT
494548
when RBS::Types::Interface
495-
# unimplemented
496-
OBJECT
549+
args = return_type.args.map { from_rbs_type _1, self_type, extra_vars, nesting }
550+
InterfaceType.new return_type.name, args
497551
when RBS::Types::ClassInstance
498552
klass = return_type.name.to_namespace.path.reduce(Object) { _1.const_get _2 }
499553
if return_type.args
500-
params = return_type.args.map { from_rbs_type _1, self_type, extra_vars }
554+
params = return_type.args.map { from_rbs_type _1, self_type, extra_vars, nesting }
501555
end
502556
InstanceType.new(klass, params || [])
503557
else
@@ -517,24 +571,24 @@ def self.match_free_variables(vars, types, values)
517571
accumulator.transform_values { UnionType[*_1] }
518572
end
519573

520-
def self._match_free_variable(vars, rbs_type, value, accumulator)
574+
def self._match_free_variable(vars, rbs_type, value, accumulator, nesting = 0)
521575
case [rbs_type, value]
522576
in [RBS::Types::Variable,]
523577
(accumulator[rbs_type.name] ||= []) << value if vars.include? rbs_type.name
524578
in [RBS::Types::ClassInstance, InstanceType]
525579
names = rbs_builder.build_singleton(rbs_type.name).type_params
526580
names.zip(rbs_type.args).each do |name, arg|
527581
v = value.named_params[name]
528-
_match_free_variable vars, arg, v, accumulator if v
582+
_match_free_variable vars, arg, v, accumulator, nesting if v
529583
end
530584
in [RBS::Types::Tuple, InstanceType] if value.klass == Array
531585
v = value.params[0]
532586
rbs_type.types.each do |t|
533-
_match_free_variable vars, t, v, accumulator
587+
_match_free_variable vars, t, v, accumulator, nesting
534588
end
535589
in [RBS::Types::Record, InstanceType] if value.klass == Hash
536590
# TODO
537-
in [RBS::Types::Interface,]
591+
in [RBS::Types::Interface,] if nesting < EXPANSION_NESTING_LIMIT
538592
definition = rbs_builder.build_interface rbs_type.name
539593
convert = {}
540594
definition.type_params.zip(rbs_type.args).each do |from, arg|
@@ -546,13 +600,20 @@ def self._match_free_variable(vars, rbs_type, value, accumulator)
546600
return_type = method_return_type value, method_name
547601
method.defs.each do |method_def|
548602
interface_return_type = method_def.type.type.return_type
549-
_match_free_variable convert, interface_return_type, return_type, ac
603+
_match_free_variable convert, interface_return_type, return_type, ac, nesting + 1
550604
end
551605
end
552606
convert.each do |from, to|
553607
values = ac[from]
554608
(accumulator[to] ||= []).concat values if values
555609
end
610+
in [RBS::Types::Union,]
611+
rbs_type.types.each do |t|
612+
_match_free_variable vars, t, value, accumulator, nesting
613+
end
614+
in [RBS::Types::Alias,]
615+
expanded = expand_alias_type(rbs_type, nesting)
616+
_match_free_variable vars, expanded, value, accumulator, nesting + 1 if expanded
556617
else
557618
end
558619
end

test/repl_type_completor/test_type_analyze.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,8 @@ def test_block_args
774774
def test_array_aref
775775
assert_call('[1][0..].', include: [Array, NilClass], exclude: Integer)
776776
assert_call('[1][0].', include: Integer, exclude: [Array, NilClass])
777+
# Float matches the `int` index param by duck typing (Float#to_int)
778+
assert_call('[1][0.5].', include: Integer, exclude: Array)
777779
assert_call('[1].[](0).', include: Integer, exclude: [Array, NilClass])
778780
assert_call('[1].[](0){}.', include: Integer, exclude: [Array, NilClass])
779781
end

test/repl_type_completor/test_types.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'repl_type_completor'
4+
require 'tmpdir'
45
require_relative './helper'
56

67
module TestReplTypeCompletor
@@ -94,6 +95,104 @@ def foobar; end
9495
assert_include type.all_methods, :rand
9596
end
9697

98+
def type_from_rbs(rbs_string)
99+
ReplTypeCompletor::Types.load_rbs_builder unless ReplTypeCompletor::Types.rbs_builder
100+
rbs_type = RBS::Parser.parse_type(rbs_string)
101+
ReplTypeCompletor::Types.from_rbs_type(rbs_type, ReplTypeCompletor::Types::OBJECT)
102+
end
103+
104+
def test_interface_type
105+
to_int = type_from_rbs('::_ToInt')
106+
assert_equal '_ToInt', to_int.inspect
107+
assert_equal [:to_int], to_int.methods
108+
assert ReplTypeCompletor::Types.intersect?(ReplTypeCompletor::Types::FLOAT, to_int)
109+
assert ReplTypeCompletor::Types.intersect?(to_int, ReplTypeCompletor::Types::FLOAT)
110+
refute ReplTypeCompletor::Types.intersect?(ReplTypeCompletor::Types::STRING, to_int)
111+
112+
to_ary = type_from_rbs('::_ToAry[::Integer]')
113+
assert_equal '_ToAry[Integer]', to_ary.inspect
114+
return_type = ReplTypeCompletor::Types.method_return_type(to_ary, :to_ary)
115+
assert_equal Array, return_type.klass
116+
assert_equal Integer, return_type.params[0].klass
117+
end
118+
119+
def test_alias_type_expansion
120+
int_type = type_from_rbs('::int')
121+
assert_equal 'Integer | _ToInt', int_type.inspect
122+
end
123+
124+
def with_isolated_rbs_env(rbs_source)
125+
Dir.mktmpdir do |dir|
126+
File.write File.join(dir, 'test.rbs'), rbs_source
127+
loader = RBS::EnvironmentLoader.new core_root: nil
128+
loader.add path: Pathname(dir)
129+
env = RBS::Environment.from_loader(loader)
130+
builder = RBS::DefinitionBuilder.new env: env.resolve_type_names
131+
original_builder = ReplTypeCompletor::Types.rbs_builder
132+
begin
133+
ReplTypeCompletor::Types.instance_variable_set :@rbs_builder, builder
134+
yield
135+
ensure
136+
ReplTypeCompletor::Types.instance_variable_set :@rbs_builder, original_builder
137+
end
138+
end
139+
end
140+
141+
def test_recursive_alias_type_expansion
142+
rbs_source = <<~RBS
143+
type json = Integer | Array[json] | Hash[String, json]
144+
type unguarded = unguarded | Integer
145+
type opt = [opt]? | Integer
146+
type a = Integer | [a]
147+
type b = a | [a]
148+
type c = b | [b]
149+
type d = c | [c]
150+
type e = d | [d]
151+
interface _Generic[T]
152+
def get: () -> T
153+
end
154+
type generic_rec = _Generic[generic_rec] | Integer
155+
RBS
156+
with_isolated_rbs_env rbs_source do
157+
json_type = type_from_rbs('::json')
158+
assert_equal [Array, Hash, Integer], json_type.types.map(&:klass).sort_by(&:name)
159+
json_elem = json_type.types.find { _1.klass == Array }.params[0]
160+
assert_include json_elem.types.map(&:klass), Integer
161+
162+
# Invalid in RBS (RecursiveTypeAliasError by `rbs validate`) but loadable
163+
assert_include type_from_rbs('::unguarded').types.map(&:klass), Integer
164+
165+
# Recursion through optional and interface type args
166+
assert_include type_from_rbs('::opt').types.map(&:klass), Integer
167+
generic_rec_type = type_from_rbs('::generic_rec')
168+
assert_include generic_rec_type.types.grep(ReplTypeCompletor::Types::InstanceType).map(&:klass), Integer
169+
170+
# Exponentially expanding alias chain
171+
assert_include type_from_rbs('::e').types.map(&:klass), Array
172+
end
173+
end
174+
175+
def test_cyclic_generic_interface_match
176+
rbs_source = <<~RBS
177+
interface _CycA[T]
178+
def a: () -> _CycB[T]
179+
end
180+
interface _CycB[T]
181+
def b: () -> _CycA[T]
182+
end
183+
RBS
184+
with_isolated_rbs_env rbs_source do
185+
var = RBS::Types::Variable.new(name: :X, location: nil)
186+
cyclic = RBS::Types::Interface.new(
187+
name: ReplTypeCompletor::Types.rbs_absolute_type_name('_CycA'),
188+
args: [var],
189+
location: nil
190+
)
191+
matched = ReplTypeCompletor::Types.match_free_variables([:X], [cyclic], [ReplTypeCompletor::Types::INTEGER])
192+
assert_kind_of Hash, matched
193+
end
194+
end
195+
97196
def test_basic_object_methods
98197
bo = BasicObject.new
99198
def bo.foobar; end

0 commit comments

Comments
 (0)