diff --git a/lib/typeprof/core/ast/misc.rb b/lib/typeprof/core/ast/misc.rb index 04a957d9..f1d729ac 100644 --- a/lib/typeprof/core/ast/misc.rb +++ b/lib/typeprof/core/ast/misc.rb @@ -256,8 +256,9 @@ def install0(genv) vtx = @expr.install(genv) a_args = ActualArguments.new([], [], nil, nil) - # Keep missing `to_a` receivers separate so Union members can fall back independently. - to_a_fallback = Vertex.new(self) + # Receiver types with no `to_a` are collected here, so that each union + # member can independently fall back to wrapping itself as `[x]`. + unresolved_recv = Vertex.new(self) to_a_box = @changes.add_method_call_box( genv, vtx, @@ -265,10 +266,10 @@ def install0(genv) a_args, false, suppress_errors: true, - fallback: to_a_fallback, + unresolved_recv: unresolved_recv, ) - @changes.add_splat_box(genv, to_a_box.ret, nil, to_a_fallback).ret + @changes.add_splat_box(genv, to_a_box.ret, nil, unresolved_recv).ret end end diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index a83df8fd..3f9ce93a 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -643,13 +643,13 @@ def wrong_return_type(f_ret_show, changes) end class SplatBox < Box - def initialize(node, genv, ary, idx, fallback = nil) + def initialize(node, genv, ary, idx, unresolved_recv = nil) super(node) @ary = ary @idx = idx - @fallback = fallback + @unresolved_recv = unresolved_recv @ary.add_edge(genv, self) - @fallback.add_edge(genv, self) if @fallback + @unresolved_recv.add_edge(genv, self) if @unresolved_recv @ret = Vertex.new(node) end @@ -675,8 +675,8 @@ def run0(genv, changes) end end # For types where to_a is not defined, [*x] wraps x as [x] - if @fallback - @fallback.each_type do |ty| + if @unresolved_recv + @unresolved_recv.each_type do |ty| changes.add_edge(genv, Source.new(ty), @ret) end end @@ -1047,7 +1047,11 @@ def run0(genv, changes) end class MethodCallBox < Box - def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false, fallback: nil) + # `unresolved_recv`, when given, collects the receiver types for which no + # method entity was found. Without it those types only become "undefined + # method" diagnostics; with it the caller can handle them itself, as `[*x]` + # does to wrap a receiver that has no `to_a`. + def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false, unresolved_recv: nil) raise mid.to_s unless mid super(node) @recv = recv.new_vertex(genv, node) @@ -1059,7 +1063,7 @@ def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false @ret = Vertex.new(node) @subclasses = subclasses @suppress_errors = suppress_errors - @fallback = fallback + @unresolved_recv = unresolved_recv @generics = {} end @@ -1074,8 +1078,8 @@ def run0(genv, changes) box = add_symbol_proc_call_box(changes, genv, orig_ty.sym, @a_args.positionals, @a_args.keywords) changes.add_edge(genv, box.ret, @ret) if box elsif !me - if @fallback - changes.add_edge(genv, Source.new(orig_ty), @fallback) + if @unresolved_recv + changes.add_edge(genv, Source.new(orig_ty), @unresolved_recv) end unless @suppress_errors if error_count < 3 diff --git a/lib/typeprof/core/graph/change_set.rb b/lib/typeprof/core/graph/change_set.rb index d80a8122..64d22c1c 100644 --- a/lib/typeprof/core/graph/change_set.rb +++ b/lib/typeprof/core/graph/change_set.rb @@ -71,9 +71,9 @@ def add_edge(genv, src, dst) # TODO: if an edge is removed during one analysis, we may need to remove sub-boxes? - def add_method_call_box(genv, recv, mid, a_args, subclasses, suppress_errors: false, fallback: nil) - key = [:mcall, recv, mid, a_args, subclasses, suppress_errors, fallback] - new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors, fallback: fallback) + def add_method_call_box(genv, recv, mid, a_args, subclasses, suppress_errors: false, unresolved_recv: nil) + key = [:mcall, recv, mid, a_args, subclasses, suppress_errors, unresolved_recv] + new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors, unresolved_recv: unresolved_recv) end def add_escape_box(genv, a_ret) @@ -81,9 +81,9 @@ def add_escape_box(genv, a_ret) new_boxes[key] ||= EscapeBox.new(@node, genv, a_ret) end - def add_splat_box(genv, arg, idx = nil, fallback = nil) - key = [:splat, arg, idx, fallback] - new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, fallback) + def add_splat_box(genv, arg, idx = nil, unresolved_recv = nil) + key = [:splat, arg, idx, unresolved_recv] + new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, unresolved_recv) end def add_hash_splat_box(genv, arg, unified_key, unified_val) diff --git a/scenario/array/splat.rb b/scenario/array/splat.rb index 5578825f..a285330d 100644 --- a/scenario/array/splat.rb +++ b/scenario/array/splat.rb @@ -24,3 +24,37 @@ def foo(x) class Object def foo: (:sym | [:int]) -> Array[:int | :sym] end + +## update +def foo(x) + [*x] +end + +foo([1]) +foo(nil) + +## assert +class Object + def foo: ([Integer]?) -> Array[Integer] +end + +## update +class C + def to_ary + [1] + end +end + +def foo(x) + [*x] +end + +foo(C.new) + +## assert +class C + def to_ary: -> [Integer] +end +class Object + def foo: (C) -> Array[C] +end diff --git a/scenario/incremental/splat-to-a.rb b/scenario/incremental/splat-to-a.rb new file mode 100644 index 00000000..7450ea56 --- /dev/null +++ b/scenario/incremental/splat-to-a.rb @@ -0,0 +1,54 @@ +## update +class C +end + +def foo(x) + [*x] +end + +foo(C.new) + +## assert +class C +end +class Object + def foo: (C) -> Array[C] +end + +## update +class C + def to_a + [1] + end +end + +def foo(x) + [*x] +end + +foo(C.new) + +## assert +class C + def to_a: -> [Integer] +end +class Object + def foo: (C) -> Array[Integer] +end + +## update +class C +end + +def foo(x) + [*x] +end + +foo(C.new) + +## assert +class C +end +class Object + def foo: (C) -> Array[C] +end