From 3b4aff13b545def35195e1ffac108568cffb83a3 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Tue, 1 Sep 2026 17:45:03 +0900 Subject: [PATCH] Guard against an include/extend cycle in on_ancestors_updated `module Foo; module Util; include Foo; end; extend Util; end` is valid Ruby but made `on_ancestors_updated` recurse until SystemStackError. `include`/`prepend` cycles are rejected by Ruby itself, so `extend` was the first way to reach it. Add the same guard `each_descendant` has, plus scenarios for extend resolution. --- lib/typeprof/core/env/module_entity.rb | 2 + scenario/class/extend-cycle.rb | 20 +++++++ scenario/class/extend.rb | 77 ++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 scenario/class/extend-cycle.rb create mode 100644 scenario/class/extend.rb diff --git a/lib/typeprof/core/env/module_entity.rb b/lib/typeprof/core/env/module_entity.rb index 06dc7e89..c961266b 100644 --- a/lib/typeprof/core/env/module_entity.rb +++ b/lib/typeprof/core/env/module_entity.rb @@ -482,6 +482,8 @@ def on_parent_modules_changed(genv) end def on_ancestors_updated(genv, base_mod) + # `include` and `extend` can form a cycle in valid Ruby + return if base_mod == self @child_modules.each_key {|child_mod| child_mod.on_ancestors_updated(genv, base_mod || self) } @static_reads.each_value do |static_reads| static_reads.each do |static_read| diff --git a/scenario/class/extend-cycle.rb b/scenario/class/extend-cycle.rb new file mode 100644 index 00000000..da3d0b87 --- /dev/null +++ b/scenario/class/extend-cycle.rb @@ -0,0 +1,20 @@ +## update +module Foo + module Util + include Foo + def helper = "h" + end + extend Util +end + +def f = Foo.helper + +## assert +module Foo + module Util + def helper: -> String + end +end +class Object + def f: -> String +end diff --git a/scenario/class/extend.rb b/scenario/class/extend.rb new file mode 100644 index 00000000..b74bbc1f --- /dev/null +++ b/scenario/class/extend.rb @@ -0,0 +1,77 @@ +## update +module A + def a = 1 +end +module B + def b = "s" +end +class C + extend A + extend B +end + +def f = [C.a, C.b] + +## assert +module A + def a: -> Integer +end +module B + def b: -> String +end +class C + extend A + extend B +end +class Object + def f: -> [Integer, String] +end + +## update +module M + def hi = "from M" +end +class C + extend M + def self.hi = 42 +end + +def f = C.hi + +## assert +module M + def hi: -> String +end +class C + extend M + def self.hi: -> Integer +end +class Object + def f: -> Integer +end + +## update +module Inner + def deep = 1.0 +end +module Outer + include Inner +end +class C + extend Outer +end + +def f = C.deep + +## assert +module Inner + def deep: -> Float +end +module Outer +end +class C + extend Outer +end +class Object + def f: -> Float +end