123456789_123456789_123456789_123456789_123456789_

Class: RBS::Prototype::RBI

Relationships & Source Files
Inherits: Object
Defined in: lib/rbs/prototype/rbi.rb

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.newRBI

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 8

def initialize
  @decls = []

  @modules = []
end

Instance Attribute Details

#decls (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 4

attr_reader :decls

#last_sig (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 6

attr_reader :last_sig

#modules (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 5

attr_reader :modules

Instance Method Details

#call_node?(node, name:, receiver: -> (node) { node.type == :CONST && node.children[0] == :T }, args: -> (node) { true }) ⇒ Boolean

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 505

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

#const_to_name(node)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 509

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

#current_module

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 83

def current_module
  modules.last
end

#current_namespace

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 41

def current_namespace
  modules.inject(Namespace.empty) do |parent, mod|
    parent + mod.name.to_namespace
  end
end

#each_arg(array, &block)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 549

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

#each_child(node)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 563

def each_child(node)
  node.children.each do |child|
    if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
      yield child
    end
  end
end

#join_comments(nodes, comments)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 98

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

#method_type(args_node, type_node, variables:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 258

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

#nested_name(name)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 37

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

#node_to_hash(node)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 571

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

#parse(string)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 14

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

#parse_params(args_node, args, method_type, variables:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 320

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

#pop_sig

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 92

def pop_sig
  @last_sig.tap do
    @last_sig = nil
  end
end

#proc_type?(type_node) ⇒ Boolean

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 497

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

#process(node, outer: [], comments:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 103

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

#push_class(name, super_class, comment:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 47

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

#push_module(name, comment:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 65

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

#push_sig(node)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 87

def push_sig(node)
  @last_sig ||= []
  @last_sig << node
end

#type_of(type_node, variables:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 424

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

#type_of0(type_node, variables:)

[ GitHub ]

  
# File 'lib/rbs/prototype/rbi.rb', line 437

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