module Zeitwerk::Loader::EagerLoad

Public Instance Methods

eager_load(force: false) click to toggle source
Eager loads all files in the root directories, recursively. Files do not
need to be in `$LOAD_PATH`, absolute file names are used. Ignored and
shadowed files are not eager loaded. You can opt-out specifically in
specific files and directories with `do_not_eager_load`, and that can be
overridden passing `force: true`.

: (?force: boolish) -> void

# File lib/zeitwerk/loader/eager_load.rb, line 9
def eager_load(force: false)
  mutex.synchronize do
    break if @eager_loaded
    raise Zeitwerk::SetupRequired unless @setup

    log { "eager load start" }

    actual_roots.each do |root_dir, root_namespace|
      actual_eager_load_dir(root_dir, root_namespace, force: force)
    end

    autoloaded_dirs.each do |autoloaded_dir|
      Zeitwerk::Registry.autoloads.unregister(autoloaded_dir)
    end
    autoloaded_dirs.clear

    @eager_loaded = true

    log { "eager load end" }
  end
end
eager_load_dir(path) click to toggle source

: (String | Pathname) -> void

# File lib/zeitwerk/loader/eager_load.rb, line 32
def eager_load_dir(path)
  raise Zeitwerk::SetupRequired unless @setup

  abspath = File.expand_path(path)

  raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)

  paths = []

  root_namespace = nil
  @fs.walk_up(abspath) do |dir|
    return if ignored_path?(dir)
    return if eager_load_exclusions.member?(dir)

    break if root_namespace = roots[dir]

    basename = File.basename(dir)
    return if @fs.hidden?(basename)

    paths << [basename, dir] unless collapse?(dir)
  end

  raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace

  return if @eager_loaded

  namespace = root_namespace
  paths.reverse_each do |basename, dir|
    cname = cname_for(basename, dir)
    # Can happen if there are no Ruby files. This is not an error condition,
    # the directory is actually managed. Could have Ruby files later.
    return unless namespace.const_defined?(cname, false)
    namespace = namespace.const_get(cname, false)
  end

  # A shortcircuiting test depends on the invocation of this method. Please
  # keep them in sync if refactored.
  actual_eager_load_dir(abspath, namespace)
end
eager_load_namespace(mod) click to toggle source

: (Module) -> void

# File lib/zeitwerk/loader/eager_load.rb, line 73
def eager_load_namespace(mod)
  raise Zeitwerk::SetupRequired unless @setup

  unless mod.is_a?(Module)
    raise Zeitwerk::Error, "#{mod.inspect} is not a class or module object"
  end

  return if @eager_loaded

  mod_name = real_mod_name(mod)
  return unless mod_name

  actual_roots.each do |root_dir, root_namespace|
    if Object.equal?(mod)
      # A shortcircuiting test depends on the invocation of this method.
      # Please keep them in sync if refactored.
      actual_eager_load_dir(root_dir, root_namespace)
    elsif root_namespace.equal?(Object)
      eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
    else
      root_namespace_name = real_mod_name(root_namespace)
      if root_namespace_name.start_with?(mod_name + "::")
        actual_eager_load_dir(root_dir, root_namespace)
      elsif mod_name == root_namespace_name
        actual_eager_load_dir(root_dir, root_namespace)
      elsif mod_name.start_with?(root_namespace_name + "::")
        eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
      else
        # Unrelated constant hierarchies, do nothing.
      end
    end
  end
end
load_file(path) click to toggle source
Loads the given Ruby file.

Raises if the argument is ignored, shadowed, or not managed by the receiver.

The method is implemented as `constantize` for files, in a sense, to be able
to descend orderly and make sure the file is loadable.

: (String | Pathname) -> void

# File lib/zeitwerk/loader/eager_load.rb, line 115
def load_file(path)
  abspath = File.expand_path(path)

  raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
  raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
  raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)

  file_basename = File.basename(abspath, ".rb")
  raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)

  root_namespace = nil
  paths = []

  @fs.walk_up(File.dirname(abspath)) do |dir|
    raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)

    break if root_namespace = roots[dir]

    basename = File.basename(dir)
    raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)

    paths << [basename, dir] unless collapse?(dir)
  end

  raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace

  base_cname = cname_for(file_basename, abspath)

  namespace = root_namespace
  paths.reverse_each do |basename, dir|
    cname = cname_for(basename, dir)
    namespace = namespace.const_get(cname, false)
  end

  raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)

  namespace.const_get(base_cname, false)
end

Private Instance Methods

actual_eager_load_dir(dir, namespace, force: false) click to toggle source
The caller is responsible for making sure `namespace` is the namespace that
corresponds to `dir`.

: (String, Module, ?force: boolish) -> void

# File lib/zeitwerk/loader/eager_load.rb, line 158
        def actual_eager_load_dir(dir, namespace, force: false)
  honour_exclusions = !force
  return if honour_exclusions && excluded_from_eager_load?(dir)

  log { "eager load directory #{dir} start" }

  queue = [[dir, namespace]]
  while (current_dir, namespace = queue.shift)
    @fs.ls(current_dir) do |basename, abspath, ftype|
      next if honour_exclusions && eager_load_exclusions.member?(abspath)

      if ftype == :file
        if (cref = autoloads[abspath])
          cref.get
        end
      else
        if collapse?(abspath)
          queue << [abspath, namespace]
        else
          cname = cname_for(basename, abspath)
          queue << [abspath, namespace.const_get(cname, false)]
        end
      end
    end
  end

  log { "eager load directory #{dir} end" }
end
eager_load_child_namespace(child, child_name, root_dir, root_namespace) click to toggle source
In order to invoke this method, the caller has to ensure `child` is a
strict namespace descendant of `root_namespace`.

: (Module, String, String, Module) -> void

# File lib/zeitwerk/loader/eager_load.rb, line 191
        def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
  suffix = child_name
  unless root_namespace.equal?(Object)
    suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
  end

  # These directories are at the same namespace level, there may be more if
  # we find collapsed ones. As we scan, we look for matches for the first
  # segment, and store them in `next_dirs`. If there are any, we look for
  # the next segments in those matches. Repeat.
  #
  # If we exhaust the search locating directories that match all segments,
  # we just need to eager load those ones.
  dirs = [root_dir]
  next_dirs = []

  suffix.split("::").each do |segment|
    while (dir = dirs.shift)
      @fs.ls(dir) do |basename, abspath, ftype|
        next unless ftype == :directory

        if collapse?(abspath)
          dirs << abspath
        elsif segment == cname_for(basename, abspath).to_s
          next_dirs << abspath
        end
      end
    end

    return if next_dirs.empty?

    dirs.replace(next_dirs)
    next_dirs.clear
  end

  dirs.each do |dir|
    actual_eager_load_dir(dir, child)
  end
end