123456789_123456789_123456789_123456789_123456789_

Class: RBS::Prototype::Runtime

Relationships & Source Files
Namespace Children
Modules:
Classes:
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
self, Helpers, Helpers
Inherits: Object
Defined in: lib/rbs/prototype/runtime.rb,
lib/rbs/prototype/runtime/helpers.rb,
lib/rbs/prototype/runtime/reflection.rb,
lib/rbs/prototype/runtime/value_object_generator.rb

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(patterns:, env:, merge:, todo: false, owners_included: []) ⇒ Runtime

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 70

def initialize(patterns:, env:, merge:, todo: false, owners_included: [])
  @patterns = patterns
  @decls = nil
  @modules = {}
  @env = env
  @merge = merge
  @owners_included = owners_included.map do |name|
    Object.const_get(name)
  end
  @outline = false
  @todo = todo
end

Instance Attribute Details

#env (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 64

attr_reader :env

#merge (readonly)

[ GitHub ]

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

attr_reader :merge

#outline (rw)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 68

attr_accessor :outline

#owners_included (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 67

attr_reader :owners_included

#patterns (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 63

attr_reader :patterns

#todo (readonly)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 66

attr_reader :todo

Instance Method Details

#block_from_ast_of(method)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 650

def block_from_ast_of(method)
  return nil if RUBY_VERSION < '3.1'

  begin
    ast = RubyVM::AbstractSyntaxTree.of(method)
  rescue ArgumentError
    return # When the method is defined in eval
  rescue RuntimeError => error
    raise unless error.message.include?("prism")
    return # When the method was compiled by prism
  end

  if ast && ast.type == :SCOPE
    block_from_body(ast)
  end
end

#builder

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 100

def builder
  @builder ||= DefinitionBuilder.new(env: env)
end

#can_alias?(mod, method) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 412

private def can_alias?(mod, method)
  return false if method.name == method.original_name

  begin
    mod.instance_method(method.original_name) && true
  rescue NameError
    false
  end
end

#decls

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 108

def decls
  unless @decls
    @decls = []
    @modules = ObjectSpace.each_object(Module)
      .map { |mod| [const_name(mod), mod] }
      .select { |name, _| name }
      .to_h
    @modules.select { |name, mod| target?(mod) }.sort_by { |name, _| name }.each do |_, mod|
      case mod
      when Class
        generate_class mod
      when Module
        generate_module mod
      end
    end
  end

  @decls or raise
end

#each_mixined_module(type_name, mod) (private)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 128

private def each_mixined_module(type_name, mod)
  each_mixined_module_one(type_name, mod) do |module_name, module_full_name, is_prepend|
    yield module_name, module_full_name, is_prepend ? AST::Members::Prepend : AST::Members::Include
  end
  each_mixined_module_one(type_name, mod.singleton_class) do |module_name, module_full_name, _|
    yield module_name, module_full_name, AST::Members::Extend
  end
end

#each_mixined_module_one(type_name, mod) (private)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 137

private def each_mixined_module_one(type_name, mod)
  supers = Set[] #: Set[Module]
  prepends = mod.ancestors.take_while { |m| !mod.equal?(m) }.to_set

  mod.included_modules.each do |mix|
    supers.merge(mix.included_modules)
  end

  if mod.is_a?(Class)
    if superclass = mod.superclass
      superclass.included_modules.each do |mix|
        supers << mix
        supers.merge(mix.included_modules)
      end
    end
  end

  mod.included_modules.uniq.each do |mix|
    if !supers.include?(mix) || prepends.include?(mix)
      unless const_name(mix)
        RBS.logger.warn("Skipping anonymous module #{mix} included in #{mod}")
      else
        module_name = module_full_name = to_type_name(const_name!(mix), full_name: true)
        if module_full_name.namespace == type_name.namespace
          module_name = TypeName.new(name: module_full_name.name, namespace: Namespace.empty)
        end

        yield module_name, module_full_name, prepends.include?(mix)
      end
    end
  end
end

#ensure_outer_module_declarations(mod)

Generate/find outer module declarations This is broken down into another method to comply with DRY This generates/finds declarations in nested form & returns the last array of declarations

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 579

