Skip to content
Open
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
204 changes: 180 additions & 24 deletions lib/rbs/prototype/rbi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,40 @@ class RBI
attr_reader :modules
attr_reader :last_sig

class Context
attr_accessor :singleton
attr_accessor :visibility

def initialize(singleton:, visibility:)
@singleton = singleton
@visibility = visibility
end
end

def initialize
@decls = []

@modules = []
@contexts = []
@emitted_visibility = {}
end

def parse(string)
comments = parse_comments(string, include_trailing: true)
process RubyVM::AbstractSyntaxTree.parse(string), comments: comments
end

def nested_name(name)
(current_namespace + const_to_name(name).to_namespace).to_type_name.relative!
end

def current_namespace
modules.inject(Namespace.empty) do |parent, mod|
parent + mod.name.to_namespace
def append_decl(decl)
if mod = current_module
mod.members << decl
else
decls << decl
end
end

def push_class(name, super_class, comment:)
class_decl = AST::Declarations::Class.new(
name: nested_name(name),
name: const_to_name(name),
super_class: super_class && AST::Declarations::Class::Super.new(name: const_to_name(super_class), args: [], location: nil),
type_params: [],
members: [],
Expand All @@ -41,17 +51,20 @@ def push_class(name, super_class, comment:)
comment: comment
)

append_decl class_decl
modules << class_decl
decls << class_decl
@contexts << Context.new(singleton: false, visibility: :public)
@emitted_visibility[class_decl.object_id] = :public

yield
ensure
@contexts.pop
modules.pop
end

def push_module(name, comment:)
module_decl = AST::Declarations::Module.new(
name: nested_name(name),
name: const_to_name(name),
type_params: [],
members: [],
annotations: [],
Expand All @@ -60,11 +73,14 @@ def push_module(name, comment:)
comment: comment
)

append_decl module_decl
modules << module_decl
decls << module_decl
@contexts << Context.new(singleton: false, visibility: :public)
@emitted_visibility[module_decl.object_id] = :public

yield
ensure
@contexts.pop
modules.pop
end

Expand All @@ -76,6 +92,34 @@ def current_module!
current_module or raise
end

def current_context
@contexts.last
end

def current_context!
current_context or raise
end

def sync_visibility(visibility)
# RBS has no protected visibility. Private is the conservative fallback.
visibility = :private if visibility == :protected

mod = current_module!
return if @emitted_visibility[mod.object_id] == visibility

member = case visibility
when :public
AST::Members::Public.new(location: nil)
when :private
AST::Members::Private.new(location: nil)
else
raise "Unexpected visibility: #{visibility}"
end

mod.members << member
@emitted_visibility[mod.object_id] = visibility
end

