diff --git a/lib/typeprof/core/ast/meta.rb b/lib/typeprof/core/ast/meta.rb index b7f0b12d..600c9d20 100644 --- a/lib/typeprof/core/ast/meta.rb +++ b/lib/typeprof/core/ast/meta.rb @@ -250,25 +250,14 @@ def initialize(raw_node, members, kind, lenv) attr_reader :static_cpath, :members, :kind, :block_body + # Not a valid constant name, so no Ruby code can refer to it + BASE_CNAME = :"" + + def struct_base_cpath = @static_cpath + [BASE_CNAME] + def subnodes = { block_body: } def attrs = { static_cpath:, members:, kind: } - # A method written in the block body (e.g. a custom initialize) - # overrides the auto-generated one, which should then be hidden - # from the RBS output. - def block_defines_method?(singleton, mid) - return false unless @block_body - found = false - @block_body.traverse do |event, node| - if event == :enter && node.is_a?(DefNode) && - node.singleton == singleton && node.mid == mid && - node.lenv.cref.cpath == @static_cpath - found = true - end - end - found - end - # Interface expected by MethodDefBox def req_positionals = @kind == :struct ? @members : [] def opt_positionals = [] @@ -283,8 +272,9 @@ def define0(genv) mod = genv.resolve_cpath(@static_cpath) # add_module_def internally calls get_const(name).add_def(self) cdef = mod.add_module_def(genv, self) + genv.resolve_cpath(struct_base_cpath).add_module_def(genv, self) @members.each do |member| - ive = genv.resolve_ivar(@static_cpath, false, member) + ive = genv.resolve_ivar(struct_base_cpath, false, member) ive.add_def(self) end @block_body.define(genv) if @block_body @@ -295,8 +285,11 @@ def define_copy(genv) mod = genv.resolve_cpath(@static_cpath) mod.add_module_def(genv, self) mod.remove_module_def(genv, @prev_node) + base = genv.resolve_cpath(struct_base_cpath) + base.add_module_def(genv, self) + base.remove_module_def(genv, @prev_node) @members.each do |member| - ive = genv.resolve_ivar(@static_cpath, false, member) + ive = genv.resolve_ivar(struct_base_cpath, false, member) ive.add_def(self) ive.remove_def(@prev_node) end @@ -306,8 +299,9 @@ 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) @members.each do |member| - ive = genv.resolve_ivar(@static_cpath, false, member) + ive = genv.resolve_ivar(struct_base_cpath, false, member) ive.remove_def(self) end @block_body.undefine(genv) if @block_body @@ -320,7 +314,7 @@ def install0(genv) @changes.add_edge(genv, mod_val, @static_ret.vtx) end - cpath = @static_cpath + cpath = struct_base_cpath @members.each do |member| # Use bare `:member` (not `:@member`) so the slot can't collide with a # user-written @member ivar — Struct/Data fields are not real ivars. @@ -357,7 +351,8 @@ def install0(genv) # Struct.[] is an alias for Struct.new if @kind == :struct - self_ret = @changes.add_escape_box(genv, Source.new(Type::Instance.new(genv, genv.resolve_cpath(cpath), []))) + # 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 diff --git a/lib/typeprof/core/env/module_entity.rb b/lib/typeprof/core/env/module_entity.rb index 3d5c9a45..016023f0 100644 --- a/lib/typeprof/core/env/module_entity.rb +++ b/lib/typeprof/core/env/module_entity.rb @@ -285,7 +285,8 @@ def find_superclass_const_read when AST::ModuleNode return nil when AST::StructNewNode - return [] # inherits from Object (Struct < Object) + base_cpath = mdef.struct_base_cpath + return base_cpath == @cpath ? [] : base_cpath else raise end diff --git a/lib/typeprof/core/service.rb b/lib/typeprof/core/service.rb index 52d112e3..97c71fc9 100644 --- a/lib/typeprof/core/service.rb +++ b/lib/typeprof/core/service.rb @@ -405,7 +405,9 @@ def completion(path, trigger, pos) break end end - yield mid, "#{ mod.cpath.join("::" )}#{ singleton ? "." : "#" }#{ mid } : #{ sig }" if sig + # The base class has no name to show + owner = mod.cpath.last == AST::StructNewNode::BASE_CNAME ? base_ty.mod : mod + yield mid, "#{ owner.cpath.join("::" )}#{ singleton ? "." : "#" }#{ mid } : #{ sig }" if sig end end end @@ -481,6 +483,10 @@ def dump_declarations(path) s = "class #{ format_declared_const_path(node.static_cpath, stack) }" mod = @genv.resolve_cpath(node.static_cpath) superclass = mod.superclass + # The base class is not shown; its members are printed inline + while superclass && superclass.cpath.last == AST::StructNewNode::BASE_CNAME + superclass = superclass.superclass + end if superclass == nil s << " # failed to identify its superclass" elsif superclass.cpath != [] @@ -499,7 +505,9 @@ def dump_declarations(path) end # Output method definitions from meta nodes (StructNewNode etc.) node.boxes(:mdef) do |mdef| - next if node.is_a?(AST::StructNewNode) && node.block_defines_method?(mdef.singleton, mdef.mid) + # A user-written method overrides the generated one + next if node.is_a?(AST::StructNewNode) && + !@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 else diff --git a/scenario/incremental/struct-new-block.rb b/scenario/incremental/struct-new-block.rb new file mode 100644 index 00000000..4f5e606e --- /dev/null +++ b/scenario/incremental/struct-new-block.rb @@ -0,0 +1,45 @@ +## update +Dog = Struct.new(:name, :age) +Dog.new("fred", 5) + +## assert +class Dog + def name: -> String + def name=: (untyped) -> untyped + def age: -> Integer + def age=: (untyped) -> untyped + def initialize: (String, Integer) -> void + def self.[]: (String, Integer) -> Dog +end + +## update +Dog = Struct.new(:name, :age) do + def initialize(name, age) + super(name.to_s, age.to_i) + end +end +Dog.new("fred", "5") + +## assert +class Dog + def name: -> String + def name=: (untyped) -> untyped + def age: -> Integer + def age=: (untyped) -> untyped + def self.[]: (String, Integer) -> Dog + def initialize: (String, String) -> void +end + +## update +Dog = Struct.new(:name, :age) +Dog.new("fred", 5) + +## assert +class Dog + def name: -> String + def name=: (untyped) -> untyped + def age: -> Integer + def age=: (untyped) -> untyped + 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 bef9f469..af3f587c 100644 --- a/scenario/misc/struct_new.rb +++ b/scenario/misc/struct_new.rb @@ -91,3 +91,57 @@ def y=: (untyped) -> untyped def self.[]: (Integer, Integer) -> Pt def initialize: (?Integer, ?Integer) -> void end + +## update +Dog = Struct.new(:name, :age) do + def initialize(name, age) + super(name.to_s, age.to_i) + end +end +Dog.new("fred", "5") + +## assert +class Dog + def name: -> String + def name=: (untyped) -> untyped + def age: -> Integer + def age=: (untyped) -> untyped + def self.[]: (String, Integer) -> Dog + def initialize: (String, String) -> void +end + +## update +D = Data.define(:name, :age) do + def initialize(name:, age:) + super(name: name.to_s, age: age.to_i) + end +end +D.new(name: "fred", age: "5") + +## assert +class D + def name: -> String + def age: -> Integer + def initialize: (name: String, age: String) -> void +end + +## update +Pt = Struct.new(:x, :y) +class Pt + def initialize(x = 0, y = 0) + super + end +end +Pt.new(3, 4) + +## assert +class Pt + def x: -> Integer + def x=: (untyped) -> untyped + def y: -> Integer + def y=: (untyped) -> untyped + def self.[]: (Integer, Integer) -> Pt +end +class Pt + def initialize: (?Integer, ?Integer) -> void +end