Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion lib/typeprof/core/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ def self.create_node(raw_node, lenv, use_result = true, allow_meta = false)
when :constant_read_node, :constant_path_node
ConstantReadNode.new(raw_node, lenv)
when :constant_write_node, :constant_path_write_node
ConstantWriteNode.new(raw_node, AST.create_node(raw_node.value, lenv), lenv)
if (members = detect_struct_new(raw_node.value))
StructNewNode.new(raw_node, members, :struct, lenv)
elsif (members = detect_data_define(raw_node.value))
StructNewNode.new(raw_node, members, :data, lenv)
else
ConstantWriteNode.new(raw_node, AST.create_node(raw_node.value, lenv), lenv)
end
when :constant_operator_write_node
read = ConstantReadNode.new(raw_node, lenv)
rhs = OperatorNode.new(raw_node, read, lenv)
Expand Down Expand Up @@ -526,5 +532,31 @@ def self.create_rbs_type(raw_decl, lenv)
raise "unknown RBS type: #{ raw_decl.class }"
end
end

def self.detect_struct_new(raw_value)
return nil unless raw_value.type == :call_node
return nil unless raw_value.name == :new
recv = raw_value.receiver
return nil unless recv&.type == :constant_read_node && recv.name == :Struct
extract_symbol_args(raw_value)
end

def self.detect_data_define(raw_value)
return nil unless raw_value.type == :call_node
return nil unless raw_value.name == :define
recv = raw_value.receiver
return nil unless recv&.type == :constant_read_node && recv.name == :Data
extract_symbol_args(raw_value)
end

def self.extract_symbol_args(raw_call)
return nil unless raw_call.arguments
members = []
raw_call.arguments.arguments.each do |arg|
return nil unless arg.type == :symbol_node
members << arg.value.to_sym
end
members
end
end
end
130 changes: 130 additions & 0 deletions lib/typeprof/core/ast/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,135 @@ def install0(genv)
Source.new
end
end

class StructNewNode < Node
def initialize(raw_node, members, kind, lenv)
super(raw_node, lenv)
case raw_node.type
when :constant_write_node
@static_cpath = lenv.cref.cpath + [raw_node.name]
when :constant_path_write_node
@static_cpath = AST.parse_cpath(raw_node.target, lenv.cref)
else
raise
end
@members = members
@kind = kind # :struct or :data

# Parse block body if present (Struct.new(:foo) do ... end)
raw_value = raw_node.value
if raw_value.block && raw_value.block.type == :block_node && raw_value.block.body
ncref = CRef.new(@static_cpath, :instance, nil, lenv.cref)
nlenv = LocalEnv.new(lenv.file_context, ncref, {}, [])
@block_body = AST.create_node(raw_value.block.body, nlenv)
end
end

attr_reader :static_cpath, :members, :kind, :block_body

def subnodes = { block_body: }
def attrs = { static_cpath:, members:, kind: }

# Interface expected by MethodDefBox
def req_positionals = @kind == :struct ? @members : []
def opt_positionals = []
def rest_positionals = nil
def post_positionals = []
def req_keywords = @kind == :data ? @members : []
def opt_keywords = []
def rest_keywords = nil
def no_keywords = @kind == :struct

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)
@members.each do |member|
ive = genv.resolve_ivar(@static_cpath, false, member)
ive.add_def(self)
end
@block_body.define(genv) if @block_body
cdef
end

def define_copy(genv)
mod = genv.resolve_cpath(@static_cpath)
mod.add_module_def(genv, self)
mod.remove_module_def(genv, @prev_node)
@members.each do |member|
ive = genv.resolve_ivar(@static_cpath, false, member)
ive.add_def(self)
ive.remove_def(@prev_node)
end
super(genv)
end

def undefine0(genv)
mod = genv.resolve_cpath(@static_cpath)
mod.remove_module_def(genv, self)
@members.each do |member|
ive = genv.resolve_ivar(@static_cpath, false, member)
ive.remove_def(self)
end
@block_body.undefine(genv) if @block_body
end

def install0(genv)
# Register the class singleton type as the constant value
mod_val = Source.new(Type::Singleton.new(genv, genv.resolve_cpath(@static_cpath)))
if @static_cpath
@changes.add_edge(genv, mod_val, @static_ret.vtx)
end

cpath = @static_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.
ivar_box = @changes.add_ivar_read_box(genv, cpath, false, member)
ret_box = @changes.add_escape_box(genv, ivar_box.ret)
@changes.add_method_def_box(genv, cpath, false, member, FormalArguments::Empty, [ret_box])