def ensure_outer_module_declarations(mod)
  # @type var outer_module_names: Array[String]
  *outer_module_names, _ = const_name!(mod).split(/::/) #=> parent = [A, B], mod = C
  # Copy the entries in ivar @decls, not .dup
  destination = @decls || [] #: Array[AST::Declarations::Class::member]

  outer_module_names&.each_with_index do |outer_module_name, i|
    current_name = outer_module_names.take(i+1).join('::')
    outer_module = @modules[current_name]
    outer_decl = destination.detect do |decl|
      case outer_module
      when Class
        decl.is_a?(AST::Declarations::Class) && decl.name.name == outer_module_name.to_sym
      when Module
        decl.is_a?(AST::Declarations::Module) && decl.name.name == outer_module_name.to_sym
      end
    end #: AST::Declarations::Class | AST::Declarations::Module | nil

    # Insert AST::Declarations if declarations are not added previously
    unless outer_decl
      outer_module or raise

      if outer_module.is_a?(Class)
        outer_decl = AST::Declarations::Class.new(
          name: to_type_name(outer_module_name),
          type_params: type_params(outer_module),
          super_class: generate_super_class(outer_module),
          members: [],
          annotations: [],
          location: nil,
          comment: nil
        )
      else
        outer_decl = AST::Declarations::Module.new(
          name: to_type_name(outer_module_name),
          type_params: type_params(outer_module),
          self_types: [],
          members: [],
          annotations: [],
          location: nil,
          comment: nil
        )
      end

      destination << outer_decl
    end

    destination = outer_decl.members
  end

  # Return the array of declarations checked out at the end
  destination
end

#generate_class(mod)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 484

def generate_class(mod)
  type_name_absolute = to_type_name(const_name!(mod), full_name: true).absolute!
  type_name = to_type_name(const_name!(mod))
  outer_decls = ensure_outer_module_declarations(mod)

  # Check if a declaration exists for the actual module
  decl = outer_decls.detect do |decl|
    decl.is_a?(AST::Declarations::Class) && decl.name.name == only_name(mod).to_sym
  end #: AST::Declarations::Class?

  unless decl
    if StructGenerator.generatable?(mod)
      decl = StructGenerator.new(mod).build_decl
    elsif DataGenerator.generatable?(mod)
      decl = DataGenerator.new(mod).build_decl
    else
      decl = AST::Declarations::Class.new(
        name: to_type_name(only_name(mod)),
        type_params: type_params(mod),
        super_class: generate_super_class(mod),
        members: [],
        annotations: [],
        location: nil,
        comment: nil
      )
    end

    outer_decls << decl
  end

  generate_mixin(mod, decl, type_name, type_name_absolute)

  unless mod < Struct || (RUBY_VERSION >= '3.2' && mod < Data)
    generate_methods(mod, type_name, decl.members) unless outline
  end

  generate_constants mod, decl.members
end

#generate_constants(mod, decls)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 422

def generate_constants(mod, decls)
  module_name = const_name!(mod)
  Reflection.constants_of(mod, false).sort.each do |name|
    next if todo_object&.skip_constant?(module_name: module_name, name: name)

    begin
      value = mod.const_get(name)
    rescue StandardError, LoadError => e
      RBS.logger.warn("Skipping constant #{name} of #{mod} since #{e}")
      next
    end

    next if Reflection.object_class(value).equal?(Class)
    next if Reflection.object_class(value).equal?(Module)

    unless Reflection.object_class(value).name
      RBS.logger.warn("Skipping constant #{name} #{value} of #{mod} as an instance of anonymous class")
      next
    end

    type = case value
           when true, false
             Types::Bases::Bool.new(location: nil)
           when nil
             Types::Optional.new(
               type: Types::Bases::Any.new(location: nil),
               location: nil
             )
           when ARGF
             Types::ClassInstance.new(name: TypeName("::RBS::Unnamed::ARGFClass"), args: [], location: nil)
           when ENV
             Types::ClassInstance.new(name: TypeName("::RBS::Unnamed::ENVClass"), args: [], location: nil)
           else
             value_type_name = to_type_name(const_name!(Reflection.object_class(value)), full_name: true).absolute!
             args = type_args(value_type_name)
             Types::ClassInstance.new(name: value_type_name, args: args, location: nil)
           end

    decls << AST::Declarations::Constant.new(
      name: to_type_name(name.to_s),
      type: type,
      location: nil,
      comment: nil
    )
  end
end

#generate_methods(mod, module_name, members)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 298

