From bca3ea0756c4a224afc1a65809b59ed3faec0d3d Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Thu, 20 Nov 2025 21:49:30 +0900 Subject: [PATCH] Fix type inference for Class.new assigned to constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a constant is assigned the result of Class.new (e.g., MyError = Class.new(StandardError)), TypeProf now correctly infers the constant as a singleton type instead of a Class instance. This fixes a NoMethodError when using such dynamically created classes in rescue clauses with assignment (rescue MyError => e). Changes: - Detect Class.new pattern in ConstantWriteNode#install0 - Create singleton type for the constant when Class.new is detected - Support both Class.new and ::Class.new patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lib/typeprof/core/ast/const.rb | 22 ++++++++++++++++++- .../control/rescue-assign-with-class-new.rb | 15 +++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 scenario/control/rescue-assign-with-class-new.rb diff --git a/lib/typeprof/core/ast/const.rb b/lib/typeprof/core/ast/const.rb index 927f05cb..2943a68f 100644 --- a/lib/typeprof/core/ast/const.rb +++ b/lib/typeprof/core/ast/const.rb @@ -112,10 +112,30 @@ def install0(genv) @cpath.install(genv) if @cpath val = @rhs.install(genv) if @static_cpath - @changes.add_edge(genv, val, @static_ret.vtx) + if detect_class_new?(@rhs) + # Create a module entity for the dynamically created class + mod = genv.resolve_cpath(@static_cpath) + + # Create singleton type for the new class + singleton_ty = Type::Singleton.new(genv, mod) + vtx = Source.new(singleton_ty) + @changes.add_edge(genv, vtx, @static_ret.vtx) + else + @changes.add_edge(genv, val, @static_ret.vtx) + end end val end + + private + + def detect_class_new?(node) + node.is_a?(CallBaseNode) && + node.mid == :new && + node.recv.is_a?(ConstantReadNode) && + node.recv.cname == :Class && + node.recv.cbase.nil? + end end end end diff --git a/scenario/control/rescue-assign-with-class-new.rb b/scenario/control/rescue-assign-with-class-new.rb new file mode 100644 index 00000000..ebfc82c7 --- /dev/null +++ b/scenario/control/rescue-assign-with-class-new.rb @@ -0,0 +1,15 @@ +## update: my_error.rb +MyError = Class.new(StandardError) + +## update: test.rb +class C + def foo + rescue MyError => e + raise ArgumentError, e.message + end +end + +## assert: test.rb +class C + def foo: -> nil +end