lib/require_everything.rb



# frozen_string_literal: true
# typed: true

require 'pathname'

require_relative './gem_loader'
require_relative './status'

class ExitCalledError < RuntimeError
end

class Sorbet::Private::RequireEverything
  # Goes through the most common ways to require all your userland code
  def self.require_everything
    return if @already_ran
    @already_ran = true
    patch_kernel
    load_rails
    load_bundler # this comes second since some rails projects fail `Bundler.require' before rails is loaded
    require_all_files
  end

  def self.load_rails
    return unless rails?
    require './config/application'
    rails = Object.const_get(:Rails)
    rails.application.require_environment!
    rails.application.eager_load!
    true
  end

  def self.load_bundler
    return unless File.exist?('Gemfile')
    begin
      require 'bundler'
    rescue LoadError
      return
    end
    Sorbet::Private::GemLoader.require_all_gems
  end

  def self.require_all_files
    excluded_paths = Set.new
    excluded_paths += excluded_rails_files if rails?

    abs_paths = Dir.glob("#{Dir.pwd}/**/*.rb")
    errors = []
    abs_paths.each_with_index do |abs_path, i|
      # Executable files are likely not meant to be required.
      # Some things we're trying to prevent against:
      # - misbehaving require-time side effects (removing files, reading from stdin, etc.)
      # - extra long runtime (making network requests, running a benchmark)
      # While this isn't a perfect heuristic for these things, it's pretty good.
      next if File.executable?(abs_path)
      next if excluded_paths.include?(abs_path)

      # Skip db/schema.rb, as requiring it can wipe the database. This is left
      # out of exclude_rails_files, as it is possible to use the packages that
      # generate it without using the whole rails ecosystem.
      next if /db\/schema.rb$/.match(abs_path)

      next if /^# +typed: +ignore$/.match(File.read(abs_path).scrub)

      begin
        my_require(abs_path, i+1, abs_paths.size)
      rescue LoadError, NoMethodError, SyntaxError
        next
      rescue
        errors << abs_path
        next
      end
    end
    # one more chance for order dependent things
    errors.each_with_index do |abs_path, i|
      begin
        my_require(abs_path, i+1, errors.size)
      rescue
      end
    end

    Sorbet::Private::Status.done
  end

  def self.my_require(abs_path, numerator, denominator)
    rel_path = Pathname.new(abs_path).relative_path_from(Pathname.new(Dir.pwd)).to_s
    Sorbet::Private::Status.say("[#{numerator}/#{denominator}] require_relative './#{rel_path}'")
    require_relative abs_path
  end

  def self.patch_kernel
    Kernel.send(:define_method, :exit) do |*|
      puts 'Kernel#exit was called while requiring ruby source files'
      raise ExitCalledError.new
    end

    Kernel.send(:define_method, :at_exit) do |&block|
      if File.split($0).last == 'rake'
        # Let `rake test` work
        super
        return proc {}
      end
      # puts "Ignoring at_exit: #{block}"
      proc {}
    end
  end

  private

  def self.excluded_rails_files
    excluded_paths = Set.new

    # Exclude files that have already been loaded by rails
    rails = Object.const_get(:Rails)
    load_paths = rails.application.send(:_all_load_paths)
    load_paths.each do |path|
      excluded_paths += Dir.glob("#{path}/**/*.rb")
    end

    # Exclude initializers, as they have already been run by rails and
    # can contain side-effects like monkey-patching that should
    # only be run once.
    excluded_paths += Dir.glob("#{Dir.pwd}/config/initializers/**/*.rb")
  end

  def self.rails?
    return false unless File.exist?('config/application.rb')
    begin
      require 'rails'
    rescue LoadError
      return false
    end
    true
  end
end