lib/serialize.rb



# frozen_string_literal: true
# typed: true

require 'bigdecimal'

class Sorbet::Private::Serialize
  BLACKLIST_CONSTANTS = [
    ['DidYouMean', :NameErrorCheckers], # https://github.com/yuki24/did_you_mean/commit/b72fdbe194401f1be21f8ad7b6e3f784a0ad197d
    ['Net', :OpenSSL], # https://github.com/yuki24/did_you_mean/commit/b72fdbe194401f1be21f8ad7b6e3f784a0ad197d
  ]

  SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]

  def initialize(constant_cache)
    @constant_cache = constant_cache
  end

  private def get_constants(mod, inherited=nil)
    @real_constants ||= Module.instance_method(:constants)
    if inherited.nil?
      @real_constants.bind(mod).call
    else
      @real_constants.bind(mod).call(inherited)
    end
  end

  def self.header(typed="true", subcommand="update")
    buffer = []
    buffer << "# This file is autogenerated. Do not edit it by hand. Regenerate it with:"
    buffer << "#   srb rbi #{subcommand}"
    if typed
      buffer << ""
      buffer << "# typed: #{typed}"
    end
    buffer << ""
    buffer.join("\n")
  end

  def class_or_module(class_name)
    if !valid_class_name(class_name)
      return " # Skipping serializing #{class_name} because it is an invalid name\n"
    end

    klass = @constant_cache.class_by_name(class_name)
    is_nil = nil.equal?(klass)
    raise "#{class_name} is not a Class or Module. Maybe it was miscategorized?" if is_nil

    ret = String.new

    superclass = Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? Sorbet::Private::RealStdlib.real_superclass(klass) : nil
    if superclass
      superclass_str = Sorbet::Private::RealStdlib.real_eqeq(superclass, Object) ? '' : @constant_cache.name_by_class(superclass)
    else
      superclass_str = ''
    end
    superclass_str = !superclass_str || superclass_str.empty? ? '' : " < #{superclass_str}"
    ret << (Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? "class #{class_name}#{superclass_str}\n" : "module #{class_name}\n")
    ret << "  extend ::T::Sig\n"

    # We don't use .included_modules since that also has all the aweful things
    # that are mixed into Object. This way we at least have a delimiter before
    # the awefulness starts (the superclass).
    Sorbet::Private::RealStdlib.real_ancestors(klass).each do |ancestor|
      next if Sorbet::Private::RealStdlib.real_eqeq(ancestor, klass)
      break if Sorbet::Private::RealStdlib.real_eqeq(ancestor, superclass)
      ancestor_name = @constant_cache.name_by_class(ancestor)
      next unless ancestor_name
      next if ancestor_name == class_name
      if Sorbet::Private::RealStdlib.real_is_a?(ancestor, Class)
        ret << "  # Skipping `include #{ancestor_name}` because it is a Class\n"
        next
      end
      if !valid_class_name(ancestor_name)
        ret << "  # Skipping `include #{ancestor_name}` because it is an invalid name\n"
        next
      end
      ret << "  include ::#{ancestor_name}\n"
    end
    Sorbet::Private::RealStdlib.real_singleton_class(klass).ancestors.each do |ancestor|
      next if ancestor == Sorbet::Private::RealStdlib.real_singleton_class(klass)
      break if superclass && ancestor == Sorbet::Private::RealStdlib.real_singleton_class(superclass)
      break if ancestor == Module
      break if ancestor == Object
      ancestor_name = @constant_cache.name_by_class(ancestor)
      next unless ancestor_name
      if Sorbet::Private::RealStdlib.real_is_a?(ancestor, Class)
        ret << "  # Skipping `extend #{ancestor_name}` because it is a Class\n"
        next
      end
      if !valid_class_name(ancestor_name)
        ret << "  # Skipping `extend #{ancestor_name}` because it is an invalid name\n"
        next
      end
      ret << "  extend ::#{ancestor_name}\n"
    end

    constants = []
    # Declare all the type_members and type_templates
    constants += get_constants(klass).uniq.map do |const_sym|
      # We have to not pass `false` because `klass.constants` intentionally is
      # pulling in all the ancestor constants
      next if Sorbet::Private::ConstantLookupCache::DEPRECATED_CONSTANTS.include?("#{class_name}::#{const_sym}")
      begin
        value = klass.const_get(const_sym)
      rescue LoadError, NameError, RuntimeError, ArgumentError
        ret << "# Failed to load #{class_name}::#{const_sym}\n"
        next
      end
      # next if !Sorbet::Private::RealStdlib.real_is_a?(value, T::Types::TypeVariable)
      next if Sorbet::Private::RealStdlib.real_is_a?(value, Module)
      next if !comparable?(value)
      [const_sym, value]
    end
    constants += get_constants(klass, false).uniq.map do |const_sym|
      next if BLACKLIST_CONSTANTS.include?([class_name, const_sym])
      next if Sorbet::Private::ConstantLookupCache::DEPRECATED_CONSTANTS.include?("#{class_name}::#{const_sym}")
      begin
        value = klass.const_get(const_sym, false)
      rescue LoadError, NameError, RuntimeError, ArgumentError
        ret << "# Failed to load #{class_name}::#{const_sym}\n"
        next
      end
      next if Sorbet::Private::RealStdlib.real_is_a?(value, Module)
      next if !comparable?(value)
      [const_sym, value]
    end
    constants_sorted = constants.compact.sort_by do |const_sym, _value|
      const_sym
    end
    constants_uniq = constants_sorted.uniq do |const_sym, _value|
      const_sym.hash
    end
    constants_serialized = constants_uniq.map do |const_sym, value|
      constant(const_sym, value)
    end
    ret << constants_serialized.join("\n")
    ret << "\n\n" if !constants_serialized.empty?

    methods = []
    instance_methods = Sorbet::Private::RealStdlib.real_instance_methods(klass, false)
    begin
      initialize = klass.instance_method(:initialize)
    rescue
      initialize = nil
    end
    if initialize && initialize.owner == klass
      # This method never apears in the reflection list...
      instance_methods += [:initialize]
    end
    Sorbet::Private::RealStdlib.real_ancestors(klass).reject {|ancestor| @constant_cache.name_by_class(ancestor)}.each do |ancestor|
      instance_methods += ancestor.instance_methods(false)
    end

    # uniq here is required because we populate additional methos from anonymous superclasses and there
    # might be duplicates
    methods += instance_methods.sort.uniq.map do |method_sym|
      begin
        method = klass.instance_method(method_sym)
      rescue => e
        ret << "# #{e}\n"
        next
      end
      next if blacklisted_method(method)
      next if ancestor_has_method(method, klass)
      serialize_method(method)
    end
    # uniq is not required here, but added to be on the safe side
    methods += Sorbet::Private::RealStdlib.real_singleton_methods(klass, false).sort.uniq.map do |method_sym|
      begin
        method = klass.singleton_method(method_sym)
      rescue => e
        ret << "# #{e}\n"
        next
      end
      next if blacklisted_method(method)
      next if ancestor_has_method(method, Sorbet::Private::RealStdlib.real_singleton_class(klass))
      serialize_method(method, true)
    end
    ret << methods.join("\n")
    ret << "end\n"

    ret
  end

  def alias(base, other_name)
    ret = String.new
    ret << "#{other_name} = #{base}"
    ret
  end

  def comparable?(value)
    return false if Sorbet::Private::RealStdlib.real_is_a?(value, BigDecimal) && value.nan?
    return false if Sorbet::Private::RealStdlib.real_is_a?(value, Float) && value.nan?
    return false if Sorbet::Private::RealStdlib.real_is_a?(value, Complex)
    true
  end

  def blacklisted_method(method)
    method.name =~ /__validator__[0-9]{8}/ || method.name =~ /.*:.*/
  end

  def valid_method_name(name)
    return true if SPECIAL_METHOD_NAMES.include?(name)
    return false if name =~ /^\d/
    name =~ /^[[:word:]]+[?!=]?$/
  end

  def ancestor_has_method(method, klass)
    return false if !Sorbet::Private::RealStdlib.real_is_a?(klass, Class)
    first_ancestor = klass.ancestors.find do |ancestor|
      next if ancestor == klass
      begin
        ancestor.instance_method(method.name)
      rescue NameError
        nil
      end
    end
    return false unless first_ancestor
    first_ancestor.instance_method(method.name).parameters == method.parameters
  end

  def constant(const, value)
    #if Sorbet::Private::RealStdlib.real_is_a?(value, T::Types::TypeTemplate)
      #"  #{const} = type_template"
    #elsif Sorbet::Private::RealStdlib.real_is_a?(value, T::Types::TypeMember)
      #"  #{const} = type_member"
    #else
      #"  #{const} = ::T.let(nil, ::T.untyped)"
    #end
    if KEYWORDS.include?(const.to_sym)
      return "# Illegal constant name: #{const}"
    end
    "  #{const} = ::T.let(nil, ::T.untyped)"
  end

  def serialize_method(method, static=false, with_sig: true)
    name = method.name.to_s
    ret = String.new
    if !valid_method_name(name)
      ret << "# Illegal method name: #{name}"
      return
    end
    parameters = from_method(method)
    # a hack for appeasing Sorbet in the presence of the Enumerable interface
    if name == 'each' && !parameters.any? {|(kind, _)| kind == :block}
      parameters.push([:block, "blk"])
    end
    ret << serialize_sig(parameters) if with_sig
    args = parameters.map do |(kind, param_name)|
      to_sig(kind, param_name)
    end.compact.join(', ')
    ret << "  def #{static ? 'self.' : ''}#{name}(#{args}); end\n"
    ret
  end

  def valid_class_name(name)
    name.split("::").each do |piece|
      return false if piece[0].upcase != piece[0]
    end
    return false if [
      'Sorbet::Private::GemGeneratorTracepoint::Tracer::ClassOverride',
      'Sorbet::Private::GemGeneratorTracepoint::Tracer::ModuleOverride',
      'Sorbet::Private::GemGeneratorTracepoint::Tracer::ObjectOverride',
    ].include?(name)
    true
  end

  def serialize_sig(parameters)
    ret = String.new
    if !parameters.empty?
      ret << "  sig do\n"
      ret << "    params(\n"
      parameters.each do |(_kind, name)|
        ret << "      #{name}: ::T.untyped,\n"
      end
      ret << "    )\n"
      ret << "    .returns(::T.untyped)\n"
      ret << "  end\n"
    else
      ret << "  sig {returns(::T.untyped)}\n"
    end
    ret
  end

  # from https://docs.ruby-lang.org/en/2.4.0/keywords_rdoc.html
  KEYWORDS = [
    :__ENCODING__,
    :__LINE__,
    :__FILE__,
    :BEGIN,
    :END,
    :alias,
    :and,
    :begin,
    :break,
    :case,
    :class,
    :def,
    :defined?,
    :do,
    :else,
    :elsif,
    :end,
    :ensure,
    :false,
    :for,
    :if,
    :in,
    :module,
    :next,
    :nil,
    :not,
    :or,
    :redo,
    :rescue,
    :retry,
    :return,
    :self,
    :super,
    :then,
    :true,
    :undef,
    :unless,
    :until,
    :when,
    :while,
    :yield,
  ]

  def from_method(method)
    uniq = 0
    method.parameters.map.with_index do |(kind, name), index|
      if !name
        arg_name = method.name.to_s[0...-1]
        if (!KEYWORDS.include?(arg_name.to_sym)) && method.name.to_s.end_with?('=') && arg_name =~ /\A[a-z_][a-z0-9A-Z_]*\Z/ && index == 0
          name = arg_name
        else
          name = '_' + (uniq == 0 ? '' : uniq.to_s)
          uniq += 1
        end
      end
      [kind, name]
    end
  end

  def to_sig(kind, name)
    case kind
    when :req
      name.to_s
    when :opt
      "#{name}=T.unsafe(nil)"
    when :rest
      "*#{name}"
    when :keyreq
      "#{name}:"
    when :key
      "#{name}: T.unsafe(nil)"
    when :keyrest
      "**#{name}"
    when :block
      "&#{name}"
    end
  end
end