class DeepCover::AutoloadTracker
Constants
- AutoloadEntry
Attributes
Public Class Methods
# File lib/deep_cover/autoload_tracker.rb, line 27 def initialize @autoloads_by_basename = {} @interceptor_files_by_path = {} end
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
# 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
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
# 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
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
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
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
# 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
# File lib/deep_cover/autoload_tracker.rb, line 181 def basename_without_extension(path) without_extension(File.basename(path)) end
# 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
# 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
# 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
# File lib/deep_cover/autoload_tracker.rb, line 185 def with_rb_extension(path) path += '.rb' unless find_requirable_extension(path) path end
# 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
# File lib/deep_cover/autoload_tracker.rb, line 198 def find_requirable_extension(path) ext = File.extname(path) REQUIRABLE_EXTENSIONS[ext] && ext end