From 8223d31d144c98e4a5da5ad1744a6bb5f5bed18b Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Wed, 25 Feb 2026 07:43:47 +0000 Subject: [PATCH] Fix superclass resolution for `class Foo < Foo` and add circular inheritance diagnostic When a class is defined with a superclass of the same name (e.g., `class Foo < Foo` inside a module), the superclass constant was incorrectly resolving to the class being defined itself, because TypeProf registers all definitions simultaneously. In Ruby, the superclass expression is evaluated before the class constant is created, so it naturally finds the outer definition. Fix this by using `lenv.cref.outer` for the superclass constant lookup when the superclass is a bare constant with the same name as the class being defined. Also add a diagnostic for circular inheritance detection during install, covering both direct self-reference (`class A < A`) and mutual recursion (`class A < B; class B < A`). Fixes #373 Co-Authored-By: pvcresin --- lib/typeprof/core/ast/module.rb | 32 ++++++++++++++++++- scenario/class/circular.rb | 3 ++ scenario/class/circular_mutual.rb | 23 +++++++++++++ .../regressions/superclass-self-reference.rb | 19 +++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 scenario/class/circular_mutual.rb create mode 100644 scenario/regressions/superclass-self-reference.rb diff --git a/lib/typeprof/core/ast/module.rb b/lib/typeprof/core/ast/module.rb index 8d97aabfd..cf125acce 100644 --- a/lib/typeprof/core/ast/module.rb +++ b/lib/typeprof/core/ast/module.rb @@ -89,7 +89,22 @@ class ClassNode < ModuleBaseNode def initialize(raw_node, lenv, use_result) super(raw_node, lenv, raw_node.constant_path, false, raw_node.body, use_result) raw_superclass = raw_node.superclass - @superclass_cpath = raw_superclass ? AST.create_node(raw_superclass, lenv) : nil + if raw_superclass + # In Ruby, the superclass expression is evaluated before the class constant + # is created. When the superclass is a bare constant with the same name as + # the class being defined (e.g., `class Foo < Foo` inside a module), use the + # outer scope to avoid resolving to the class itself. + if @static_cpath && lenv.cref.outer && + raw_superclass.type == :constant_read_node && + raw_superclass.name == @static_cpath.last + slenv = LocalEnv.new(lenv.file_context, lenv.cref.outer, {}, []) + @superclass_cpath = AST.create_node(raw_superclass, slenv) + else + @superclass_cpath = AST.create_node(raw_superclass, lenv) + end + else + @superclass_cpath = nil + end end attr_reader :superclass_cpath @@ -113,6 +128,21 @@ def undefine0(genv) def install0(genv) @superclass_cpath.install(genv) if @superclass_cpath + if @static_cpath && @superclass_cpath + const_read = @superclass_cpath.static_ret + if const_read && const_read.cpath + super_mod = genv.resolve_cpath(const_read.cpath) + self_mod = genv.resolve_cpath(@static_cpath) + mod = super_mod + while mod + if mod == self_mod + @changes.add_diagnostic(:code_range, "circular inheritance", @superclass_cpath) + break + end + mod = mod.superclass + end + end + end super(genv) end end diff --git a/scenario/class/circular.rb b/scenario/class/circular.rb index cfd30da14..5eefeee0c 100644 --- a/scenario/class/circular.rb +++ b/scenario/class/circular.rb @@ -16,3 +16,6 @@ class B # failed to identify its superclass end module M end + +## diagnostics +(4,10)-(4,11): circular inheritance diff --git a/scenario/class/circular_mutual.rb b/scenario/class/circular_mutual.rb new file mode 100644 index 000000000..9e00aa5b5 --- /dev/null +++ b/scenario/class/circular_mutual.rb @@ -0,0 +1,23 @@ +## update +class Foo +end + +class Bar + class Baz < Foo + end + class Foo < Baz + end +end + +## assert +class Foo +end +class Bar + class Bar::Baz < Bar::Foo + end + class Bar::Foo # failed to identify its superclass + end +end + +## diagnostics +(7,14)-(7,17): circular inheritance diff --git a/scenario/regressions/superclass-self-reference.rb b/scenario/regressions/superclass-self-reference.rb new file mode 100644 index 000000000..9fa1ecd3f --- /dev/null +++ b/scenario/regressions/superclass-self-reference.rb @@ -0,0 +1,19 @@ +## update: model.rb +class Foo + def value = 1 +end + +module Bar + class Foo < Foo + end +end + +## update: test.rb +def call + Bar::Foo.new.value +end + +## assert: test.rb +class Object + def call: -> Integer +end