if @kind == :struct
# attr_writer (Struct only, Data is frozen)
ive = genv.resolve_ivar(cpath, false, member)
vtx = Vertex.new(self)
@changes.add_edge(genv, vtx, ive.vtx)
writer_ret = @changes.add_escape_box(genv, vtx)
f_args = FormalArguments.new([vtx], [], nil, [], [], [], nil, nil)
@changes.add_method_def_box(genv, cpath, false, :"#{ member }=", f_args, [writer_ret])
end
end

# initialize
init_vtxs = @members.map do |member|
ive = genv.resolve_ivar(cpath, false, member)
vtx = Vertex.new(self)
@changes.add_edge(genv, vtx, ive.vtx)
vtx
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)
else
# Data.define uses keyword arguments
init_f_args = FormalArguments.new([], [], nil, [], init_vtxs, [], nil, nil)
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
self_ret = @changes.add_escape_box(genv, Source.new(Type::Instance.new(genv, genv.resolve_cpath(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)
@block_body.install(genv)
end

mod_val
end
end
end
end
2 changes: 2 additions & 0 deletions lib/typeprof/core/env/module_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ def find_superclass_const_read
next
when AST::ModuleNode
return nil
when AST::StructNewNode
return [] # inherits from Object (Struct < Object)
else
raise
end
Expand Down
4 changes: 3 additions & 1 deletion lib/typeprof/core/graph/box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,9 @@ def show(output_parameter_names)
@f_args.post_positionals.each do |var|
args << Type.strip_parens(var.show)
end
if @node.is_a?(AST::DefNode)
if @node.respond_to?(:req_keywords) &&
@node.req_keywords.size == @f_args.req_keywords.size &&
@node.opt_keywords.size == @f_args.opt_keywords.size
@node.req_keywords.zip(@f_args.req_keywords) do |name, f_vtx|
args << "#{ name }: #{Type.strip_parens(f_vtx.show)}"
end
Expand Down
6 changes: 5 additions & 1 deletion lib/typeprof/core/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def dump_declarations(path)
out << " " * stack.size + "end"
end
end
when AST::ClassNode, AST::SingletonClassNode
when AST::ClassNode, AST::SingletonClassNode, AST::StructNewNode
if node.static_cpath
next if stack.any? { node.is_a?(AST::SingletonClassNode) && (_1.is_a?(AST::ClassNode) || _1.is_a?(AST::ModuleNode)) && node.static_cpath == _1.static_cpath }

Expand All @@ -496,6 +496,10 @@ def dump_declarations(path)
out << " " * stack.size + "include #{ inc_mod.show_cpath }"
end
end
# Output method definitions from meta nodes (StructNewNode etc.)
node.boxes(:mdef) do |mdef|
out << " " * stack.size + "def #{ mdef.singleton ? "self." : "" }#{ mdef.mid }: " + mdef.show(@options[:output_parameter_names])
end
else
stack.pop
out << " " * stack.size + "end"
Expand Down
14 changes: 14 additions & 0 deletions scenario/known-issues/struct-new-as-superclass.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## update
class Point < Struct.new(:x, :y)
end
Point.new(1, "hello").x

## assert
class Point < Struct[untyped]
def x: -> Integer
def y: -> String
def x=: (Integer) -> Integer
def y=: (untyped) -> untyped
def initialize: (Integer, String) -> void
def self.[]: (Integer, String) -> Point
end
71 changes: 71 additions & 0 deletions scenario/misc/struct_new.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
## update
Foo = Struct.new(:bar, :baz)
f = Foo.new(1, "hello")
f.bar
f.baz
f.bar = 2
g = Foo[3, "world"]

## assert
class Foo
def bar: -> Integer
def bar=: (Integer) -> Integer
def baz: -> String
def baz=: (untyped) -> untyped
def initialize: (Integer, String) -> void
def self.[]: (Integer, String) -> Foo
end

## update
Pt = Data.define(:x, :y)
p = Pt.new(x: 1, y: "hello")
p.x
p.y

## assert
class Pt
def x: -> Integer
def y: -> String
def initialize: (x: Integer, y: String) -> void
end

## update
Bar = Struct.new(:n) do
def double
n * 2
end
end
Bar.new(5).double

## assert
class Bar
def n: -> Integer
def n=: (untyped) -> untyped
def initialize: (Integer) -> void
def self.[]: (Integer) -> Bar
def double: -> Integer
end

## update
# The Struct member `v` is not a real Ruby ivar, so a user-written @v inside
# the block body must not share the member's type.
Baz = Struct.new(:v) do
def set_label
@v = "label"
end
def ivar
@v
end
end
Baz.new(42).v
Baz.new(42).ivar

## assert
class Baz
def v: -> Integer
def v=: (untyped) -> untyped
def initialize: (Integer) -> void
def self.[]: (Integer) -> Baz
def set_label: -> String
def ivar: -> String
end
Loading