From e643d70a0e43ce880dcb7619c5e6c7f2575a8686 Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Sun, 27 Sep 2026 17:44:45 +0900 Subject: [PATCH 1/2] Dispatch Struct.[] to the initialize of the receiver class Struct.[] is an alias for Struct.new, but TypeProf generated it from the member list independently of initialize. When a struct overrode initialize, `self.[]` did not follow it: the RBS showed a stale signature and valid calls were reported as wrong number of arguments. --- lib/typeprof/core/ast/meta.rb | 18 ++++++++++-------- lib/typeprof/core/graph/box.rb | 4 ++-- lib/typeprof/core/service.rb | 8 ++++++++ scenario/diagnostics/struct-aref.rb | 13 +++++++++++++ scenario/incremental/struct-new-block.rb | 2 +- scenario/misc/struct_new.rb | 6 +++--- 6 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 scenario/diagnostics/struct-aref.rb diff --git a/lib/typeprof/core/ast/meta.rb b/lib/typeprof/core/ast/meta.rb index a219a184f..af5488f24 100644 --- a/lib/typeprof/core/ast/meta.rb +++ b/lib/typeprof/core/ast/meta.rb @@ -323,6 +323,10 @@ def define0(genv) ive = genv.resolve_ivar(struct_base_cpath, false, member) ive.add_def(self) end + if @kind == :struct + # Struct.[] calls the receiver's initialize, like Class#new + genv.resolve_method(struct_base_cpath, true, :[]).builtin = Builtin.new(genv).method(:class_new) + end @block_body.define(genv) if @block_body cdef end @@ -345,11 +349,16 @@ def define_copy(genv) def undefine0(genv) mod = genv.resolve_cpath(@static_cpath) mod.remove_module_def(genv, self) - genv.resolve_cpath(struct_base_cpath).remove_module_def(genv, self) + base = genv.resolve_cpath(struct_base_cpath) + base.remove_module_def(genv, self) @members.each do |member| ive = genv.resolve_ivar(struct_base_cpath, false, member) ive.remove_def(self) end + # On an update, the new node has already set the builtin + if base.module_defs.to_a.none? { _1.kind == :struct } + genv.resolve_method(struct_base_cpath, true, :[]).builtin = nil + end @block_body.undefine(genv) if @block_body end @@ -395,13 +404,6 @@ def install0(genv) end @changes.add_method_def_box(genv, cpath, false, :initialize, init_f_args, [init_ret]) - # Struct.[] is an alias for Struct.new - if @kind == :struct - # Struct.[] builds the struct class itself, not the base class - self_ret = @changes.add_escape_box(genv, Source.new(Type::Instance.new(genv, genv.resolve_cpath(@static_cpath), []))) - @changes.add_method_def_box(genv, cpath, true, :[], init_f_args, [self_ret]) - end - # Install block body (additional method definitions) if @block_body @block_body.lenv.locals[:"*self"] = @block_body.lenv.cref.get_self(genv) diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index 68053326c..25cce3d97 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -840,7 +840,7 @@ def call(changes, genv, a_args, ret) end end - def show(output_parameter_names) + def show(output_parameter_names, ret: nil) block_show = [] if @record_block.used blk_f_args = @record_block.f_args.map {|arg| arg.show }.join(", ") @@ -891,7 +891,7 @@ def show(output_parameter_names) args = args.join(", ") s = args.empty? ? [] : ["(#{ args })"] s << "#{ block_show.sort.join(" | ") }" unless block_show.empty? - s << "-> #{ @mid == :initialize ? "void" : @ret.show }" + s << "-> #{ ret || (@mid == :initialize ? "void" : @ret.show) }" s.join(" ") end end diff --git a/lib/typeprof/core/service.rb b/lib/typeprof/core/service.rb index ed8077f9d..a71b44fea 100644 --- a/lib/typeprof/core/service.rb +++ b/lib/typeprof/core/service.rb @@ -520,6 +520,14 @@ def dump_declarations(path) !@genv.resolve_method(node.static_cpath, mdef.singleton, mdef.mid).defs.empty? out << " " * stack.size + "def #{ mdef.singleton ? "self." : "" }#{ mdef.mid }: " + mdef.show(@options[:output_parameter_names]) end + if node.is_a?(AST::StructNewNode) && node.kind == :struct && + @genv.resolve_method(node.static_cpath, true, :[]).defs.empty? + # Struct.[] takes the same arguments as the initialize it reaches + init_mdef = @genv.resolve_method(node.static_cpath, false, :initialize).defs.to_a.first || + @genv.resolve_method(node.struct_base_cpath, false, :initialize).defs.to_a.first + ret = Type::Instance.new(@genv, mod, []).show + out << " " * stack.size + "def self.[]: " + init_mdef.show(@options[:output_parameter_names], ret:) + end else stack.pop out << " " * stack.size + "end" diff --git a/scenario/diagnostics/struct-aref.rb b/scenario/diagnostics/struct-aref.rb new file mode 100644 index 000000000..c9e1aba81 --- /dev/null +++ b/scenario/diagnostics/struct-aref.rb @@ -0,0 +1,13 @@ +## update +Pt = Struct.new(:x, :y) do + def initialize(x = 0, y = 0) + super + end +end + +Pt[] +Pt[3, 4] +Pt[1, 2, 3] + +## diagnostics +(9,2)-(9,11): wrong number of arguments (3 for 0...2) diff --git a/scenario/incremental/struct-new-block.rb b/scenario/incremental/struct-new-block.rb index 4f5e606ee..35547bf5a 100644 --- a/scenario/incremental/struct-new-block.rb +++ b/scenario/incremental/struct-new-block.rb @@ -26,7 +26,7 @@ def name: -> String def name=: (untyped) -> untyped def age: -> Integer def age=: (untyped) -> untyped - def self.[]: (String, Integer) -> Dog + def self.[]: (String, String) -> Dog def initialize: (String, String) -> void end diff --git a/scenario/misc/struct_new.rb b/scenario/misc/struct_new.rb index af3f587c1..67fb64e62 100644 --- a/scenario/misc/struct_new.rb +++ b/scenario/misc/struct_new.rb @@ -88,7 +88,7 @@ def x: -> Integer def x=: (untyped) -> untyped def y: -> Integer def y=: (untyped) -> untyped - def self.[]: (Integer, Integer) -> Pt + def self.[]: (?Integer, ?Integer) -> Pt def initialize: (?Integer, ?Integer) -> void end @@ -106,7 +106,7 @@ def name: -> String def name=: (untyped) -> untyped def age: -> Integer def age=: (untyped) -> untyped - def self.[]: (String, Integer) -> Dog + def self.[]: (String, String) -> Dog def initialize: (String, String) -> void end @@ -140,7 +140,7 @@ def x: -> Integer def x=: (untyped) -> untyped def y: -> Integer def y=: (untyped) -> untyped - def self.[]: (Integer, Integer) -> Pt + def self.[]: (?Integer, ?Integer) -> Pt end class Pt def initialize: (?Integer, ?Integer) -> void From b65ce979d4e58034b245ad3a0170096c23c5fcdb Mon Sep 17 00:00:00 2001 From: Takumi Shotoku Date: Sun, 27 Sep 2026 17:48:10 +0900 Subject: [PATCH 2/2] Make the members optional in the generated Struct initialize Struct.new(:x, :y).new and Struct.new(:x, :y)[] are valid, but TypeProf required every member as an argument and reported these calls as wrong number of arguments. The omitted members are not typed as nil, so the readers keep the types of the given values. --- lib/typeprof/core/ast/meta.rb | 7 ++++--- scenario/diagnostics/struct-optional-members.rb | 10 ++++++++++ scenario/incremental/struct-new-block.rb | 8 ++++---- scenario/misc/struct_new.rb | 12 ++++++------ 4 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 scenario/diagnostics/struct-optional-members.rb diff --git a/lib/typeprof/core/ast/meta.rb b/lib/typeprof/core/ast/meta.rb index af5488f24..f3c2ff69b 100644 --- a/lib/typeprof/core/ast/meta.rb +++ b/lib/typeprof/core/ast/meta.rb @@ -305,8 +305,9 @@ def subnodes = { block_body: } def attrs = { static_cpath:, members:, kind: } # Interface expected by MethodDefBox - def req_positionals = @kind == :struct ? @members : [] - def opt_positionals = [] + def req_positionals = [] + # Struct.new(:x).new is valid, so every member is optional + def opt_positionals = @kind == :struct ? @members : [] def rest_positionals = nil def post_positionals = [] def req_keywords = @kind == :data ? @members : [] @@ -397,7 +398,7 @@ def install0(genv) end init_ret = @changes.add_escape_box(genv, Source.new(genv.nil_type)) if @kind == :struct - init_f_args = FormalArguments.new(init_vtxs, [], nil, [], [], [], nil, nil) + init_f_args = FormalArguments.new([], init_vtxs, nil, [], [], [], nil, nil) else # Data.define uses keyword arguments init_f_args = FormalArguments.new([], [], nil, [], init_vtxs, [], nil, nil) diff --git a/scenario/diagnostics/struct-optional-members.rb b/scenario/diagnostics/struct-optional-members.rb new file mode 100644 index 000000000..edab19be3 --- /dev/null +++ b/scenario/diagnostics/struct-optional-members.rb @@ -0,0 +1,10 @@ +## update +Pt = Struct.new(:x, :y) + +Pt.new +Pt.new(1) +Pt[] +Pt[1, 2, 3] + +## diagnostics +(6,2)-(6,11): wrong number of arguments (3 for 0...2) diff --git a/scenario/incremental/struct-new-block.rb b/scenario/incremental/struct-new-block.rb index 35547bf5a..f34057d30 100644 --- a/scenario/incremental/struct-new-block.rb +++ b/scenario/incremental/struct-new-block.rb @@ -8,8 +8,8 @@ def name: -> String def name=: (untyped) -> untyped def age: -> Integer def age=: (untyped) -> untyped - def initialize: (String, Integer) -> void - def self.[]: (String, Integer) -> Dog + def initialize: (?String, ?Integer) -> void + def self.[]: (?String, ?Integer) -> Dog end ## update @@ -40,6 +40,6 @@ def name: -> String def name=: (untyped) -> untyped def age: -> Integer def age=: (untyped) -> untyped - def initialize: (String, Integer) -> void - def self.[]: (String, Integer) -> Dog + def initialize: (?String, ?Integer) -> void + def self.[]: (?String, ?Integer) -> Dog end diff --git a/scenario/misc/struct_new.rb b/scenario/misc/struct_new.rb index 67fb64e62..21df5dff6 100644 --- a/scenario/misc/struct_new.rb +++ b/scenario/misc/struct_new.rb @@ -12,8 +12,8 @@ def bar: -> Integer def bar=: (Integer) -> Integer def baz: -> String def baz=: (untyped) -> untyped - def initialize: (Integer, String) -> void - def self.[]: (Integer, String) -> Foo + def initialize: (?Integer, ?String) -> void + def self.[]: (?Integer, ?String) -> Foo end ## update @@ -41,8 +41,8 @@ def double class Bar def n: -> Integer def n=: (untyped) -> untyped - def initialize: (Integer) -> void - def self.[]: (Integer) -> Bar + def initialize: (?Integer) -> void + def self.[]: (?Integer) -> Bar def double: -> Integer end @@ -64,8 +64,8 @@ def ivar class Baz def v: -> Integer def v=: (untyped) -> untyped - def initialize: (Integer) -> void - def self.[]: (Integer) -> Baz + def initialize: (?Integer) -> void + def self.[]: (?Integer) -> Baz def set_label: -> String def ivar: -> String end