From abfb0751cff99c6159aed10c5d1fe3ab494b167f Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Wed, 15 Apr 2026 01:06:33 +0900 Subject: [PATCH] Fix infinite loop in `define_all` for RBS include with same-name nested constant When `module X` contains `class X`, resolving `include X` in RBS searched the ancestor chain (`search_ancestors=true`), which caused the resolved constant to flip between X (module) and X::X (class) on every re-evaluation, looping `define_all` forever. The .rb side already avoids this via `use_strict_const_scope` in IncludeMetaNode. Apply the same `strict_const_scope=true` to SigIncludeNode and SigPrependNode so that include/prepend targets are resolved through lexical scope only. --- lib/typeprof/core/ast/sig_decl.rb | 8 +++--- .../regressions/include-nested-same-name.rb | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 scenario/regressions/include-nested-same-name.rb diff --git a/lib/typeprof/core/ast/sig_decl.rb b/lib/typeprof/core/ast/sig_decl.rb index 324f514e..fa9acd83 100644 --- a/lib/typeprof/core/ast/sig_decl.rb +++ b/lib/typeprof/core/ast/sig_decl.rb @@ -232,10 +232,10 @@ def attrs = { cpath:, toplevel: } def define0(genv) @args.each {|arg| arg.define(genv) } const_reads = [] - const_read = BaseConstRead.new(genv, @cpath.first, @toplevel ? CRef::Toplevel : @lenv.cref, false) + const_read = BaseConstRead.new(genv, @cpath.first, @toplevel ? CRef::Toplevel : @lenv.cref, true) const_reads << const_read @cpath[1..].each do |cname| - const_read = ScopedConstRead.new(cname, const_read, false) + const_read = ScopedConstRead.new(cname, const_read, true) const_reads << const_read end mod = genv.resolve_cpath(@lenv.cref.cpath) @@ -281,10 +281,10 @@ def attrs = { cpath:, toplevel: } def define0(genv) @args.each {|arg| arg.define(genv) } const_reads = [] - const_read = BaseConstRead.new(genv, @cpath.first, @toplevel ? CRef::Toplevel : @lenv.cref, false) + const_read = BaseConstRead.new(genv, @cpath.first, @toplevel ? CRef::Toplevel : @lenv.cref, true) const_reads << const_read @cpath[1..].each do |cname| - const_read = ScopedConstRead.new(cname, const_read, false) + const_read = ScopedConstRead.new(cname, const_read, true) const_reads << const_read end mod = genv.resolve_cpath(@lenv.cref.cpath) diff --git a/scenario/regressions/include-nested-same-name.rb b/scenario/regressions/include-nested-same-name.rb new file mode 100644 index 00000000..83314201 --- /dev/null +++ b/scenario/regressions/include-nested-same-name.rb @@ -0,0 +1,25 @@ +# Regression: when a module contains a same-name nested class, +# RBS `include` resolution used to oscillate between the module +# and the nested class, causing define_all to loop forever. + +## update: test.rbs +module M + class M + end + + def foo: () -> String +end + +class C + include M +end + +## update: test.rb +def test + C.new.foo +end + +## assert +class Object + def test: -> String +end