Skip to content

Commit ba9a78e

Browse files
committed
Support class Foo = Bar / module Foo = Bar declarations in RBS
The empty `when RBS::AST::Declarations::AliasDecl` branch in `AST.create_rbs_decl` was previously a no-op (the prior fix only avoided the crash). This commit implements actual semantic resolution for class and module aliases, so that: - `module YAML = Psych` makes `YAML.load`, `YAML::SyntaxError`, and `include YAML` resolve identically to `Psych.load` etc. - `class HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess` (used in gem_rbs_collection's activesupport RBS) resolves correctly. Implementation (Option A from the design discussion): - `ModuleEntity` gains `@alias_decls` (decl => target_mod) and `@alias_target`, with `add_alias_decl`/`remove_alias_decl` mirroring the existing module decl lifecycle. The alias decl is also registered as a const on the outer module so const lookup of the alias name succeeds. - `Genv#resolve_cpath` follows `alias_target` after resolving each cpath segment, with cycle detection to handle `module A = B; module B = A`. - New AST nodes `SigClassAliasNode` / `SigModuleAliasNode` register the alias via the standard `define`/`undefine`/`install` lifecycle. They access the alias's own `ModuleEntity` via `outer.inner_modules[cname]` directly to avoid being short-circuited by `resolve_cpath`'s alias following. - `AST.create_rbs_decl` replaces the empty `when AliasDecl` branch with explicit `when ClassAlias` / `when ModuleAlias` branches. Tests cover singleton method, nested constant, instance method, mixin, nested-module alias, and the cycle case.
1 parent 6f94f26 commit ba9a78e

7 files changed

Lines changed: 165 additions & 2 deletions

File tree

lib/typeprof/core/ast.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,10 @@ def self.create_rbs_decl(raw_decl, lenv)
436436
SigInterfaceNode.new(raw_decl, lenv)
437437
when RBS::AST::Declarations::Constant
438438
SigConstNode.new(raw_decl, lenv)
439-
when RBS::AST::Declarations::AliasDecl
439+
when RBS::AST::Declarations::ClassAlias
440+
SigClassAliasNode.new(raw_decl, lenv)
441+
when RBS::AST::Declarations::ModuleAlias
442+
SigModuleAliasNode.new(raw_decl, lenv)
440443
when RBS::AST::Declarations::TypeAlias
441444
SigTypeAliasNode.new(raw_decl, lenv)
442445
# TODO: check

lib/typeprof/core/ast/sig_decl.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,52 @@ def install0(genv)
414414
end
415415
end
416416

417+
class SigModuleAliasBaseNode < Node
418+
def initialize(raw_decl, lenv)
419+
super(raw_decl, lenv)
420+
@cpath = AST.resolve_rbs_name(raw_decl.new_name, lenv)
421+
@old_cpath = AST.resolve_rbs_name(raw_decl.old_name, lenv)
422+
end
423+
424+
attr_reader :cpath, :old_cpath
425+
def attrs = { cpath:, old_cpath: }
426+
427+
def define0(genv)
428+
outer = genv.resolve_cpath(@cpath[0..-2])
429+
cname = @cpath.last
430+
alias_mod = outer.inner_modules[cname] ||= ModuleEntity.new(outer.cpath + [cname], outer)
431+
target_mod = genv.resolve_cpath(@old_cpath)
432+
alias_mod.add_alias_decl(genv, self, target_mod)
433+
end
434+
435+
def define_copy(genv)
436+
outer = genv.resolve_cpath(@cpath[0..-2])
437+
alias_mod = outer.inner_modules[@cpath.last]
438+
target_mod = genv.resolve_cpath(@old_cpath)
439+
alias_mod.add_alias_decl(genv, self, target_mod)
440+
alias_mod.remove_alias_decl(genv, @prev_node)
441+
super(genv)
442+
end
443+
444+
def undefine0(genv)
445+
outer = genv.resolve_cpath(@cpath[0..-2])
446+
alias_mod = outer.inner_modules[@cpath.last]
447+
alias_mod.remove_alias_decl(genv, self)
448+
end
449+
450+
def install0(genv)
451+
mod_val = Source.new(Type::Singleton.new(genv, genv.resolve_cpath(@cpath)))
452+
@changes.add_edge(genv, mod_val, @static_ret.vtx)
453+
Source.new
454+
end
455+
end
456+
457+
class SigClassAliasNode < SigModuleAliasBaseNode
458+
end
459+
460+
class SigModuleAliasNode < SigModuleAliasBaseNode
461+
end
462+
417463
class SigConstNode < Node
418464
def initialize(raw_decl, lenv)
419465
super(raw_decl, lenv)

lib/typeprof/core/env.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ def resolve_cpath(cpath)
221221
raise unless cpath # annotation
222222
cpath.each do |cname|
223223
mod = mod.inner_modules[cname] ||= ModuleEntity.new(mod.cpath + [cname], mod)
224+
mod = follow_alias(mod)
225+
end
226+
mod
227+
end
228+
229+
def follow_alias(mod)
230+
visited = nil
231+
while mod.alias_target
232+
visited ||= Set.empty
233+
break if visited.include?(mod) # cycle
234+
visited << mod
235+
mod = mod.alias_target
224236
end
225237
mod
226238
end

lib/typeprof/core/env/module_entity.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ def initialize(cpath, outer_module = self)
1010
@prepend_decls = []
1111
@prepend_defs = []
1212

13+
# `class Foo = Bar` / `module Foo = Bar` declarations attached to this entity.
14+
# Maps an alias decl to the target ModuleEntity at the time of registration.
15+
@alias_decls = {}
16+
@alias_target = nil
17+
1318
@inner_modules = {}
1419
@outer_module = outer_module
1520

@@ -42,6 +47,8 @@ def initialize(cpath, outer_module = self)
4247
attr_reader :cpath
4348
attr_reader :module_decls
4449
attr_reader :module_defs
50+
attr_reader :alias_decls
51+
attr_reader :alias_target
4552

4653
attr_reader :inner_modules
4754
attr_reader :outer_module
@@ -79,7 +86,7 @@ def get_cname
7986
end
8087

8188
def exist?
82-
!@module_decls.empty? || !@module_defs.empty?
89+
!@module_decls.empty? || !@module_defs.empty? || !@alias_decls.empty?
8390
end
8491

8592
def on_inner_modules_changed(genv, changed_cname)
@@ -176,6 +183,22 @@ def remove_module_def(genv, node)
176183
on_module_removed(genv)
177184
end
178185

186+
def add_alias_decl(genv, decl, target_mod)
187+
on_module_added(genv)
188+
@alias_decls[decl] = target_mod
189+
@alias_target = @alias_decls.values.first
190+
ce = @outer_module.get_const(get_cname)
191+
ce.add_decl(decl)
192+
ce
193+
end
194+
195+
def remove_alias_decl(genv, decl)
196+
@outer_module.get_const(get_cname).remove_decl(decl)
197+
@alias_decls.delete(decl) || raise
198+
@alias_target = @alias_decls.values.first
199+
on_module_removed(genv)
200+
end
201+
179202
def add_include_decl(genv, node)
180203
@include_decls << node
181204
genv.add_static_eval_queue(:parent_modules_changed, self)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## update: test.rbs
2+
module A = B
3+
module B = A
4+
5+
## update: test.rb
6+
def test
7+
A
8+
end
9+
10+
## diagnostics: test.rb
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## update: test.rbs
2+
module Outer
3+
module Inner
4+
def self.greet: () -> String
5+
CONST: Integer
6+
end
7+
module InnerAlias = Inner
8+
end
9+
10+
## update: test.rb
11+
def test1
12+
Outer::InnerAlias.greet
13+
end
14+
15+
def test2
16+
Outer::InnerAlias::CONST
17+
end
18+
19+
## assert: test.rb
20+
class Object
21+
def test1: -> String
22+
def test2: -> Integer
23+
end
24+
25+
## diagnostics: test.rb

scenario/rbs/class-module-alias.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
## update: test.rbs
2+
class Foo
3+
def foo: () -> Integer
4+
end
5+
class Bar = Foo
6+
module M
7+
def m: () -> String
8+
CONST: Symbol
9+
end
10+
module N = M
11+
12+
## update: test.rb
13+
def test1
14+
Foo.new.foo
15+
end
16+
17+
def test2
18+
Bar.new.foo
19+
end
20+
21+
def test3
22+
N::CONST
23+
end
24+
25+
class UseN
26+
include N
27+
28+
def test4
29+
m
30+
end
31+
end
32+
33+
## assert: test.rb
34+
class Object
35+
def test1: -> Integer
36+
def test2: -> Integer
37+
def test3: -> Symbol
38+
end
39+
class UseN
40+
include M
41+
def test4: -> String
42+
end
43+
44+
## diagnostics: test.rb

0 commit comments

Comments
 (0)