zeitwerk 2.2.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1021 -111
- data/lib/zeitwerk/core_ext/kernel.rb +64 -0
- data/lib/zeitwerk/core_ext/module.rb +20 -0
- data/lib/zeitwerk/cref/map.rb +159 -0
- data/lib/zeitwerk/cref.rb +76 -0
- data/lib/zeitwerk/error.rb +24 -0
- data/lib/zeitwerk/gem_inflector.rb +6 -8
- data/lib/zeitwerk/gem_loader.rb +68 -0
- data/lib/zeitwerk/inflector.rb +12 -15
- data/lib/zeitwerk/internal.rb +13 -0
- data/lib/zeitwerk/loader/callbacks.rb +59 -34
- data/lib/zeitwerk/loader/config.rb +420 -0
- data/lib/zeitwerk/loader/constant_path_validator.rb +17 -0
- data/lib/zeitwerk/loader/eager_load.rb +226 -0
- data/lib/zeitwerk/loader/file_system.rb +212 -0
- data/lib/zeitwerk/loader/helpers.rb +43 -0
- data/lib/zeitwerk/loader.rb +490 -566
- data/lib/zeitwerk/null_inflector.rb +6 -0
- data/lib/zeitwerk/real_mod_name.rb +10 -6
- data/lib/zeitwerk/registry/autoloads.rb +38 -0
- data/lib/zeitwerk/registry/explicit_namespaces.rb +61 -0
- data/lib/zeitwerk/registry/inceptions.rb +31 -0
- data/lib/zeitwerk/registry/loaders.rb +33 -0
- data/lib/zeitwerk/registry.rb +49 -107
- data/lib/zeitwerk/version.rb +2 -1
- data/lib/zeitwerk.rb +25 -8
- metadata +26 -11
- data/lib/zeitwerk/explicit_namespace.rb +0 -80
- data/lib/zeitwerk/kernel.rb +0 -33
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kernel
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
# Zeitwerk's main idea is to define autoloads for project constants, and then
|
|
7
|
+
# intercept them when triggered in this thin `Kernel#require` wrapper.
|
|
8
|
+
#
|
|
9
|
+
# That allows us to complete the circle, invoke callbacks, autovivify modules,
|
|
10
|
+
# define autoloads for just autoloaded namespaces, update internal state, etc.
|
|
11
|
+
#
|
|
12
|
+
# On the other hand, if you publish a new version of a gem that is now managed
|
|
13
|
+
# by Zeitwerk, client code can reference directly your classes and modules and
|
|
14
|
+
# should not require anything. But if someone has legacy require calls around,
|
|
15
|
+
# they will work as expected, and in a compatible way. This feature is by now
|
|
16
|
+
# EXPERIMENTAL and UNDOCUMENTED.
|
|
17
|
+
alias_method :zeitwerk_original_require, :require
|
|
18
|
+
class << self
|
|
19
|
+
alias_method :zeitwerk_original_require, :require
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: (String) -> bool
|
|
23
|
+
def require(path)
|
|
24
|
+
if loader = Zeitwerk::Registry.autoloads.registered?(path)
|
|
25
|
+
if path.end_with?('.rb')
|
|
26
|
+
required = zeitwerk_original_require(path)
|
|
27
|
+
loader.__on_file_autoloaded(path) if required
|
|
28
|
+
required
|
|
29
|
+
else
|
|
30
|
+
loader.__on_dir_autoloaded(path)
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
required = zeitwerk_original_require(path)
|
|
35
|
+
if required
|
|
36
|
+
abspath = $LOADED_FEATURES.last
|
|
37
|
+
if loader = Zeitwerk::Registry.autoloads.registered?(abspath)
|
|
38
|
+
loader.__on_file_autoloaded(abspath)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
required
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# By now, I have seen no way so far to decorate require_relative.
|
|
46
|
+
#
|
|
47
|
+
# For starters, at least in CRuby, require_relative does not delegate to
|
|
48
|
+
# require. Both require and require_relative delegate the bulk of their work
|
|
49
|
+
# to an internal C function called rb_require_safe. So, our require wrapper is
|
|
50
|
+
# not executed.
|
|
51
|
+
#
|
|
52
|
+
# On the other hand, we cannot use the aliasing technique above because
|
|
53
|
+
# require_relative receives a path relative to the directory of the file in
|
|
54
|
+
# which the call is performed. If a wrapper here invoked the original method,
|
|
55
|
+
# Ruby would resolve the relative path taking lib/zeitwerk as base directory.
|
|
56
|
+
#
|
|
57
|
+
# A workaround could be to extract the base directory from caller_locations,
|
|
58
|
+
# but what if someone else decorated require_relative before us? You can't
|
|
59
|
+
# really know with certainty where's the original call site in the stack.
|
|
60
|
+
#
|
|
61
|
+
# However, the main use case for require_relative is to load files from your
|
|
62
|
+
# own project. Projects managed by Zeitwerk don't do this for files managed by
|
|
63
|
+
# Zeitwerk, precisely.
|
|
64
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zeitwerk::ConstAdded # :nodoc:
|
|
4
|
+
#: (Symbol) -> void
|
|
5
|
+
def const_added(cname)
|
|
6
|
+
if loader = Zeitwerk::Registry.explicit_namespaces.loader_for(self, cname)
|
|
7
|
+
namespace = const_get(cname, false)
|
|
8
|
+
cref = Zeitwerk::Cref.new(self, cname)
|
|
9
|
+
|
|
10
|
+
unless namespace.is_a?(Module)
|
|
11
|
+
raise Zeitwerk::Error, "#{cref} is expected to be a namespace, should be a class or module (got #{namespace.class})"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
loader.__on_namespace_loaded(cref, namespace)
|
|
15
|
+
end
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Module.prepend(self)
|
|
20
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Description of the structure
|
|
4
|
+
# ----------------------------
|
|
5
|
+
#
|
|
6
|
+
# This class emulates a hash table whose keys are of type Zeitwerk::Cref.
|
|
7
|
+
#
|
|
8
|
+
# It is a synchronized 2-level hash.
|
|
9
|
+
#
|
|
10
|
+
# The keys of the top one, stored in `@map`, are class and module objects, but
|
|
11
|
+
# their hash code is forced to be their object IDs because class and module
|
|
12
|
+
# objects may not be hashable (https://github.com/fxn/zeitwerk/issues/188).
|
|
13
|
+
#
|
|
14
|
+
# Then, each one of them stores a hash table with their constants and values.
|
|
15
|
+
# Constants are stored as symbols.
|
|
16
|
+
#
|
|
17
|
+
# For example, if we store values 0, 1, and 2 for the crefs that would
|
|
18
|
+
# correspond to `M::X`, `M::Y`, and `N::Z`, the map will look like this:
|
|
19
|
+
#
|
|
20
|
+
# { M => { X: 0, Y: 1 }, N => { Z: 2 } }
|
|
21
|
+
#
|
|
22
|
+
# This structure is internal, so only the needed interface is implemented.
|
|
23
|
+
#
|
|
24
|
+
# Alternative approaches
|
|
25
|
+
# -----------------------
|
|
26
|
+
#
|
|
27
|
+
# 1. We could also use a 1-level hash whose keys are constant paths. In the
|
|
28
|
+
# example above it would be:
|
|
29
|
+
#
|
|
30
|
+
# { 'M::X' => 0, 'M::Y' => 1, 'N::Z' => 2 }
|
|
31
|
+
#
|
|
32
|
+
# The gem used this approach for several years.
|
|
33
|
+
#
|
|
34
|
+
# 2. Write a custom `hash`/`eql?` in Zeitwerk::Cref. Hash code would be
|
|
35
|
+
#
|
|
36
|
+
# real_mod_hash(@mod) ^ @cname.hash
|
|
37
|
+
#
|
|
38
|
+
# where `real_mod_hash(@mod)` would actually be a call to the real `hash`
|
|
39
|
+
# method in Module. Like what we do for module names to bypass overrides.
|
|
40
|
+
#
|
|
41
|
+
# 3. Similar to 2, but use
|
|
42
|
+
#
|
|
43
|
+
# @mod.object_id ^ @cname.object_id
|
|
44
|
+
#
|
|
45
|
+
# as hash code instead.
|
|
46
|
+
#
|
|
47
|
+
# Benchmarks
|
|
48
|
+
# ----------
|
|
49
|
+
#
|
|
50
|
+
# Writing:
|
|
51
|
+
#
|
|
52
|
+
# map - baseline
|
|
53
|
+
# (3) - 1.74x slower
|
|
54
|
+
# (2) - 2.91x slower
|
|
55
|
+
# (1) - 3.87x slower
|
|
56
|
+
#
|
|
57
|
+
# Reading:
|
|
58
|
+
#
|
|
59
|
+
# map - baseline
|
|
60
|
+
# (3) - 1.99x slower
|
|
61
|
+
# (2) - 2.80x slower
|
|
62
|
+
# (1) - 3.48x slower
|
|
63
|
+
#
|
|
64
|
+
# Extra ball
|
|
65
|
+
# ----------
|
|
66
|
+
#
|
|
67
|
+
# In addition to that, the map is synchronized and provides `delete_mod_cname`,
|
|
68
|
+
# which is ad-hoc for the hot path in `const_added`, we do not need to create
|
|
69
|
+
# unnecessary cref objects for constants we do not manage (but we do not know in
|
|
70
|
+
# advance there).
|
|
71
|
+
|
|
72
|
+
#: [Value]
|
|
73
|
+
class Zeitwerk::Cref::Map # :nodoc: all
|
|
74
|
+
#: () -> void
|
|
75
|
+
def initialize
|
|
76
|
+
@map = {}
|
|
77
|
+
@map.compare_by_identity
|
|
78
|
+
@mutex = Mutex.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#: (Zeitwerk::Cref, Value) -> Value
|
|
82
|
+
def []=(cref, value)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
cnames = (@map[cref.mod] ||= {})
|
|
85
|
+
cnames[cref.cname] = value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#: (Zeitwerk::Cref) -> Value?
|
|
90
|
+
def [](cref)
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
@map[cref.mod]&.[](cref.cname)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#: (Zeitwerk::Cref, { () -> Value }) -> Value
|
|
97
|
+
def get_or_set(cref, &block)
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
cnames = (@map[cref.mod] ||= {})
|
|
100
|
+
cnames.fetch(cref.cname) { cnames[cref.cname] = block.call }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
#: (Zeitwerk::Cref) -> Value?
|
|
105
|
+
def delete(cref)
|
|
106
|
+
delete_mod_cname(cref.mod, cref.cname)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer
|
|
110
|
+
# to not create a cref in every call, since that is global.
|
|
111
|
+
#
|
|
112
|
+
#: (Module, Symbol) -> Value?
|
|
113
|
+
def delete_mod_cname(mod, cname)
|
|
114
|
+
@mutex.synchronize do
|
|
115
|
+
if cnames = @map[mod]
|
|
116
|
+
value = cnames.delete(cname)
|
|
117
|
+
@map.delete(mod) if cnames.empty?
|
|
118
|
+
value
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (Value) -> void
|
|
124
|
+
def delete_by_value(value)
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
@map.delete_if do |mod, cnames|
|
|
127
|
+
cnames.delete_if { _2 == value }
|
|
128
|
+
cnames.empty?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Order of yielded crefs is undefined.
|
|
134
|
+
#
|
|
135
|
+
#: () { (Zeitwerk::Cref) -> void } -> void
|
|
136
|
+
def each_key
|
|
137
|
+
@mutex.synchronize do
|
|
138
|
+
@map.each do |mod, cnames|
|
|
139
|
+
cnames.each_key do |cname|
|
|
140
|
+
yield Zeitwerk::Cref.new(mod, cname)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#: () -> void
|
|
147
|
+
def clear
|
|
148
|
+
@mutex.synchronize do
|
|
149
|
+
@map.clear
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
#: () -> bool
|
|
154
|
+
def empty? # for tests
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
@map.empty?
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This private class encapsulates pairs (mod, cname).
|
|
4
|
+
#
|
|
5
|
+
# Objects represent the constant `cname` in the class or module object `mod`,
|
|
6
|
+
# and have API to manage them. Examples:
|
|
7
|
+
#
|
|
8
|
+
# cref.path
|
|
9
|
+
# cref.set(value)
|
|
10
|
+
# cref.get
|
|
11
|
+
#
|
|
12
|
+
# The constant may or may not exist in `mod`.
|
|
13
|
+
class Zeitwerk::Cref
|
|
14
|
+
require_relative 'cref/map'
|
|
15
|
+
|
|
16
|
+
include Zeitwerk::RealModName
|
|
17
|
+
|
|
18
|
+
#: Module
|
|
19
|
+
attr_reader :mod
|
|
20
|
+
|
|
21
|
+
#: Symbol
|
|
22
|
+
attr_reader :cname
|
|
23
|
+
|
|
24
|
+
# The type of the first argument is Module because Class < Module, class
|
|
25
|
+
# objects are also valid.
|
|
26
|
+
#
|
|
27
|
+
#: (Module, Symbol) -> void
|
|
28
|
+
def initialize(mod, cname)
|
|
29
|
+
@mod = mod
|
|
30
|
+
@cname = cname
|
|
31
|
+
@path = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: () -> String
|
|
35
|
+
def path
|
|
36
|
+
@path ||= Object == @mod ? @cname.name : "#{real_mod_name(@mod)}::#{@cname.name}".freeze
|
|
37
|
+
end
|
|
38
|
+
alias to_s path
|
|
39
|
+
|
|
40
|
+
#: () -> String?
|
|
41
|
+
def autoload?
|
|
42
|
+
@mod.autoload?(@cname, false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#: (String) -> nil
|
|
46
|
+
def autoload(abspath)
|
|
47
|
+
@mod.autoload(@cname, abspath)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: () -> bool
|
|
51
|
+
def defined?
|
|
52
|
+
@mod.const_defined?(@cname, false)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: (top) -> top
|
|
56
|
+
def set(value)
|
|
57
|
+
@mod.const_set(@cname, value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: () -> top ! NameError
|
|
61
|
+
def get
|
|
62
|
+
@mod.const_get(@cname, false)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: () -> void ! NameError
|
|
66
|
+
def remove
|
|
67
|
+
@mod.__send__(:remove_const, @cname)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#: () -> String?
|
|
71
|
+
def location
|
|
72
|
+
if (location = @mod.const_source_location(@cname)) && !location.empty?
|
|
73
|
+
location.join(':')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/zeitwerk/error.rb
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Zeitwerk
|
|
2
4
|
class Error < StandardError
|
|
3
5
|
end
|
|
4
6
|
|
|
5
7
|
class ReloadingDisabledError < Error
|
|
8
|
+
#: () -> void
|
|
9
|
+
def initialize
|
|
10
|
+
super("can't reload, please call loader.enable_reloading before setup")
|
|
11
|
+
end
|
|
6
12
|
end
|
|
7
13
|
|
|
8
14
|
class NameError < ::NameError
|
|
9
15
|
end
|
|
16
|
+
|
|
17
|
+
class SetupRequired < Error
|
|
18
|
+
#: () -> void
|
|
19
|
+
def initialize
|
|
20
|
+
super('please, finish your configuration and call Zeitwerk::Loader#setup once all is ready')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ConflictingNamespaceDefinitionError < Error
|
|
25
|
+
#: (String, location: String?, conflicting_file: String) -> void
|
|
26
|
+
def initialize(cpath, location:, conflicting_file:)
|
|
27
|
+
if location
|
|
28
|
+
super("conflicting namespace definition for #{cpath}: #{conflicting_file} conflicts with #{location}")
|
|
29
|
+
else
|
|
30
|
+
super("conflicting namespace definition for #{cpath}: #{conflicting_file} conflicts with an already defined namespace")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
10
34
|
end
|
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Zeitwerk
|
|
4
4
|
class GemInflector < Inflector
|
|
5
|
-
|
|
5
|
+
#: (String) -> void
|
|
6
6
|
def initialize(root_file)
|
|
7
|
-
namespace = File.basename(root_file,
|
|
8
|
-
|
|
9
|
-
@version_file = File.join(
|
|
7
|
+
namespace = File.basename(root_file, '.rb')
|
|
8
|
+
root_dir = File.dirname(root_file)
|
|
9
|
+
@version_file = File.join(root_dir, namespace, 'version.rb')
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
# @param abspath [String]
|
|
14
|
-
# @return [String]
|
|
12
|
+
#: (String, String) -> String
|
|
15
13
|
def camelize(basename, abspath)
|
|
16
|
-
abspath == @version_file ?
|
|
14
|
+
abspath == @version_file ? 'VERSION' : super
|
|
17
15
|
end
|
|
18
16
|
end
|
|
19
17
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zeitwerk
|
|
4
|
+
# @private
|
|
5
|
+
class GemLoader < Loader
|
|
6
|
+
include RealModName
|
|
7
|
+
|
|
8
|
+
# Users should not create instances directly, the public interface is
|
|
9
|
+
# `Zeitwerk::Loader.for_gem`.
|
|
10
|
+
private_class_method :new
|
|
11
|
+
|
|
12
|
+
# @private
|
|
13
|
+
#: (String, namespace: Module, warn_on_extra_files: boolish) -> Zeitwerk::GemLoader
|
|
14
|
+
def self.__new(root_file, namespace:, warn_on_extra_files:)
|
|
15
|
+
new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: (String, namespace: Module, warn_on_extra_files: boolish) -> void
|
|
19
|
+
def initialize(root_file, namespace:, warn_on_extra_files:)
|
|
20
|
+
super()
|
|
21
|
+
|
|
22
|
+
@tag = File.basename(root_file, '.rb')
|
|
23
|
+
@tag = real_mod_name(namespace) + '-' + @tag unless namespace.equal?(Object)
|
|
24
|
+
|
|
25
|
+
@inflector = GemInflector.new(root_file)
|
|
26
|
+
@root_file = File.expand_path(root_file)
|
|
27
|
+
@root_dir = File.dirname(root_file)
|
|
28
|
+
@warn_on_extra_files = warn_on_extra_files
|
|
29
|
+
|
|
30
|
+
push_dir(@root_dir, namespace: namespace)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: () -> void
|
|
34
|
+
def setup
|
|
35
|
+
warn_on_extra_files if @warn_on_extra_files
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
#: () -> void
|
|
42
|
+
def warn_on_extra_files
|
|
43
|
+
expected_namespace_dir = @root_file.delete_suffix('.rb')
|
|
44
|
+
|
|
45
|
+
@fs.ls(@root_dir) do |basename, abspath, ftype|
|
|
46
|
+
next if abspath == @root_file
|
|
47
|
+
next if abspath == expected_namespace_dir
|
|
48
|
+
|
|
49
|
+
basename_without_ext = basename.delete_suffix('.rb')
|
|
50
|
+
cname = cname_for(basename_without_ext, abspath)
|
|
51
|
+
|
|
52
|
+
warn(<<~EOS)
|
|
53
|
+
WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}
|
|
54
|
+
|
|
55
|
+
#{abspath}
|
|
56
|
+
|
|
57
|
+
To prevent that, please configure the loader to ignore it:
|
|
58
|
+
|
|
59
|
+
loader.ignore("\#{__dir__}/#{basename}")
|
|
60
|
+
|
|
61
|
+
Otherwise, there is a flag to silence this warning:
|
|
62
|
+
|
|
63
|
+
Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
64
|
+
EOS
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/zeitwerk/inflector.rb
CHANGED
|
@@ -5,33 +5,30 @@ module Zeitwerk
|
|
|
5
5
|
# Very basic snake case -> camel case conversion.
|
|
6
6
|
#
|
|
7
7
|
# inflector = Zeitwerk::Inflector.new
|
|
8
|
-
# inflector.camelize(
|
|
9
|
-
# inflector.camelize(
|
|
10
|
-
# inflector.camelize(
|
|
8
|
+
# inflector.camelize('post', ...) # => 'Post'
|
|
9
|
+
# inflector.camelize('users_controller', ...) # => 'UsersController'
|
|
10
|
+
# inflector.camelize('api', ...) # => 'Api'
|
|
11
11
|
#
|
|
12
12
|
# Takes into account hard-coded mappings configured with `inflect`.
|
|
13
13
|
#
|
|
14
|
-
|
|
15
|
-
# @param _abspath [String]
|
|
16
|
-
# @return [String]
|
|
14
|
+
#: (String, String) -> String
|
|
17
15
|
def camelize(basename, _abspath)
|
|
18
|
-
overrides[basename] || basename.split('_').
|
|
16
|
+
overrides[basename] || basename.split('_').each(&:capitalize!).join
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
# Configures hard-coded inflections:
|
|
22
20
|
#
|
|
23
21
|
# inflector = Zeitwerk::Inflector.new
|
|
24
22
|
# inflector.inflect(
|
|
25
|
-
#
|
|
26
|
-
#
|
|
23
|
+
# 'html_parser' => 'HTMLParser',
|
|
24
|
+
# 'mysql_adapter' => 'MySQLAdapter'
|
|
27
25
|
# )
|
|
28
26
|
#
|
|
29
|
-
# inflector.camelize(
|
|
30
|
-
# inflector.camelize(
|
|
31
|
-
# inflector.camelize(
|
|
27
|
+
# inflector.camelize('html_parser', abspath) # => 'HTMLParser'
|
|
28
|
+
# inflector.camelize('mysql_adapter', abspath) # => 'MySQLAdapter'
|
|
29
|
+
# inflector.camelize('users_controller', abspath) # => 'UsersController'
|
|
32
30
|
#
|
|
33
|
-
|
|
34
|
-
# @return [void]
|
|
31
|
+
#: (Hash[String, String]) -> void
|
|
35
32
|
def inflect(inflections)
|
|
36
33
|
overrides.merge!(inflections)
|
|
37
34
|
end
|
|
@@ -41,7 +38,7 @@ module Zeitwerk
|
|
|
41
38
|
# Hard-coded basename to constant name user maps that override the default
|
|
42
39
|
# inflection logic.
|
|
43
40
|
#
|
|
44
|
-
|
|
41
|
+
#: () -> Hash[String, String]
|
|
45
42
|
def overrides
|
|
46
43
|
@overrides ||= {}
|
|
47
44
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This is a private module.
|
|
4
|
+
module Zeitwerk::Internal
|
|
5
|
+
#: (Symbol) -> void
|
|
6
|
+
def internal(method_name)
|
|
7
|
+
private method_name
|
|
8
|
+
|
|
9
|
+
mangled = "__#{method_name}"
|
|
10
|
+
alias_method mangled, method_name
|
|
11
|
+
public mangled
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,32 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zeitwerk::Loader::Callbacks # :nodoc: all
|
|
4
|
+
extend Zeitwerk::Internal
|
|
3
5
|
|
|
4
6
|
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
|
5
7
|
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# @return [void]
|
|
9
|
-
def on_file_autoloaded(file)
|
|
8
|
+
#: (String) -> void ! Zeitwerk::NameError
|
|
9
|
+
internal def on_file_autoloaded(file)
|
|
10
10
|
cref = autoloads.delete(file)
|
|
11
|
-
to_unload[cpath(*cref)] = [file, cref] if reloading_enabled?
|
|
12
|
-
Zeitwerk::Registry.unregister_autoload(file)
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
Zeitwerk::Registry.autoloads.unregister(file)
|
|
13
|
+
|
|
14
|
+
if cref.defined?
|
|
15
|
+
log { "constant #{cref} loaded from file #{file}" }
|
|
16
|
+
to_unload[file] = cref if reloading_enabled?
|
|
17
|
+
run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
|
|
18
|
+
else
|
|
19
|
+
msg = "expected file #{file} to define constant #{cref}, but didn't"
|
|
20
|
+
log { msg }
|
|
21
|
+
|
|
22
|
+
# Ruby still keeps the autoload defined, but we remove it because the
|
|
23
|
+
# contract in Zeitwerk is more strict.
|
|
24
|
+
cref.remove
|
|
25
|
+
|
|
26
|
+
# Since the expected constant was not defined, there is nothing to unload.
|
|
27
|
+
# However, if the exception is rescued and reloading is enabled, we still
|
|
28
|
+
# need to deleted the file from $LOADED_FEATURES.
|
|
29
|
+
to_unload[file] = cref if reloading_enabled?
|
|
30
|
+
|
|
31
|
+
raise Zeitwerk::NameError.new(msg, cref.cname)
|
|
18
32
|
end
|
|
19
33
|
end
|
|
20
34
|
|
|
21
35
|
# Invoked from our decorated Kernel#require when a managed directory is
|
|
22
36
|
# autoloaded.
|
|
23
37
|
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
# directories ourselves, so the callback needs to account for concurrency.
|
|
38
|
+
#: (String) -> void
|
|
39
|
+
internal def on_dir_autoloaded(dir)
|
|
40
|
+
# Module#autoload does not serialize concurrent requires in CRuby < 3.2, and
|
|
41
|
+
# we handle directories ourselves without going through Kernel#require, so
|
|
42
|
+
# the callback needs to account for concurrency.
|
|
30
43
|
#
|
|
31
44
|
# Multi-threading would introduce a race condition here in which thread t1
|
|
32
45
|
# autovivifies the module, and while autoloads for its children are being
|
|
@@ -35,13 +48,13 @@ module Zeitwerk::Loader::Callbacks
|
|
|
35
48
|
# Without the mutex and subsequent delete call, t2 would reset the module.
|
|
36
49
|
# That not only would reassign the constant (undesirable per se) but, worse,
|
|
37
50
|
# the module object created by t2 wouldn't have any of the autoloads for its
|
|
38
|
-
# children, since t1 would have correctly deleted its
|
|
39
|
-
|
|
51
|
+
# children, since t1 would have correctly deleted its namespace_dirs entry.
|
|
52
|
+
dirs_autoload_monitor.synchronize do
|
|
40
53
|
if cref = autoloads.delete(dir)
|
|
41
|
-
|
|
42
|
-
log
|
|
54
|
+
implicit_namespace = cref.set(Module.new)
|
|
55
|
+
log { "module #{cref} autovivified from directory #{dir}" }
|
|
43
56
|
|
|
44
|
-
to_unload[
|
|
57
|
+
to_unload[dir] = cref if reloading_enabled?
|
|
45
58
|
|
|
46
59
|
# We don't unregister `dir` in the registry because concurrent threads
|
|
47
60
|
# wouldn't find a loader associated to it in Kernel#require and would
|
|
@@ -49,23 +62,35 @@ module Zeitwerk::Loader::Callbacks
|
|
|
49
62
|
# these to be able to unregister later if eager loading.
|
|
50
63
|
autoloaded_dirs << dir
|
|
51
64
|
|
|
52
|
-
on_namespace_loaded(
|
|
65
|
+
on_namespace_loaded(cref, implicit_namespace)
|
|
66
|
+
|
|
67
|
+
run_on_load_callbacks(cref.path, implicit_namespace, dir) unless on_load_callbacks.empty?
|
|
53
68
|
end
|
|
54
69
|
end
|
|
55
70
|
end
|
|
56
71
|
|
|
57
|
-
# Invoked when a
|
|
58
|
-
#
|
|
59
|
-
#
|
|
72
|
+
# Invoked when a namespace is created, either from const_added or from module
|
|
73
|
+
# autovivification. If the namespace has matching subdirectories, we descend
|
|
74
|
+
# into them now.
|
|
60
75
|
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
subdirs.each do |subdir|
|
|
67
|
-
set_autoloads_in_dir(subdir, namespace)
|
|
76
|
+
#: (Zeitwerk::Cref, Module) -> void
|
|
77
|
+
internal def on_namespace_loaded(cref, namespace)
|
|
78
|
+
if dirs = namespace_dirs.delete(cref)
|
|
79
|
+
dirs.each do |dir|
|
|
80
|
+
define_autoloads_for_dir(dir, namespace, external: false)
|
|
68
81
|
end
|
|
69
82
|
end
|
|
70
83
|
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
#: (String, top, String) -> void
|
|
88
|
+
def run_on_load_callbacks(cpath, value, abspath)
|
|
89
|
+
# Order matters. If present, run the most specific one.
|
|
90
|
+
callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
|
|
91
|
+
callbacks&.each { |c| c.call(value, abspath) }
|
|
92
|
+
|
|
93
|
+
callbacks = on_load_callbacks[:ANY]
|
|
94
|
+
callbacks&.each { |c| c.call(cpath, value, abspath) }
|
|
95
|
+
end
|
|
71
96
|
end
|