class DeepCover::AutoloadTracker

Constants

AutoloadEntry

Attributes

warned_for_frozen_module[RW]
autoloads_by_basename[R]
interceptor_files_by_path[R]

Public Class Methods

new() click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 27
def initialize
  @autoloads_by_basename = {}
  @interceptor_files_by_path = {}
end
warn_frozen_module(mod) click to toggle source

Using frozen modules/classes is almost unheard of, but a warning makes things easier if someone does it

# File lib/deep_cover/autoload_tracker.rb, line 130
def self.warn_frozen_module(mod)
  return if warned_for_frozen_module
  self.warned_for_frozen_module ||= true
  warn "There is an autoload on a frozen module/class: #{mod}, DeepCover cannot handle those, failure is probable. " \
       "This warning won't be displayed again (even for different module/class)"
end

Public Instance Methods

autoload_path_for(mod, name, path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 32
def autoload_path_for(mod, name, path)
  interceptor_path = setup_interceptor_for(mod, name, path)

  if DeepCover.custom_requirer.is_being_required?(path)
    already_loaded_feature
  else
    interceptor_path
  end
end
initialize_autoloaded_paths(mods = ObjectSpace.each_object(Module)) { |mod, name, interceptor_path| ... } click to toggle source

In JRuby, ObjectSpace.each_object is allowed for Module and Class, so we are good.

# File lib/deep_cover/autoload_tracker.rb, line 79
def initialize_autoloaded_paths(mods = ObjectSpace.each_object(Module)) # &do_autoload_block
  mods.each do |mod|
    # Module's constants are shared with Object. But if you set autoloads directly on Module, they
    # appear on multiple classes. So just skip, Object will take care of those.
    next if mod == Module
    # This happens with JRuby
    next unless mod.respond_to?(:constants)

    if mod.frozen?
      if mod.constants.any? { |name| mod.autoload?(name) }
        self.class.warn_frozen_module(mod)
      end
      next
    end

    mod.constants.each do |name|
      # JRuby can talk about deprecated constants here
      path = Tools.silence_warnings do
        mod.autoload?(name)
      end
      next unless path
      interceptor_path = setup_interceptor_for(mod, name, path)
      yield mod, name, interceptor_path
    end
  end
end
possible_autoload_target?(requested_path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 42
def possible_autoload_target?(requested_path)
  basename = basename_without_extension(requested_path)
  autoloads = @autoloads_by_basename[basename]
  autoloads && !autoloads.empty?
end
remove_interceptors() { |mod, name, target_path| ... } click to toggle source

We need to remove the interceptor hooks, otherwise, the problem if manually requiring something that is autoloaded will cause issues.

# File lib/deep_cover/autoload_tracker.rb, line 108
def remove_interceptors # &do_autoload_block
  @autoloads_by_basename.each do |basename, entries|
    entries.each do |entry|
      mod = entry.mod_if_available
      next unless mod
      # Module's constants are shared with Object. But if you set autoloads directly on Module, they
      # appear on multiple classes. So just skip, Object will take care of those.
      next if mod == Module
      yield mod, entry.name, entry.target_path
    end
  end

  @autoloaded_paths = {}
  @interceptor_files_by_path = {}
end
wrap_require(requested_path, absolute_path_found) { || ... } click to toggle source

JRuby dislikes that we change the autoload as it is executing an autoload Things seems to work when we do nothing special

# File lib/deep_cover/autoload_tracker.rb, line 51
def wrap_require(requested_path, absolute_path_found) # &block
  yield
end

Protected Instance Methods

already_loaded_feature() click to toggle source

It is not possible to simply remove an autoload. So, instead, we must change the autoload to an already loaded path. The autoload will be set back to what it was once the require returns. This is needed in case that required path wasn't the one that fulfilled the autoload, or if the constant and $LOADED_FEATURES gets removed, since in that situation, the autoload is supposed to be active again.

# File lib/deep_cover/autoload_tracker.rb, line 209
def already_loaded_feature
  $LOADED_FEATURES.first
end
autoload_interceptor_for(path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 213
    def autoload_interceptor_for(path)
      existing_files = @interceptor_files_by_path[path] || []
      reusable_file = existing_files.detect { |f| !$LOADED_FEATURES.include?(f.path) }
      return reusable_file.path if reusable_file

      new_file = Tempfile.new([File.basename(path), '.rb'])
      # Need to store all the tempfiles so that they are not GCed, which would delete the files themselves.
      # Keeping them by path allows us to reuse them.
      @interceptor_files_by_path[path] ||= []
      @interceptor_files_by_path[path] << new_file
      new_file.write(<<-RUBY)
# Intermediary file for ruby's autoload made by deep-cover
require #{path.to_s.inspect}
      RUBY
      new_file.close

      new_file.path
    end
basename_without_extension(path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 181
def basename_without_extension(path)
  without_extension(File.basename(path))
end
entries_for_target(requested_path, absolute_path_found) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 150
def entries_for_target(requested_path, absolute_path_found)
  basename = basename_without_extension(requested_path)
  autoloads = @autoloads_by_basename[basename] || []

  if absolute_path_found
    autoloads.select { |entry| entry_is_target?(entry, requested_path, absolute_path_found) }
  elsif requested_path == File.absolute_path(requested_path)
    []
  elsif requested_path.start_with?('./', '../')
    []
  else
    # We didn't find a path that goes through the $LOAD_PATH
    # It's possible that RubyGems will actually add the $LOAD_PATH and require an actual file
    # So we must make a best-guest for possible matches
    requested_path_to_compare = without_extension(requested_path)
    autoloads.select { |entry| requested_path_to_compare == without_extension(entry.target_path) }
  end
end
entry_is_target?(entry, requested_path, absolute_path_found) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 169
def entry_is_target?(entry, requested_path, absolute_path_found)
  return true if entry.target_path == requested_path
  target_path_rb = with_rb_extension(entry.target_path)
  return true if target_path_rb == requested_path

  # Even though this is not efficient, it's safer to resolve entries' target_path each time
  # instead of storing the result, in case subsequent changes to $LOAD_PATH gives different results
  entry_absolute_path = DeepCover.custom_requirer.resolve_path(entry.target_path)
  return true if entry_absolute_path == absolute_path_found
  false
end
setup_interceptor_for(mod, name, path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 139
def setup_interceptor_for(mod, name, path)
  interceptor_path = autoload_interceptor_for(path)
  entry = AutoloadEntry.new(WeakRef.new(mod), name, path, interceptor_path)

  basename = basename_without_extension(path)

  @autoloads_by_basename[basename] ||= []
  @autoloads_by_basename[basename] << entry
  interceptor_path
end
with_rb_extension(path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 185
def with_rb_extension(path)
  path += '.rb' unless find_requirable_extension(path)
  path
end
without_extension(path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 190
def without_extension(path)
  if (ext = find_requirable_extension(path))
    path[0...-ext.length]
  else
    path
  end
end

Private Instance Methods

find_requirable_extension(path) click to toggle source
# File lib/deep_cover/autoload_tracker.rb, line 198
        def find_requirable_extension(path)
  ext = File.extname(path)
  REQUIRABLE_EXTENSIONS[ext] && ext
end