def generate_methods(mod, module_name, members)
  module_name_absolute = to_type_name(const_name!(mod), full_name: true).absolute!
  mod.singleton_methods.select {|name| target_method?(mod, singleton: name) }.sort.each do |name|
    method = mod.singleton_class.instance_method(name)
    next if todo_object&.skip_singleton_method?(module_name: module_name_absolute, method: method, accessibility: :public)

    if can_alias?(mod.singleton_class, method)
      members << AST::Members::Alias.new(
        new_name: method.name,
        old_name: method.original_name,
        kind: :singleton,
        location: nil,
        comment: nil,
        annotations: [],
      )
    else
      merge_rbs(module_name, members, singleton: name) do
        RBS.logger.info "missing #{module_name}.#{name} #{method.source_location}"

        members << AST::Members::MethodDefinition.new(
          name: method.name,
          overloads: [
            AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: method_type(method))
          ],
          kind: :singleton,
          location: nil,
          comment: nil,
          annotations: [],
          overloading: false,
          visibility: nil
        )
      end
    end
  end

  public_instance_methods = mod.public_instance_methods.select {|name| target_method?(mod, instance: name) }
  unless public_instance_methods.empty?
    public_instance_methods.sort.each do |name|
      method = mod.instance_method(name)
      next if todo_object&.skip_instance_method?(module_name: module_name_absolute, method: method, accessibility: :public)

      if can_alias?(mod, method)
        members << AST::Members::Alias.new(
          new_name: method.name,
          old_name: method.original_name,
          kind: :instance,
          location: nil,
          comment: nil,
          annotations: [],
        )
      else
        merge_rbs(module_name, members, instance: name) do
          RBS.logger.info "missing #{module_name}##{name} #{method.source_location}"

          members << AST::Members::MethodDefinition.new(
            name: method.name,
            overloads: [
              AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: method_type(method))
            ],
            kind: :instance,
            location: nil,
            comment: nil,
            annotations: [],
            overloading: false,
            visibility: nil
          )
        end
      end
    end
  end

  private_instance_methods = mod.private_instance_methods.select {|name| target_method?(mod, instance: name) }
  unless private_instance_methods.empty?
    added = false
    members << AST::Members::Private.new(location: nil)

    private_instance_methods.sort.each do |name|
      method = mod.instance_method(name)
      next if todo_object&.skip_instance_method?(module_name: module_name_absolute, method: method, accessibility: :private)

      added = true
      if can_alias?(mod, method)
        members << AST::Members::Alias.new(
          new_name: method.name,
          old_name: method.original_name,
          kind: :instance,
          location: nil,
          comment: nil,
          annotations: [],
        )
      else
        merge_rbs(module_name, members, instance: name) do
          RBS.logger.info "missing #{module_name}##{name} #{method.source_location}"

          members << AST::Members::MethodDefinition.new(
            name: method.name,
            overloads: [
              AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: method_type(method))
            ],
            kind: :instance,
            location: nil,
            comment: nil,
            annotations: [],
            overloading: false,
            visibility: nil
          )
        end
      end
    end

    members.pop unless added
  end
end

#generate_mixin(mod, decl, type_name, type_name_absolute)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 561

def generate_mixin(mod, decl, type_name, type_name_absolute)
  each_mixined_module(type_name, mod) do |module_name, module_full_name, mixin_class|
    next if todo_object&.skip_mixin?(type_name: type_name_absolute, module_name: module_full_name, mixin_class: mixin_class)

    args = type_args(module_full_name)
    decl.members << mixin_class.new(
      name: module_name,
      args: args,
      location: nil,
      comment: nil,
      annotations: []
    )
  end
end

#generate_module(mod)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 523

def generate_module(mod)
  name = const_name(mod)

  unless name
    RBS.logger.warn("Skipping anonymous module #{mod}")
    return
  end

  type_name_absolute = to_type_name(name, full_name: true).absolute!
  type_name = to_type_name(name)
  outer_decls = ensure_outer_module_declarations(mod)

  # Check if a declaration exists for the actual class
  decl = outer_decls.detect do |decl|
    decl.is_a?(AST::Declarations::Module) && decl.name.name == only_name(mod).to_sym
  end #: AST::Declarations::Module?

  unless decl
    decl = AST::Declarations::Module.new(
      name: to_type_name(only_name(mod)),
      type_params: type_params(mod),
      self_types: [],
      members: [],
      annotations: [],
      location: nil,
      comment: nil
    )

    outer_decls << decl
  end

  generate_mixin(mod, decl, type_name, type_name_absolute)

  generate_methods(mod, type_name, decl.members) unless outline

  generate_constants mod, decl.members
end

#generate_super_class(mod)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 469

def generate_super_class(mod)
  superclass = mod.superclass

  if superclass.nil? || superclass == ::Object
    nil
  elsif const_name(superclass).nil?
    RBS.logger.warn("Skipping anonymous superclass #{superclass} of #{mod}")
    nil
  else
    super_name = to_type_name(const_name!(superclass), full_name: true).absolute!
    super_args = type_args(super_name)
    AST::Declarations::Class::Super.new(name: super_name, args: super_args, location: nil)
  end
end

#merge_rbs(module_name, members, instance: nil, singleton: nil)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 239

