Skip to content

Commit 8223d31

Browse files
mamepvcresin
andcommitted
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 <pvcresin0730@gmail.com>
1 parent 9959a28 commit 8223d31

4 files changed

Lines changed: 76 additions & 1 deletion

File tree

lib/typeprof/core/ast/module.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,22 @@ class ClassNode < ModuleBaseNode
8989
def initialize(raw_node, lenv, use_result)
9090
super(raw_node, lenv, raw_node.constant_path, false, raw_node.body, use_result)
9191
raw_superclass = raw_node.superclass
92-
@superclass_cpath = raw_superclass ? AST.create_node(raw_superclass, lenv) : nil
92+
if raw_superclass
93+
# In Ruby, the superclass expression is evaluated before the class constant
94+
# is created. When the superclass is a bare constant with the same name as
95+
# the class being defined (e.g., `class Foo < Foo` inside a module), use the
96+
# outer scope to avoid resolving to the class itself.
97+
if @static_cpath && lenv.cref.outer &&
98+
raw_superclass.type == :constant_read_node &&
99+
raw_superclass.name == @static_cpath.last
100+
slenv = LocalEnv.new(lenv.file_context, lenv.cref.outer, {}, [])
101+
@superclass_cpath = AST.create_node(raw_superclass, slenv)
102+
else
103+
@superclass_cpath = AST.create_node(raw_superclass, lenv)
104+
end
105+
else
106+
@superclass_cpath = nil
107+
end
93108
end
94109

95110
attr_reader :superclass_cpath
@@ -113,6 +128,21 @@ def undefine0(genv)
113128

114129
def install0(genv)
115130
@superclass_cpath.install(genv) if @superclass_cpath
131+
if @static_cpath && @superclass_cpath
132+
const_read = @superclass_cpath.static_ret
133+
if const_read && const_read.cpath
134+
super_mod = genv.resolve_cpath(const_read.cpath)
135+
self_mod = genv.resolve_cpath(@static_cpath)
136+
mod = super_mod
137+
while mod
138+
if mod == self_mod
139+
@changes.add_diagnostic(:code_range, "circular inheritance", @superclass_cpath)
140+
break
141+
end
142+
mod = mod.superclass
143+
end
144+
end
145+
end
116146
super(genv)
117147
end
118148
end

scenario/class/circular.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ class B # failed to identify its superclass
1616
end
1717
module M
1818
end
19+
20+
## diagnostics
21+
(4,10)-(4,11): circular inheritance

scenario/class/circular_mutual.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## update
2+
class Foo
3+
end
4+
5+
class Bar
6+
class Baz < Foo
7+
end
8+
class Foo < Baz
9+
end
10+
end
11+
12+
## assert
13+
class Foo
14+
end
15+
class Bar
16+
class Bar::Baz < Bar::Foo
17+
end
18+
class Bar::Foo # failed to identify its superclass
19+
end
20+
end
21+
22+
## diagnostics
23+
(7,14)-(7,17): circular inheritance
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## update: model.rb
2+
class Foo
3+
def value = 1
4+
end
5+
6+
module Bar
7+
class Foo < Foo
8+
end
9+
end
10+
11+
## update: test.rb
12+
def call
13+
Bar::Foo.new.value
14+
end
15+
16+
## assert: test.rb
17+
class Object
18+
def call: -> Integer
19+
end

0 commit comments

Comments
 (0)