def push_sig(node)
if last_sig = @last_sig
last_sig << node
Expand Down Expand Up @@ -107,6 +151,15 @@ def process(node, outer: [], comments:)
push_module node.children[0], comment: comment do
process node.children[1], outer: outer + [node], comments: comments
end
when :SCLASS
if node.children[0].type == :SELF
@contexts << Context.new(singleton: true, visibility: :public)
begin
process node.children[1], outer: outer + [node], comments: comments
ensure
@contexts.pop
end
end
when :FCALL
case node.children[0]
when :include
Expand All @@ -125,9 +178,9 @@ def process(node, outer: [], comments:)
end
when :extend
each_arg node.children[1] do |arg|
if arg.type == :CONST || arg.type == :COLON2
if arg.type == :CONST || arg.type == :COLON2 || arg.type == :COLON3
name = const_to_name(arg)
unless name.to_s == "T::Generic" || name.to_s == "T::Sig"
unless ["T::Generic", "T::Helpers", "T::Sig"].include?(name.to_s.delete_prefix("::"))
member = AST::Members::Extend.new(
name: name,
args: [],
Expand All @@ -142,21 +195,31 @@ def process(node, outer: [], comments:)
when :sig
out = outer.last or raise
push_sig out.children.last.children.last
when :attr_reader, :attr_writer, :attr_accessor
process_attribute node, comments: comments
when :private, :protected, :public
process_visibility node, outer: outer, comments: comments
when :alias_method
new, old = each_arg(node.children[1]).map {|x| x.children[0] }
current_module!.members << AST::Members::Alias.new(
new_name: new,
old_name: old,
location: nil,
annotations: [],
kind: :instance,
kind: current_context!.singleton ? :singleton : :instance,
comment: nil
)
end
when :VCALL
case node.children[0]
when :private, :protected, :public
current_context!.visibility = node.children[0]
end
when :DEFS
sigs = pop_sig

if sigs
sync_visibility(:public)
comment = join_comments(sigs, comments)

args = node.children[2]
Expand All @@ -178,6 +241,8 @@ def process(node, outer: [], comments:)
sigs = pop_sig

if sigs
context = current_context!
sync_visibility(context.visibility)
comment = join_comments(sigs, comments)

args = node.children[1]
Expand All @@ -188,7 +253,7 @@ def process(node, outer: [], comments:)
location: nil,
annotations: [],
overloads: types.map {|type| AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: type) },
kind: :instance,
kind: context.singleton ? :singleton : :instance,
comment: comment,
overloading: false,
visibility: nil
Expand Down Expand Up @@ -222,11 +287,7 @@ def process(node, outer: [], comments:)
end
else
name = node.children[0].yield_self do |n|
if n.is_a?(Symbol)
TypeName.new(namespace: current_namespace, name: n)
else
const_to_name(n)
end
n.is_a?(Symbol) ? TypeName.new(namespace: Namespace.empty, name: n) : const_to_name(n)
end
value_node = node.children.last
type = if value_node && value_node.type == :CALL && value_node.children[1] == :let
Expand All @@ -235,7 +296,7 @@ def process(node, outer: [], comments:)
else
Types::Bases::Any.new(location: nil)
end
decls << AST::Declarations::Constant.new(
append_decl AST::Declarations::Constant.new(
name: name,
type: type,
location: nil,
Expand All @@ -244,12 +305,13 @@ def process(node, outer: [], comments:)
)
end
when :ALIAS
sync_visibility(current_context!.visibility)
current_module!.members << AST::Members::Alias.new(
new_name: node.children[0].children[0],
old_name: node.children[1].children[0],
location: nil,
annotations: [],
kind: :instance,
kind: current_context!.singleton ? :singleton : :instance,
comment: nil
)
else
Expand All @@ -259,6 +321,93 @@ def process(node, outer: [], comments:)
end
end

def process_visibility(node, outer:, comments:)
visibility = node.children[0]
args = each_arg(node.children[1]).to_a
context = current_context!

if args.empty?
context.visibility = visibility
else
previous_visibility = context.visibility
context.visibility = visibility

begin
args.each do |arg|
if arg.type == :DEFN || arg.type == :DEFS
process arg, outer: outer + [node], comments: comments
end
end
ensure
context.visibility = previous_visibility
end
end
end

def process_attribute(node, comments:)
sigs = pop_sig
kind = node.children[0]
context = current_context!
sync_visibility(context.visibility)

type = attribute_type(kind, sigs)
comment = join_comments(sigs, comments) if sigs
member_class = case kind
when :attr_reader
AST::Members::AttrReader
when :attr_writer
AST::Members::AttrWriter
when :attr_accessor
AST::Members::AttrAccessor
else
raise "Unexpected attribute kind: #{kind}"
end

each_arg node.children[1] do |arg|
if name = symbol_literal_node?(arg)
current_module!.members << member_class.new(
name: name,
type: type,
ivar_name: nil,
kind: context.singleton ? :singleton : :instance,
annotations: [],
location: nil,
comment: comment,
visibility: nil
)
end
end
end

def attribute_type(kind, sigs)
any = Types::Bases::Any.new(location: nil)
return any unless sigs

method_types = sigs.filter_map do |sig|
method_type(nil, sig, variables: current_module!.type_params, overloads: sigs.size)
end
function = method_types.last&.type
return any unless function.is_a?(Types::Function)

parameter_type = function.required_positionals.first&.type
return_type = function.return_type

case kind
when :attr_reader
return_type
when :attr_writer
parameter_type || return_type
when :attr_accessor
if return_type.is_a?(Types::Bases::Any) || return_type.is_a?(Types::Bases::Void)
parameter_type || any
else
return_type
end
else
any
end
end

def method_type(args_node, type_node, variables:, overloads:)
if type_node
if type_node.type == :CALL
Expand Down Expand Up @@ -463,8 +612,10 @@ def type_of(type_node, variables:)
case
when type.is_a?(Types::ClassInstance) && type.name.name == BuiltinNames::BasicObject.name.name
Types::Bases::Any.new(location: nil)
when type.is_a?(Types::ClassInstance) && type.name.to_s == "T::Boolean"
when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Boolean"
Types::Bases::Bool.new(location: nil)
when type.is_a?(Types::ClassInstance) && type.name.to_s.delete_prefix("::") == "T::Class"
Types::Bases::Any.new(location: nil)
else
type
end
Expand All @@ -482,6 +633,11 @@ def type_of0(type_node, variables:)
Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil)
when call_node?(type_node, name: :[], receiver: -> (_) { true })
# The type_node represents a type application
receiver = type_node.children[0]
if [:CONST, :COLON2, :COLON3].include?(receiver.type) && const_to_name(receiver).to_s.delete_prefix("::") == "T::Class"
return Types::Bases::Any.new(location: nil)
end

type = type_of(type_node.children[0], variables: variables)
type.is_a?(Types::ClassInstance) or raise

Expand Down Expand Up @@ -559,7 +715,7 @@ def const_to_name(node)

type_name = TypeName.new(name: node.children[1], namespace: namespace)

case type_name.to_s
case type_name.to_s.delete_prefix("::")
when "T::Array"
BuiltinNames::Array.name
when "T::Hash"
Expand Down
Loading
Loading