def merge_rbs(module_name, members, instance: nil, singleton: nil)
  if merge
    if env.class_decls[module_name.absolute!]
      # @type var kind: AST::Members::MethodDefinition::kind
      case
      when instance
        method = builder.build_instance(module_name.absolute!).methods[instance]
        method_name = instance
        kind = :instance
      when singleton
        method = builder.build_singleton(module_name.absolute!).methods[singleton]
        method_name = singleton
        kind = :singleton
      end

      if method
        members << AST::Members::MethodDefinition.new(
          name: method_name,
          overloads: method.method_types.map {|type|
            AST::Members::MethodDefinition::Overload.new(
              annotations: [],
              method_type: type.update.tap do |ty|
                def ty.to_s
                  location or raise
                  location.source
                end
              end
            )
          },
          kind: kind,
          location: nil,
          comment: method.comments[0],
          annotations: method.annotations,
          overloading: false,
          visibility: nil
        )
        return
      end
    end

    yield
  else
    yield
  end
end

#method_type(method)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 170

def method_type(method)
  untyped = Types::Bases::Any.new(location: nil)

  required_positionals = [] #: Array[Types::Function::Param]
  optional_positionals = [] #: Array[Types::Function::Param]
  rest = nil #: Types::Function::Param?
  trailing_positionals = [] #: Array[Types::Function::Param]
  required_keywords = {} #: Hash[Symbol, Types::Function::Param]
  optional_keywords = {} #: Hash[Symbol, Types::Function::Param]
  rest_keywords = nil #: Types::Function::Param?

  requireds = required_positionals

  block = nil #: Types::Block?

  method.parameters.each do |(kind, name)|
    case kind
    when :req
      requireds << Types::Function::Param.new(name: name, type: untyped)
    when :opt
      requireds = trailing_positionals
      optional_positionals << Types::Function::Param.new(name: name, type: untyped)
    when :rest
      requireds = trailing_positionals
      name = nil if name == :* # For `def f(...) end` syntax
      rest = Types::Function::Param.new(name: name, type: untyped)
    when :keyreq
      name or raise
      required_keywords[name] = Types::Function::Param.new(name: nil, type: untyped)
    when :key
      name or raise
      optional_keywords[name] = Types::Function::Param.new(name: nil, type: untyped)
    when :keyrest
      rest_keywords = Types::Function::Param.new(name: nil, type: untyped)
    when :block
      block = Types::Block.new(
        type: Types::Function.empty(untyped).update(rest_positionals: Types::Function::Param.new(name: nil, type: untyped)),
        required: true,
        self_type: nil
      )
    end
  end

  block ||= block_from_ast_of(method)

  return_type = if method.name == :initialize
                  Types::Bases::Void.new(location: nil)
                else
                  untyped
                end
  method_type = Types::Function.new(
    required_positionals: required_positionals,
    optional_positionals: optional_positionals,
    rest_positionals: rest,
    trailing_positionals: trailing_positionals,
    required_keywords: required_keywords,
    optional_keywords: optional_keywords,
    rest_keywords: rest_keywords,
    return_type: return_type,
  )

  MethodType.new(
    location: nil,
    type_params: [],
    type: method_type,
    block: block
  )
end

#parse(file)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 104

def parse(file)
  require file
end

#target?(const) ⇒ Boolean

[ GitHub ]

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

def target?(const)
  name = const_name(const)
  return false unless name

  patterns.any? do |pattern|
    if pattern.end_with?("*")
      (name || "").start_with?(pattern.chop)
    else
      name == pattern
    end
  end
end

#target_method?(mod, instance: nil, singleton: nil) ⇒ Boolean

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 285

def target_method?(mod, instance: nil, singleton: nil)
  case
  when instance
    method = mod.instance_method(instance)
    method.owner == mod || owners_included.any? {|m| method.owner == m }
  when singleton
    method = mod.singleton_class.instance_method(singleton)
    method.owner == mod.singleton_class || owners_included.any? {|m| method.owner == m.singleton_class }
  else
    raise
  end
end

#todo_object

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 96

def todo_object
  @todo_object ||= Todo.new(builder: builder) if todo
end

#type_args(type_name)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 633

def type_args(type_name)
  if class_decl = env.class_decls.fetch(type_name.absolute!, nil)
    class_decl.type_params.size.times.map { Types::Bases::Any.new(location: nil) }
  else
    []
  end
end

#type_params(mod)

[ GitHub ]

  
# File 'lib/rbs/prototype/runtime.rb', line 641

def type_params(mod)
  type_name = to_type_name(const_name!(mod), full_name: true)
  if class_decl = env.class_decls[type_name.absolute!]
    class_decl.type_params
  else
    []
  end
end