File: eager_load.rb

package info (click to toggle)
ruby-zeitwerk 2.7.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 732 kB
  • sloc: ruby: 6,240; makefile: 4
file content (232 lines) | stat: -rw-r--r-- 7,416 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
module Zeitwerk::Loader::EagerLoad
  # 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
  def eager_load(force: false)
    mutex.synchronize do
      break if @eager_loaded
      raise Zeitwerk::SetupRequired unless @setup

      log("eager load start") if logger

      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") if logger
    end
  end

  #: (String | Pathname) -> void
  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 dir?(abspath)

    cnames = []

    root_namespace = nil
    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 hidden?(basename)

      unless collapse?(dir)
        cnames << inflector.camelize(basename, dir).to_sym
      end
    end

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

    return if @eager_loaded

    namespace = root_namespace
    cnames.reverse_each do |cname|
      # 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

  #: (Module) -> void
  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

  # 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
  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 dir?(abspath) || !ruby?(abspath)
    raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)

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

    base_cname = inflector.camelize(basename, abspath).to_sym

    root_namespace = nil
    cnames = []

    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 hidden?(basename)

      unless collapse?(dir)
        cnames << inflector.camelize(basename, dir).to_sym
      end
    end

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

    namespace = root_namespace
    cnames.reverse_each do |cname|
      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

  # The caller is responsible for making sure `namespace` is the namespace that
  # corresponds to `dir`.
  #
  #: (String, Module, ?force: boolish) -> void
  private 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") if logger

    queue = [[dir, namespace]]
    while (current_dir, namespace = queue.shift)
      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 = inflector.camelize(basename, abspath).to_sym
            queue << [abspath, namespace.const_get(cname, false)]
          end
        end
      end
    end

    log("eager load directory #{dir} end") if logger
  end

  # 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
  private 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)
        ls(dir) do |basename, abspath, ftype|
          next unless ftype == :directory

          if collapse?(abspath)
            dirs << abspath
          elsif segment == inflector.camelize(basename, abspath)
            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
end