From ea63fa818b581dbdd48b8d421e3a015c7cf0354d Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 3 Apr 2026 13:25:38 +0900 Subject: [PATCH 1/2] Fix splat to wrap non-array types as [x] instead of calling to_a Ruby's splat operator [*x] calls to_a on the value if it responds to to_a, and wraps it as [x] otherwise. TypeProf was unconditionally calling to_a, causing false "undefined method: Symbol#to_a" errors. Now SplatBox falls back to wrapping non-array types when to_a has no result, and the to_a MethodCallBox suppresses undefined method errors for types that don't have to_a. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/typeprof/core/ast/misc.rb | 4 ++-- lib/typeprof/core/graph/box.rb | 24 +++++++++++++++++------- lib/typeprof/core/graph/change_set.rb | 12 ++++++------ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/typeprof/core/ast/misc.rb b/lib/typeprof/core/ast/misc.rb index 9c94bc5e..52db028d 100644 --- a/lib/typeprof/core/ast/misc.rb +++ b/lib/typeprof/core/ast/misc.rb @@ -256,9 +256,9 @@ def install0(genv) vtx = @expr.install(genv) a_args = ActualArguments.new([], [], nil, nil) - vtx = @changes.add_method_call_box(genv, vtx, :to_a, a_args, false).ret + to_a_vtx = @changes.add_method_call_box(genv, vtx, :to_a, a_args, false, suppress_errors: true).ret - @changes.add_splat_box(genv, vtx).ret + @changes.add_splat_box(genv, to_a_vtx, nil, vtx).ret end end diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index 1e62042c..91fb388d 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -600,11 +600,13 @@ def wrong_return_type(f_ret_show, changes) end class SplatBox < Box - def initialize(node, genv, ary, idx) + def initialize(node, genv, ary, idx, orig = nil) super(node) @ary = ary @idx = idx + @orig = orig @ary.add_edge(genv, self) + @orig.add_edge(genv, self) if @orig @ret = Vertex.new(node) end @@ -629,6 +631,12 @@ def run0(genv, changes) "???" end end + # For types where to_a is not defined, [*x] wraps x as [x] + if @orig && @ary.types.empty? + @orig.each_type do |ty| + changes.add_edge(genv, Source.new(ty), @ret) + end + end end end @@ -992,7 +1000,7 @@ def run0(genv, changes) end class MethodCallBox < Box - def initialize(node, genv, recv, mid, a_args, subclasses) + def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false) raise mid.to_s unless mid super(node) @recv = recv.new_vertex(genv, node) @@ -1003,6 +1011,7 @@ def initialize(node, genv, recv, mid, a_args, subclasses) @a_args.block.add_edge(genv, self) if @a_args.block @ret = Vertex.new(node) @subclasses = subclasses + @suppress_errors = suppress_errors @generics = {} end @@ -1014,10 +1023,11 @@ def run0(genv, changes) error_count = 0 resolve(genv, changes) do |me, ty, mid, orig_ty| if !me - # TODO: undefined method error - if error_count < 3 - meth = @node.mid_code_range ? :mid_code_range : :code_range - changes.add_diagnostic(meth, "undefined method: #{ orig_ty.show }##{ mid }") + unless @suppress_errors + if error_count < 3 + meth = @node.mid_code_range ? :mid_code_range : :code_range + changes.add_diagnostic(meth, "undefined method: #{ orig_ty.show }##{ mid }") + end end error_count += 1 elsif me.builtin && me.builtin[changes, @node, orig_ty, @a_args, @ret] @@ -1062,7 +1072,7 @@ def run0(genv, changes) edges.each do |src, dst| changes.add_edge(genv, src, dst) end - if error_count > 3 + if error_count > 3 && !@suppress_errors meth = @node.mid_code_range ? :mid_code_range : :code_range changes.add_diagnostic(meth, "... and other #{ error_count - 3 } errors") end diff --git a/lib/typeprof/core/graph/change_set.rb b/lib/typeprof/core/graph/change_set.rb index 802d864a..57bada27 100644 --- a/lib/typeprof/core/graph/change_set.rb +++ b/lib/typeprof/core/graph/change_set.rb @@ -69,9 +69,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) - key = [:mcall, recv, mid, a_args, subclasses] - @new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses) + def add_method_call_box(genv, recv, mid, a_args, subclasses, suppress_errors: false) + key = [:mcall, recv, mid, a_args, subclasses, suppress_errors] + @new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors) end def add_escape_box(genv, a_ret) @@ -79,9 +79,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) - key = [:splat, arg, idx] - @new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx) + def add_splat_box(genv, arg, idx = nil, orig = nil) + key = [:splat, arg, idx, orig] + @new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, orig) end def add_hash_splat_box(genv, arg, unified_key, unified_val) From 26f4f354f3f56366f531089196494a129f1743e1 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 3 Apr 2026 13:36:22 +0900 Subject: [PATCH 2/2] Add known bug for splat fallback with union types When [*x] has x typed as Array[T] | Symbol, the Symbol side doesn't get wrapped as [Symbol] because to_a succeeds for the Array side, making @ary non-empty and skipping the fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- scenario/known-issues/splat-union-fallback.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 scenario/known-issues/splat-union-fallback.rb diff --git a/scenario/known-issues/splat-union-fallback.rb b/scenario/known-issues/splat-union-fallback.rb new file mode 100644 index 00000000..a6b6b625 --- /dev/null +++ b/scenario/known-issues/splat-union-fallback.rb @@ -0,0 +1,12 @@ +## update +def foo(x) + [*x] +end + +foo([:int]) +foo(:sym) + +## assert +class Object + def foo: (Array[:int] | :sym) -> Array[:int | :sym] +end