module RBS
module Prototype
class RBI
attr_reader :decls
attr_reader :modules
attr_reader :last_sig
def initialize
@decls = []
@modules = []
end
def parse(string)
comments = Ripper.lex(string).yield_self do |tokens|
tokens.each.with_object({}) do |token, hash|
if token[1] == :on_comment
line = token[0][0]
body = token[2][2..-1]
body = "\n" if body.empty?
comment = AST::Comment.new(string: body, location: nil)
if (prev_comment = hash[line - 1])
hash[line - 1] = nil
hash[line] = AST::Comment.new(string: prev_comment.string + comment.string,
location: nil)
else
hash[line] = comment
end
end
end
end
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
end
end
def push_class(name, super_class, comment:)
modules.push AST::Declarations::Class.new(
name: nested_name(name),
super_class: super_class && AST::Declarations::Class::Super.new(name: const_to_name(super_class), args: [], location: nil),
type_params: AST::Declarations::ModuleTypeParams.empty,
members: [],
annotations: [],
location: nil,
comment: comment
)
decls << modules.last
yield
ensure
modules.pop
end
def push_module(name, comment:)
modules.push AST::Declarations::Module.new(
name: nested_name(name),
type_params: AST::Declarations::ModuleTypeParams.empty,
members: [],
annotations: [],
location: nil,
self_types: [],
comment: comment
)
decls << modules.last
yield
ensure
modules.pop
end
def current_module
modules.last
end
def push_sig(node)
@last_sig ||= []
@last_sig << node
end
def pop_sig
@last_sig.tap do
@last_sig = nil
end
end
def join_comments(nodes, comments)
cs = nodes.map {|node| comments[node.first_lineno - 1] }.compact
AST::Comment.new(string: cs.map(&:string).join("\n"), location: nil)
end
def process(node, outer: [], comments:)
case node.type
when :CLASS
comment = comments[node.first_lineno - 1]
push_class node.children[0], node.children[1], comment: comment do
process node.children[2], outer: outer + [node], comments: comments
end
when :MODULE
comment = comments[node.first_lineno - 1]
push_module node.children[0], comment: comment do
process node.children[1], outer: outer + [node], comments: comments
end
when :FCALL
case node.children[0]
when :include
each_arg node.children[1] do |arg|
if arg.type == :CONST || arg.type == :COLON2 || arg.type == :COLON3
name = const_to_name(arg)
include_member = AST::Members::Include.new(
name: name,
args: [],
annotations: [],
location: nil,
comment: nil
)
current_module.members << include_member
end
end
when :extend
each_arg node.children[1] do |arg|
if arg.type == :CONST || arg.type == :COLON2
name = const_to_name(arg)
unless name.to_s == "T::Generic" || name.to_s == "T::Sig"
member = AST::Members::Extend.new(
name: name,
args: [],
annotations: [],
location: nil,
comment: nil
)
current_module.members << member
end
end
end
when :sig
push_sig outer.last.children.last.children.last
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,
comment: nil
)
end
when :DEFS
sigs = pop_sig
if sigs
comment = join_comments(sigs, comments)
args = node.children[2]
types = sigs.map {|sig| method_type(args, sig, variables: current_module.type_params) }
current_module.members << AST::Members::MethodDefinition.new(
name: node.children[1],
location: nil,
annotations: [],
types: types,
kind: :singleton,
comment: comment,
overload: false
)
end
when :DEFN
sigs = pop_sig
if sigs
comment = join_comments(sigs, comments)
args = node.children[1]
types = sigs.map {|sig| method_type(args, sig, variables: current_module.type_params) }
current_module.members << AST::Members::MethodDefinition.new(
name: node.children[0],
location: nil,
annotations: [],
types: types,
kind: :instance,
comment: comment,
overload: false
)
end
when :CDECL
if (send = node.children.last) && send.type == :FCALL && send.children[0] == :type_member
unless each_arg(send.children[1]).any? {|node|
node.type == :HASH &&
each_arg(node.children[0]).each_slice(2).any? {|a, _| a.type == :LIT && a.children[0] == :fixed }
}
if (a0 = each_arg(send.children[1]).to_a[0])&.type == :LIT
variance = case a0.children[0]
when :out
:covariant
when :in
:contravariant
end
end
current_module.type_params.add(
AST::Declarations::ModuleTypeParams::TypeParam.new(name: node.children[0],
variance: variance || :invariant,
skip_validation: false))
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
end
value_node = node.children.last
type = if value_node.type == :CALL && value_node.children[1] == :let
type_node = each_arg(value_node.children[2]).to_a[1]
type_of type_node, variables: current_module&.type_params || []
else
Types::Bases::Any.new(location: nil)
end
decls << AST::Declarations::Constant.new(
name: name,
type: type,
location: nil,
comment: nil
)
end
when :ALIAS
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,
comment: nil
)
else
each_child node do |child|
process child, outer: outer + [node], comments: comments
end
end
end
def method_type(args_node, type_node, variables:)
if type_node
if type_node.type == :CALL
method_type = method_type(args_node, type_node.children[0], variables: variables)
else
method_type = MethodType.new(
type: Types::Function.empty(Types::Bases::Any.new(location: nil)),
block: nil,
location: nil,
type_params: []
)
end
name, args = case type_node.type
when :CALL
[
type_node.children[1],
type_node.children[2]
]
when :FCALL, :VCALL
[
type_node.children[0],
type_node.children[1]
]
end
case name
when :returns
return_type = each_arg(args).to_a[0]
method_type.update(type: method_type.type.with_return_type(type_of(return_type, variables: variables)))
when :params
if args_node
parse_params(args_node, args, method_type, variables: variables)
else
vars = (node_to_hash(each_arg(args).to_a[0]) || {}).transform_values {|value| type_of(value, variables: variables) }
required_positionals = vars.map do |name, type|
Types::Function::Param.new(name: name, type: type)
end
method_type.update(type: method_type.type.update(required_positionals: required_positionals))
end
when :type_parameters
type_params = []
each_arg args do |node|
if node.type == :LIT
type_params << node.children[0]
end
end
method_type.update(type_params: type_params)
when :void
method_type.update(type: method_type.type.with_return_type(Types::Bases::Void.new(location: nil)))
when :proc
method_type
else
method_type
end
end
end
def parse_params(args_node, args, method_type, variables:)
vars = (node_to_hash(each_arg(args).to_a[0]) || {}).transform_values {|value| type_of(value, variables: variables) }
required_positionals = []
optional_positionals = []
rest_positionals = nil
trailing_positionals = []
required_keywords = {}
optional_keywords = {}
rest_keywords = nil
var_names = args_node.children[0]
pre_num, _pre_init, opt, _first_post, post_num, _post_init, rest, kw, kwrest, block = args_node.children[1].children
pre_num.times.each do |i|
name = var_names[i]
type = vars[name] || Types::Bases::Any.new(location: nil)
required_positionals << Types::Function::Param.new(type: type, name: name)
end
index = pre_num
while opt
name = var_names[index]
if (type = vars[name])
optional_positionals << Types::Function::Param.new(type: type, name: name)
end
index += 1
opt = opt.children[1]
end
if rest
name = var_names[index]
if (type = vars[name])
rest_positionals = Types::Function::Param.new(type: type, name: name)
end
index += 1
end
post_num.times do |i|
name = var_names[i+index]
if (type = vars[name])
trailing_positionals << Types::Function::Param.new(type: type, name: name)
end
index += 1
end
while kw
name, value = kw.children[0].children
if (type = vars[name])
if value
optional_keywords[name] = Types::Function::Param.new(type: type, name: name)
else
required_keywords[name] = Types::Function::Param.new(type: type, name: name)
end
end
kw = kw.children[1]
end
if kwrest
name = kwrest.children[0]
if (type = vars[name])
rest_keywords = Types::Function::Param.new(type: type, name: name)
end
end
method_block = nil
if block
if (type = vars[block])
if type.is_a?(Types::Proc)
method_block = Types::Block.new(required: true, type: type.type)
elsif type.is_a?(Types::Bases::Any)
method_block = Types::Block.new(
required: true,
type: Types::Function.empty(Types::Bases::Any.new(location: nil))
)
# Handle an optional block like `T.nilable(T.proc.void)`.
elsif type.is_a?(Types::Optional) && type.type.is_a?(Types::Proc)
method_block = Types::Block.new(required: false, type: type.type.type)
else
STDERR.puts "Unexpected block type: #{type}"
PP.pp args_node, STDERR
method_block = Types::Block.new(
required: true,
type: Types::Function.empty(Types::Bases::Any.new(location: nil))
)
end
end
end
method_type.update(
type: method_type.type.update(
required_positionals: required_positionals,
optional_positionals: optional_positionals,
rest_positionals: rest_positionals,
trailing_positionals: trailing_positionals,
required_keywords: required_keywords,
optional_keywords: optional_keywords,
rest_keywords: rest_keywords
),
block: method_block
)
end
def type_of(type_node, variables:)
type = type_of0(type_node, variables: 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"
Types::Bases::Bool.new(location: nil)
else
type
end
end
def type_of0(type_node, variables:)
case
when type_node.type == :CONST
if variables.each.include?(type_node.children[0])
Types::Variable.new(name: type_node.children[0], location: nil)
else
Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil)
end
when type_node.type == :COLON2
Types::ClassInstance.new(name: const_to_name(type_node), args: [], location: nil)
when call_node?(type_node, name: :[], receiver: -> (_) { true })
type = type_of(type_node.children[0], variables: variables)
each_arg(type_node.children[2]) do |arg|
type.args << type_of(arg, variables: variables)
end
type
when call_node?(type_node, name: :type_parameter)
name = each_arg(type_node.children[2]).to_a[0].children[0]
Types::Variable.new(name: name, location: nil)
when call_node?(type_node, name: :any)
types = each_arg(type_node.children[2]).to_a.map {|node| type_of(node, variables: variables) }
Types::Union.new(types: types, location: nil)
when call_node?(type_node, name: :all)
types = each_arg(type_node.children[2]).to_a.map {|node| type_of(node, variables: variables) }
Types::Intersection.new(types: types, location: nil)
when call_node?(type_node, name: :untyped)
Types::Bases::Any.new(location: nil)
when call_node?(type_node, name: :nilable)
type = type_of each_arg(type_node.children[2]).to_a[0], variables: variables
Types::Optional.new(type: type, location: nil)
when call_node?(type_node, name: :self_type)
Types::Bases::Self.new(location: nil)
when call_node?(type_node, name: :attached_class)
Types::Bases::Instance.new(location: nil)
when call_node?(type_node, name: :noreturn)
Types::Bases::Bottom.new(location: nil)
when call_node?(type_node, name: :class_of)
type = type_of each_arg(type_node.children[2]).to_a[0], variables: variables
case type
when Types::ClassInstance
Types::ClassSingleton.new(name: type.name, location: nil)
else
STDERR.puts "Unexpected type for `class_of`: #{type}"
Types::Bases::Any.new(location: nil)
end
when type_node.type == :ARRAY, type_node.type == :LIST
types = each_arg(type_node).map {|node| type_of(node, variables: variables) }
Types::Tuple.new(types: types, location: nil)
else
if proc_type?(type_node)
Types::Proc.new(type: method_type(nil, type_node, variables: variables).type, block: nil, location: nil)
else
STDERR.puts "Unexpected type_node:"
PP.pp type_node, STDERR
Types::Bases::Any.new(location: nil)
end
end
end
def proc_type?(type_node)
if call_node?(type_node, name: :proc)
true
else
type_node.type == :CALL && proc_type?(type_node.children[0])
end
end
def call_node?(node, name:, receiver: -> (node) { node.type == :CONST && node.children[0] == :T }, args: -> (node) { true })
node.type == :CALL && receiver[node.children[0]] && name == node.children[1] && args[node.children[2]]
end
def const_to_name(node)
case node.type
when :CONST
TypeName.new(name: node.children[0], namespace: Namespace.empty)
when :COLON2
if node.children[0]
if node.children[0].type == :COLON3
namespace = Namespace.root
else
namespace = const_to_name(node.children[0]).to_namespace
end
else
namespace = Namespace.empty
end
type_name = TypeName.new(name: node.children[1], namespace: namespace)
case type_name.to_s
when "T::Array"
BuiltinNames::Array.name
when "T::Hash"
BuiltinNames::Hash.name
when "T::Range"
BuiltinNames::Range.name
when "T::Enumerator"
BuiltinNames::Enumerator.name
when "T::Enumerable"
BuiltinNames::Enumerable.name
when "T::Set"
BuiltinNames::Set.name
else
type_name
end
when :COLON3
TypeName.new(name: node.children[0], namespace: Namespace.root)
else
raise "Unexpected node type: #{node.type}"
end
end
def each_arg(array, &block)
if block_given?
if array&.type == :ARRAY || array&.type == :LIST
array.children.each do |arg|
if arg
yield arg
end
end
end
else
enum_for :each_arg, array
end
end
def each_child(node)
node.children.each do |child|
if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
yield child
end
end
end
def node_to_hash(node)
if node&.type == :HASH
hash = {}
each_arg(node.children[0]).each_slice(2) do |var, type|
if var.type == :LIT && type
hash[var.children[0]] = type
end
end
hash
end
end
end
end
end