Skip to content

Commit bfde4e1

Browse files
mameclaude
andcommitted
Prioritize RBS ivar declarations over inferred types on read
When an instance variable has an RBS type declaration, IVarReadBox now returns the declared type instead of the union of all assigned types. This prevents nil pollution from base-class initializations like @x = nil from leaking into subclass reads that have declarations. To make this work regardless of file load order, IVarReadBox subscribes to every ive it visits via add_depended_value_entity, and ValueEntity re-runs those subscribers from on_decl_changed when a declaration is added or removed by SigInstanceVariableNode. Without this, an RBS file loaded after the corresponding Ruby file would leave stale edges from the inferred path in the type graph. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f831cdf commit bfde4e1

6 files changed

Lines changed: 111 additions & 14 deletions

File tree

lib/typeprof/core/ast/sig_decl.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -505,20 +505,20 @@ def attrs = { cpath:, class_scope: }
505505

506506
def define0(genv)
507507
@type.define(genv)
508-
mod = genv.resolve_ivar(cpath, @class_scope, @var)
509-
mod.add_decl(self)
510-
mod
508+
mod = genv.resolve_cpath(cpath)
509+
mod.add_ivar_decl(genv, @class_scope, @var, self)
511510
end
512511

513512
def define_copy(genv)
514-
mod = genv.resolve_ivar(cpath, @class_scope, @var)
515-
mod.add_decl(self)
516-
mod.remove_decl(@prev_node)
513+
mod = genv.resolve_cpath(cpath)
514+
mod.add_ivar_decl(genv, @class_scope, @var, self)
515+
mod.remove_ivar_decl(genv, @class_scope, @var, @prev_node)
517516
super(genv)
518517
end
519518

520519
def undefine0(genv)
521-
genv.resolve_ivar(cpath, @class_scope, @var).remove_decl(self)
520+
mod = genv.resolve_cpath(cpath)
521+
mod.remove_ivar_decl(genv, @class_scope, @var, self)
522522
@type.undefine(genv)
523523
end
524524

lib/typeprof/core/env/module_entity.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,19 @@ def get_ivar(singleton, name)
436436
@ivars[singleton][name] ||= ValueEntity.new
437437
end
438438

439+
def add_ivar_decl(genv, singleton, name, decl)
440+
ive = get_ivar(singleton, name)
441+
ive.add_decl(decl)
442+
ive.on_decl_changed(genv)
443+
ive
444+
end
445+
446+
def remove_ivar_decl(genv, singleton, name, decl)
447+
ive = get_ivar(singleton, name)
448+
ive.remove_decl(decl)
449+
ive.on_decl_changed(genv)
450+
end
451+
439452
def get_cvar(name)
440453
@cvars[name] ||= ValueEntity.new
441454
end

lib/typeprof/core/env/value_entity.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ def remove_decl(decl)
1717
@decls.delete(decl) || raise
1818
end
1919

20+
# Re-run all read boxes that depend on this entity. Used when a
21+
# declaration is added or removed so that dependents (e.g. an
22+
# IVarReadBox that previously fell back to the inferred type) can
23+
# observe the new state.
24+
def on_decl_changed(genv)
25+
@read_boxes.each {|box| genv.add_run(box) }
26+
end
27+
2028
def add_def(def_)
2129
@defs << def_
2230
end

lib/typeprof/core/graph/box.rb

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,23 +1280,44 @@ def run0(genv, changes)
12801280
singleton = @singleton
12811281
cur_ive = mod.get_ivar(singleton, @name)
12821282
target_vtx = nil
1283+
target_decls = nil
12831284
genv.each_direct_superclass(mod, singleton) do |mod, singleton|
12841285
ive = mod.get_ivar(singleton, @name)
1286+
# Subscribe to every visited ive so that, if one later acquires an
1287+
# RBS declaration, this box is re-run and switches to the declared
1288+
# type instead of the inferred one.
1289+
changes.add_depended_value_entity(ive)
12851290
if ive.exist?
12861291
target_vtx = ive.vtx
1292+
target_decls = ive.decls unless ive.decls.empty?
1293+
break if target_decls
12871294
end
12881295
end
1289-
edges = []
1290-
if target_vtx
1296+
1297+
if target_decls
1298+
# When declarations exist, return declared types instead of assigned types
1299+
target_decls.each do |decl|
1300+
subst = {}
1301+
if decl.cpath
1302+
decl_mod = genv.resolve_cpath(decl.cpath)
1303+
if decl_mod.type_params && !decl_mod.type_params.empty?
1304+
subst = decl_mod.type_params.to_h do |param, _default_ty|
1305+
[param, Vertex.new(@node)]
1306+
end
1307+
end
1308+
end
1309+
vtx = decl.type.covariant_vertex(genv, changes, subst)
1310+
changes.add_edge(genv, vtx, @ret)
1311+
end
1312+
elsif target_vtx
1313+
edges = []
12911314
if target_vtx != cur_ive.vtx
12921315
edges << [cur_ive.vtx, @proxy] << [@proxy, target_vtx]
12931316
end
12941317
edges << [target_vtx, @ret]
1295-
else
1296-
# TODO: error?
1297-
end
1298-
edges.each do |src, dst|
1299-
changes.add_edge(genv, src, dst)
1318+
edges.each do |src, dst|
1319+
changes.add_edge(genv, src, dst)
1320+
end
13001321
end
13011322
end
13021323
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## update: test.rb
2+
class Foo
3+
def initialize
4+
@x = nil
5+
end
6+
7+
def get_x
8+
@x
9+
end
10+
end
11+
12+
## update: test.rbs
13+
class Foo
14+
@x: Integer
15+
end
16+
17+
## assert: test.rb
18+
class Foo
19+
def initialize: -> void
20+
def get_x: -> Integer
21+
end

scenario/rbs/ivar_decl_priority.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## update: test.rbs
2+
class Base
3+
end
4+
5+
class Child < Base
6+
@x: Integer
7+
end
8+
9+
## update: test.rb
10+
class Base
11+
def initialize
12+
@x = nil
13+
end
14+
end
15+
16+
class Child < Base
17+
def initialize
18+
super
19+
@x = 1
20+
end
21+
22+
def get_x
23+
@x
24+
end
25+
end
26+
27+
## assert
28+
class Base
29+
def initialize: -> void
30+
end
31+
class Child < Base
32+
def initialize: -> void
33+
def get_x: -> Integer
34+
end

0 commit comments

Comments
 (0)