class Sorbet::Private::GemGeneratorTracepoint::TracepointSerializer

Constants

BAD_METHODS
SPECIAL_METHOD_NAMES

Public Class Methods

new(files:, delegate_classes:) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 35
def initialize(files:, delegate_classes:)
  @files = files
  @delegate_classes = delegate_classes

  @anonymous_map = {}
  @prev_anonymous_id = 0
end

Public Instance Methods

serialize(output_dir) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 44
      def serialize(output_dir)
        gem_class_defs = preprocess(@files)

        FileUtils.mkdir_p(output_dir) unless gem_class_defs.empty?

        gem_class_defs.each do |gem, klass_ids|
          File.open("#{File.join(output_dir, gem[:gem])}.rbi", 'w') do |f|
            f.write(HEADER)
            f.write("#
# If you would like to make changes to this file, great! Please create the gem's shim here:
#
#   https://github.com/sorbet/sorbet-typed/new/master?filename=lib/#{gem[:gem]}/all/#{gem[:gem]}.rbi
#
")
            f.write("# #{gem[:gem]}-#{gem[:version]}\n\n")
            klass_ids.each do |klass_id, class_def|
              klass = class_def.klass

              f.write("#{Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? 'class' : 'module'} #{class_name(klass)}")
              f.write(" < #{class_name(klass.superclass)}") if Sorbet::Private::RealStdlib.real_is_a?(klass, Class) && ![Object, nil].include?(klass.superclass)
              f.write("\n")

              rows = class_def.defs.map do |item|
                case item[:type]
                when :method
                  if !valid_method_name?(item[:method])
                    # warn("Invalid method name: #{klass}.#{item[:method]}")
                    next
                  end
                  if BAD_METHODS.include?([gem[:gem], class_name(klass), item[:method]])
                    next
                  end
                  begin
                    method = item[:singleton] ? Sorbet::Private::RealStdlib.real_method(klass, item[:method]) : klass.instance_method(item[:method])

                    "#{generate_method(method, !item[:singleton])}"
                  rescue NameError
                  end
                when :include, :extend
                  name = class_name(item[item[:type]])
                  "  #{item[:type]} #{name}"
                end
              end
              rows = rows.compact.sort
              f.write(rows.join("\n"))
              f.write("\n") if !rows.empty?
              f.write("end\n")
            end
          end
        end
      end

Private Instance Methods

anonymous_id() click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 212
def anonymous_id
  @prev_anonymous_id += 1
end
class_name(klass) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 239
def class_name(klass)
  klass = @delegate_classes[Sorbet::Private::RealStdlib.real_object_id(klass)] || klass
  name = Sorbet::Private::RealStdlib.real_name(klass) if Sorbet::Private::RealStdlib.real_is_a?(klass, Module)

  # class/module has no name; it must be anonymous
  if name.nil? || name == ""
    middle = Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? klass.superclass : klass.class
    id = @anonymous_map[Sorbet::Private::RealStdlib.real_object_id(klass)] ||= anonymous_id
    return "Anonymous_#{class_name(middle).gsub('::', '_')}_#{id}"
  end

  # if the name doesn't only contain word characters and ':', or any part doesn't start with a capital, Sorbet doesn't support it
  if name !~ /^[\w:]+$/ || !name.split('::').all? { |part| part =~ /^[A-Z]/ }
    # warn("Invalid class name: #{name}")
    id = @anonymous_map[Sorbet::Private::RealStdlib.real_object_id(klass)] ||= anonymous_id
    return "InvalidName_#{name.gsub(/[^\w]/, '_').gsub(/0x([0-9a-f]+)/, '0x00')}_#{id}"
  end

  name
end
detect_used(gem_class_defs) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 150
def detect_used(gem_class_defs)
  # subclassed, included, or extended
  used = {}

  gem_class_defs.each do |gem, klass_ids|
    klass_ids.each do |klass_id, class_def|
      klass = class_def.klass

      # only add an anon module if it's used as a superclass of a non-anon module, or is included/extended by a non-anon module
      used_value = Sorbet::Private::RealStdlib.real_is_a?(klass, Module) && !Sorbet::Private::RealStdlib.real_name(klass).nil? ? true : Sorbet::Private::RealStdlib.real_object_id(klass) # if non-anon, set it to true
      (used[Sorbet::Private::RealStdlib.real_object_id(klass.superclass)] ||= Set.new) << used_value if Sorbet::Private::RealStdlib.real_is_a?(klass, Class)
      # otherwise link to next anon class
      class_def.defs.each do |item|
        (used[item[item[:type]].object_id] ||= Set.new) << used_value if [:extend, :include].include?(item[:type])
      end
    end
  end

  used
end
files_to_gem_class_defs(files) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 103
def files_to_gem_class_defs(files)
  # Transform tracer output into hash of gems to class definitions
  files.each_with_object({}) do |(path, defined), gem_class_defs|
    gem = gem_from_location(path)
    if gem.nil?
      warn("Can't find gem for #{path}") unless path.start_with?(Dir.pwd)
      next
    end
    next if gem[:gem] == 'ruby'
    # We're currently ignoring bundler, because we can't easily pin
    # everyone to the same version of bundler in tests and in CI.
    # There is an RBI for bundler in sorbet-typed.
    next if gem[:gem] == 'bundler'
    # We ignore sorbet-runtime because because we write the RBI for it into our payload.
    # For some reason, runtime reflection generates methods with incorrect arities.
    next if gem[:gem] == 'sorbet-runtime'

    gem_class_defs[gem] ||= {}
    defined.each do |item|
      klass = item[:module]
      klass_id = Sorbet::Private::RealStdlib.real_object_id(klass)
      class_def = gem_class_defs[gem][klass_id] ||= ClassDefinition.new(klass_id, klass, [])
      class_def.defs << item unless item[:type] == :module
    end
  end
end
filter_unused(gem_class_defs) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 130
def filter_unused(gem_class_defs)
  used = detect_used(gem_class_defs)

  gem_class_defs.each_with_object({}) do |(gem, klass_defs), hsh|
    hsh[gem] = klass_defs.select do |klass_id, klass_def|
      klass = klass_def.klass

      # Unused anon classes
      next if !((Sorbet::Private::RealStdlib.real_is_a?(klass, Module) && !Sorbet::Private::RealStdlib.real_name(klass).nil?) || used?(klass_id, used))

      # Anon delegate classes
      next if Sorbet::Private::RealStdlib.real_is_a?(klass, Class) && klass.superclass == Delegator && !klass.name

      # TODO should this be here?
      # next if [Object, BasicObject, Hash].include?(klass)
      true
    end
  end
end
gem_from_location(location) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 216
def gem_from_location(location)
  match =
    location&.match(/^.*\/(?:gems\/(?:(?:j?ruby-)?[\d.]+(?:@[^\/]+)?(?:\/bundler)?\/)?|ruby\/[\d.]+\/)gems\/([^\/]+)-([^-\/]+)\//i) || # gem
    location&.match(/^.*\/(ruby)\/([\d.]+)\//) || # ruby stdlib
    location&.match(/^.*\/(jruby)-([\d.]+)\//) || # jvm ruby stdlib
    location&.match(/^.*\/(site_ruby)\/([\d.]+)\//) # rubygems
  if match.nil?
    # uncomment to generate files for methods outside of gems
    # {
    #   path: location,
    #   gem: location.gsub(/[\/\.]/, '_'),
    #   version: '1.0.0',
    # }
    nil
  else
    {
      path: match[0],
      gem: match[1],
      version: match[2],
    }
  end
end
generate_method(method, instance, spaces = 2) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 176
def generate_method(method, instance, spaces = 2)
  # method.parameters is an array of:
  # a      [:req, :a]
  # b = 1  [:opt, :b]
  # c:     [:keyreq, :c]
  # d: 1   [:key, :d]
  # *e     [:rest, :e]
  # **f    [:keyrest, :f]
  # &g     [:block, :g]
  prefix = ' ' * spaces
  parameters = method.parameters.map.with_index do |(type, name), index|
    name = "arg#{index}" if name.nil? || name.empty?
    case type
    when :req
      name
    when :opt
      "#{name} = nil"
    when :keyreq
      "#{name}:"
    when :key
      "#{name}: nil"
    when :rest
      "*#{name}"
    when :keyrest
      "**#{name}"
    when :block
      "&#{name}"
    else
      raise "Unknown parameter type: #{type}"
    end
  end
  parameters = parameters.join(', ')
  parameters = "(#{parameters})" unless parameters.empty?
  "#{prefix}def #{instance ? '' : 'self.'}#{method.name}#{parameters}; end"
end
preprocess(files) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 98
def preprocess(files)
  gem_class_defs = files_to_gem_class_defs(files)
  filter_unused(gem_class_defs)
end
used?(klass, used) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 171
def used?(klass, used)
  used_by = used[klass] || []
  used_by.any? { |user| user == true || used?(user, used) }
end
valid_method_name?(symbol) click to toggle source
# File lib/gem-generator-tracepoint/tracepoint_serializer.rb, line 260
def valid_method_name?(symbol)
  string = symbol.to_s
  return true if SPECIAL_METHOD_NAMES.include?(string)
  string =~ /^[[:word:]]+[?!=]?$/
end