Skip to content

Commit 729cab9

Browse files
tompngclaude
andcommitted
Limit type alias and interface expansion depth
Recursive type aliases are valid RBS when guarded by a type constructor (e.g. `type json = Integer | Array[json]`); only unguarded recursion is rejected, and only by `rbs validate` which does not run on environment load. Eager alias expansion caused SystemStackError on recursive aliases, and nested alias chains can expand exponentially even without recursion, so a visited set is not enough; limit expansion nesting instead, falling back to Object at the cutoff. The nesting count is threaded as an argument: from_rbs_type only recurses into itself, so each function carries its own counter and resetting at the method_return_type boundary is safe. The limit also applies to the interface case of _match_free_variable, which could recurse infinitely on cyclic generic interfaces regardless of aliases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e56296b commit 729cab9

2 files changed

Lines changed: 98 additions & 16 deletions

File tree

lib/repl_type_completor/types.rb

Lines changed: 25 additions & 16 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

@@ -475,7 +476,15 @@ def self.array_of(*types)
475476
InstanceType.new(Array, [type])
476477
end
477478

478-
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)
479488
case return_type
480489
when RBS::Types::Bases::Self
481490
self_type
@@ -508,11 +517,11 @@ def self.from_rbs_type(return_type, self_type, extra_vars = {})
508517
end
509518
when RBS::Types::Union, RBS::Types::Intersection
510519
# Intersection is unsupported. fallback to union type
511-
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 }]
512521
when RBS::Types::Proc
513522
PROC
514523
when RBS::Types::Tuple
515-
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 }]
516525
InstanceType.new(Array, [elem])
517526
when RBS::Types::Record
518527
InstanceType.new(Hash, [SYMBOL, OBJECT])
@@ -532,17 +541,17 @@ def self.from_rbs_type(return_type, self_type, extra_vars = {})
532541
OBJECT
533542
end
534543
when RBS::Types::Optional
535-
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]
536545
when RBS::Types::Alias
537-
expanded = (rbs_builder.expand_alias2 return_type.name, return_type.args rescue nil)
538-
expanded ? from_rbs_type(expanded, self_type, extra_vars) : OBJECT
546+
expanded = expand_alias_type(return_type, nesting)
547+
expanded ? from_rbs_type(expanded, self_type, extra_vars, nesting + 1) : OBJECT
539548
when RBS::Types::Interface
540-
args = return_type.args.map { from_rbs_type _1, self_type, extra_vars }
549+
args = return_type.args.map { from_rbs_type _1, self_type, extra_vars, nesting }
541550
InterfaceType.new return_type.name, args
542551
when RBS::Types::ClassInstance
543552
klass = return_type.name.to_namespace.path.reduce(Object) { _1.const_get _2 }
544553
if return_type.args
545-
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 }
546555
end
547556
InstanceType.new(klass, params || [])
548557
else
@@ -562,24 +571,24 @@ def self.match_free_variables(vars, types, values)
562571
accumulator.transform_values { UnionType[*_1] }
563572
end
564573

565-
def self._match_free_variable(vars, rbs_type, value, accumulator)
574+
def self._match_free_variable(vars, rbs_type, value, accumulator, nesting = 0)
566575
case [rbs_type, value]
567576
in [RBS::Types::Variable,]
568577
(accumulator[rbs_type.name] ||= []) << value if vars.include? rbs_type.name
569578
in [RBS::Types::ClassInstance, InstanceType]
570579
names = rbs_builder.build_singleton(rbs_type.name).type_params
571580
names.zip(rbs_type.args).each do |name, arg|
572581
v = value.named_params[name]
573-
_match_free_variable vars, arg, v, accumulator if v
582+
_match_free_variable vars, arg, v, accumulator, nesting if v
574583
end
575584
in [RBS::Types::Tuple, InstanceType] if value.klass == Array
576585
v = value.params[0]
577586
rbs_type.types.each do |t|
578-
_match_free_variable vars, t, v, accumulator
587+
_match_free_variable vars, t, v, accumulator, nesting
579588
end
580589
in [RBS::Types::Record, InstanceType] if value.klass == Hash
581590
# TODO
582-
in [RBS::Types::Interface,]
591+
in [RBS::Types::Interface,] if nesting < EXPANSION_NESTING_LIMIT
583592
definition = rbs_builder.build_interface rbs_type.name
584593
convert = {}
585594
definition.type_params.zip(rbs_type.args).each do |from, arg|
@@ -591,7 +600,7 @@ def self._match_free_variable(vars, rbs_type, value, accumulator)
591600
return_type = method_return_type value, method_name
592601
method.defs.each do |method_def|
593602
interface_return_type = method_def.type.type.return_type
594-
_match_free_variable convert, interface_return_type, return_type, ac
603+
_match_free_variable convert, interface_return_type, return_type, ac, nesting + 1
595604
end
596605
end
597606
convert.each do |from, to|
@@ -600,11 +609,11 @@ def self._match_free_variable(vars, rbs_type, value, accumulator)
600609
end
601610
in [RBS::Types::Union,]
602611
rbs_type.types.each do |t|
603-
_match_free_variable vars, t, value, accumulator
612+
_match_free_variable vars, t, value, accumulator, nesting
604613
end
605614
in [RBS::Types::Alias,]
606-
expanded = rbs_builder.expand_alias2 rbs_type.name, rbs_type.args rescue nil
607-
_match_free_variable vars, expanded, value, accumulator if expanded
615+
expanded = expand_alias_type(rbs_type, nesting)
616+
_match_free_variable vars, expanded, value, accumulator, nesting + 1 if expanded
608617
else
609618
end
610619
end

test/repl_type_completor/test_types.rb

Lines changed: 73 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
@@ -120,6 +121,78 @@ def test_alias_type_expansion
120121
assert_equal 'Integer | _ToInt', int_type.inspect
121122
end
122123

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+
123196
def test_basic_object_methods
124197
bo = BasicObject.new
125198
def bo.foobar; end

0 commit comments

Comments
 (0)