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.
@@ -0,0 +1,6 @@
1
+ class Zeitwerk::NullInflector
2
+ #: (String, String) -> String
3
+ def camelize(basename, _abspath)
4
+ basename
5
+ end
6
+ end
@@ -1,15 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk::RealModName
4
+ #: UnboundMethod
2
5
  UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
3
6
  private_constant :UNBOUND_METHOD_MODULE_NAME
4
7
 
5
- # Returns the real name of the class or module, as set after the first
6
- # constant to which it was assigned (or nil).
8
+ # Returns the real name of the class or module.
7
9
  #
8
- # The name method can be overridden, hence the indirection in this method.
10
+ # We need this indirection because the `name` method can be overridden, and
11
+ # because in practice what we really need is the constant paths of modules
12
+ # with a permanent name, not so much what the user considers to be the name of
13
+ # a certain class or module of theirs.
9
14
  #
10
- # @param mod [Class, Module]
11
- # @return [String, nil]
15
+ #: (Module) -> String?
12
16
  def real_mod_name(mod)
13
- UNBOUND_METHOD_MODULE_NAME.bind(mod).call
17
+ UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
14
18
  end
15
19
  end
@@ -0,0 +1,38 @@
1
+ module Zeitwerk::Registry
2
+ class Autoloads # :nodoc:
3
+ #: () -> void
4
+ def initialize
5
+ @autoloads = {} #: Hash[String, Zeitwerk::Loader]
6
+ end
7
+
8
+ #: (String, Zeitwerk::Loader) -> Zeitwerk::Loader
9
+ def register(abspath, loader)
10
+ @autoloads[abspath] = loader
11
+ end
12
+
13
+ #: (String) -> Zeitwerk::Loader?
14
+ def registered?(path)
15
+ @autoloads[path]
16
+ end
17
+
18
+ #: (String) -> Zeitwerk::Loader?
19
+ def unregister(abspath)
20
+ @autoloads.delete(abspath)
21
+ end
22
+
23
+ #: (Zeitwerk::Loader) -> void
24
+ def unregister_loader(loader)
25
+ @autoloads.delete_if { _2 == loader }
26
+ end
27
+
28
+ #: () -> bool
29
+ def empty? # for tests
30
+ @autoloads.empty?
31
+ end
32
+
33
+ #: () -> void
34
+ def clear # for tests
35
+ @autoloads.clear
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zeitwerk::Registry
4
+ # A registry for explicit namespaces.
5
+ #
6
+ # When a loader determines that a certain file should define an explicit
7
+ # namespace, it registers it here, associating its cref with itself.
8
+ #
9
+ # If the namespace is autoloaded, our const_added callback retrieves its
10
+ # loader by calling loader_for. That way, the loader is able to scan the
11
+ # subdirectories that conform the namespace and set autoloads for their
12
+ # expected constants just in time.
13
+ #
14
+ # Once autoloaded, the namespace is unregistered.
15
+ #
16
+ # The implementation assumes an explicit namespace is managed by one loader.
17
+ # Loaders that reopen namespaces owned by other projects are responsible for
18
+ # loading their constant before setup. This is documented.
19
+ #
20
+ # **This is a private module.**
21
+ class ExplicitNamespaces # :nodoc: all
22
+ #: () -> void
23
+ def initialize
24
+ # Maps crefs of explicit namespaces with their corresponding loader.
25
+ #
26
+ # Entries are added as the namespaces are found, and removed as they are
27
+ # autoloaded.
28
+ @loaders = Zeitwerk::Cref::Map.new
29
+ end
30
+
31
+ # Registers `cref` as being the constant path of an explicit namespace
32
+ # managed by `loader`.
33
+ #
34
+ #: (Zeitwerk::Cref, Zeitwerk::Loader) -> void
35
+ def register(cref, loader)
36
+ @loaders[cref] = loader
37
+ end
38
+
39
+ #: (Module, Symbol) -> Zeitwerk::Loader?
40
+ def loader_for(mod, cname)
41
+ @loaders.delete_mod_cname(mod, cname)
42
+ end
43
+
44
+ #: (Zeitwerk::Loader) -> void
45
+ def unregister_loader(loader)
46
+ @loaders.delete_by_value(loader)
47
+ end
48
+
49
+ # This is an internal method only used by the test suite.
50
+ #
51
+ #: (Zeitwerk::Cref) -> Zeitwerk::Loader?
52
+ def registered?(cref)
53
+ @loaders[cref]
54
+ end
55
+
56
+ #: () -> void
57
+ def clear # for tests
58
+ @loaders.clear
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ module Zeitwerk::Registry
2
+ # Loaders know their own inceptions, but there is a use case in which we need
3
+ # to know if a given cpath is an inception globally. This is what this
4
+ # registry is for.
5
+ class Inceptions # :nodoc:
6
+ #: () -> void
7
+ def initialize
8
+ @inceptions = Zeitwerk::Cref::Map.new #: Zeitwerk::Cref::Map[String]
9
+ end
10
+
11
+ #: (Zeitwerk::Cref, String) -> void
12
+ def register(cref, abspath)
13
+ @inceptions[cref] = abspath
14
+ end
15
+
16
+ #: (Zeitwerk::Cref) -> String?
17
+ def registered?(cref)
18
+ @inceptions[cref]
19
+ end
20
+
21
+ #: (Zeitwerk::Cref) -> void
22
+ def unregister(cref)
23
+ @inceptions.delete(cref)
24
+ end
25
+
26
+ #: () -> void
27
+ def clear # for tests
28
+ @inceptions.clear
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ module Zeitwerk::Registry
2
+ class Loaders # :nodoc:
3
+ #: () -> void
4
+ def initialize
5
+ @loaders = [] #: Array[Zeitwerk::Loader]
6
+ end
7
+
8
+ #: ({ (Zeitwerk::Loader) -> void }) -> void
9
+ def each(&block)
10
+ @loaders.each(&block)
11
+ end
12
+
13
+ #: (Zeitwerk::Loader) -> void
14
+ def register(loader)
15
+ @loaders << loader
16
+ end
17
+
18
+ #: (Zeitwerk::Loader) -> Zeitwerk::Loader?
19
+ def unregister(loader)
20
+ @loaders.delete(loader)
21
+ end
22
+
23
+ #: (Zeitwerk::Loader) -> bool
24
+ def registered?(loader) # for tests
25
+ @loaders.include?(loader)
26
+ end
27
+
28
+ #: () -> void
29
+ def clear # for tests
30
+ @loaders.clear
31
+ end
32
+ end
33
+ end
@@ -2,146 +2,88 @@
2
2
 
3
3
  module Zeitwerk
4
4
  module Registry # :nodoc: all
5
+ require_relative 'registry/autoloads'
6
+ require_relative 'registry/explicit_namespaces'
7
+ require_relative 'registry/inceptions'
8
+ require_relative 'registry/loaders'
9
+
5
10
  class << self
6
11
  # Keeps track of all loaders. Useful to broadcast messages and to prevent
7
12
  # them from being garbage collected.
8
13
  #
9
14
  # @private
10
- # @return [<Zeitwerk::Loader>]
15
+ #: Zeitwerk::Registry::Loaders
11
16
  attr_reader :loaders
12
17
 
13
- # Registers loaders created with `for_gem` to make the method idempotent
14
- # in case of reload.
18
+ # Registers gem loaders to let `for_gem` be idempotent in case of reload.
15
19
  #
16
20
  # @private
17
- # @return [{String => Zeitwerk::Loader}]
18
- attr_reader :loaders_managing_gems
21
+ #: Hash[String, Zeitwerk::Loader]
22
+ attr_reader :gem_loaders_by_root_file
19
23
 
20
- # Maps real paths to the loaders responsible for them.
24
+ # Maps absolute paths to the loaders responsible for them.
21
25
  #
22
26
  # This information is used by our decorated `Kernel#require` to be able to
23
27
  # invoke callbacks and autovivify modules.
24
28
  #
25
29
  # @private
26
- # @return [{String => Zeitwerk::Loader}]
30
+ #: Zeitwerk::Registry::Autoloads
27
31
  attr_reader :autoloads
28
32
 
29
- # This hash table addresses an edge case in which an autoload is ignored.
30
- #
31
- # For example, let's suppose we want to autoload in a gem like this:
32
- #
33
- # # lib/my_gem.rb
34
- # loader = Zeitwerk::Loader.new
35
- # loader.push_dir(__dir__)
36
- # loader.setup
37
- #
38
- # module MyGem
39
- # end
40
- #
41
- # if you require "my_gem", as Bundler would do, this happens while setting
42
- # up autoloads:
43
- #
44
- # 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
45
- # the constant is issued by Zeitwerk while the same file is being
46
- # required.
47
- # 2. The constant `MyGem` is undefined while setup runs.
48
- #
49
- # Therefore, a directory `lib/my_gem` would autovivify a module according to
50
- # the existing information. But that would be wrong.
51
- #
52
- # To overcome this fundamental limitation, we keep track of the constant
53
- # paths that are in this situation ---in the example above, "MyGem"--- and
54
- # take this collection into account for the autovivification logic.
55
- #
56
- # Note that you cannot generally address this by moving the setup code
57
- # below the constant definition, because we want libraries to be able to
58
- # use managed constants in the module body:
59
- #
60
- # module MyGem
61
- # include MyConcern
62
- # end
63
- #
64
33
  # @private
65
- # @return [{String => (String, Zeitwerk::Loader)}]
66
- attr_reader :inceptions
34
+ #: Zeitwerk::Registry::ExplicitNamespaces
35
+ attr_reader :explicit_namespaces
67
36
 
68
- # Registers a loader.
69
- #
70
37
  # @private
71
- # @param loader [Zeitwerk::Loader]
72
- # @return [void]
73
- def register_loader(loader)
74
- loaders << loader
75
- end
38
+ #: Zeitwerk::Registry::Inceptions
39
+ attr_reader :inceptions
76
40
 
77
- # This method returns always a loader, the same instance for the same root
78
- # file. That is how Zeitwerk::Loader.for_gem is idempotent.
79
- #
80
41
  # @private
81
- # @param root_file [String]
82
- # @return [Zeitwerk::Loader]
83
- def loader_for_gem(root_file)
84
- loaders_managing_gems[root_file] ||= begin
85
- Loader.new.tap do |loader|
86
- loader.tag = File.basename(root_file, ".rb")
87
- loader.inflector = GemInflector.new(root_file)
88
- loader.push_dir(File.dirname(root_file))
89
- end
90
- end
42
+ #: (Zeitwerk::Loader) -> void
43
+ def unregister_loader(loader)
44
+ gem_loaders_by_root_file.delete_if { |_, l| l == loader }
91
45
  end
92
46
 
93
- # @private
94
- # @param loader [Zeitwerk::Loader]
95
- # @param realpath [String]
96
- # @return [void]
97
- def register_autoload(loader, realpath)
98
- autoloads[realpath] = loader
99
- end
47
+ #: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
48
+ def conflicting_root_dir?(loader, new_root_dir)
49
+ @mutex.synchronize do
50
+ loaders.each do |existing_loader|
51
+ next if existing_loader == loader
100
52
 
101
- # @private
102
- # @param realpath [String]
103
- # @return [void]
104
- def unregister_autoload(realpath)
105
- autoloads.delete(realpath)
106
- end
53
+ existing_loader.__roots.each_key do |existing_root_dir|
54
+ # Conflicting directories are rare, optimize for the common case.
55
+ next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
107
56
 
108
- # @private
109
- # @param cpath [String]
110
- # @param realpath [String]
111
- # @param loader [Zeitwerk::Loader]
112
- # @return [void]
113
- def register_inception(cpath, realpath, loader)
114
- inceptions[cpath] = [realpath, loader]
115
- end
57
+ new_root_dir_slash = new_root_dir + '/'
58
+ existing_root_dir_slash = existing_root_dir + '/'
59
+ next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
116
60
 
117
- # @private
118
- # @param cpath [String]
119
- # @return [String, nil]
120
- def inception?(cpath)
121
- if pair = inceptions[cpath]
122
- pair.first
123
- end
124
- end
61
+ next if loader.__ignores?(existing_root_dir)
62
+ break if existing_loader.__ignores?(new_root_dir)
125
63
 
126
- # @private
127
- # @param path [String]
128
- # @return [Zeitwerk::Loader, nil]
129
- def loader_for(path)
130
- autoloads[path]
64
+ return existing_loader
65
+ end
66
+ end
67
+
68
+ nil
69
+ end
131
70
  end
132
71
 
72
+ # This method returns always a loader, the same instance for the same root
73
+ # file. That is how Zeitwerk::Loader.for_gem is idempotent.
74
+ #
133
75
  # @private
134
- # @param loader [Zeitwerk::Loader]
135
- # @return [void]
136
- def on_unload(loader)
137
- autoloads.delete_if { |_path, object| object == loader }
138
- inceptions.delete_if { |_cpath, (_path, object)| object == loader }
76
+ #: (String, namespace: Module, warn_on_extra_files: boolish) -> Zeitwerk::Loader
77
+ def loader_for_gem(root_file, namespace:, warn_on_extra_files:)
78
+ gem_loaders_by_root_file[root_file] ||= GemLoader.__new(root_file, namespace: namespace, warn_on_extra_files: warn_on_extra_files)
139
79
  end
140
80
  end
141
81
 
142
- @loaders = []
143
- @loaders_managing_gems = {}
144
- @autoloads = {}
145
- @inceptions = {}
82
+ @loaders = Loaders.new
83
+ @gem_loaders_by_root_file = {}
84
+ @autoloads = Autoloads.new
85
+ @explicit_namespaces = ExplicitNamespaces.new
86
+ @inceptions = Inceptions.new
87
+ @mutex = Mutex.new
146
88
  end
147
89
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.2.0"
4
+ #: String
5
+ VERSION = '2.8.2'
5
6
  end
data/lib/zeitwerk.rb CHANGED
@@ -1,12 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- require_relative "zeitwerk/real_mod_name"
5
- require_relative "zeitwerk/loader"
6
- require_relative "zeitwerk/registry"
7
- require_relative "zeitwerk/explicit_namespace"
8
- require_relative "zeitwerk/inflector"
9
- require_relative "zeitwerk/gem_inflector"
10
- require_relative "zeitwerk/kernel"
11
- require_relative "zeitwerk/error"
4
+ require_relative 'zeitwerk/real_mod_name'
5
+ require_relative 'zeitwerk/internal'
6
+ require_relative 'zeitwerk/cref'
7
+ require_relative 'zeitwerk/loader'
8
+ require_relative 'zeitwerk/gem_loader'
9
+ require_relative 'zeitwerk/registry'
10
+ require_relative 'zeitwerk/inflector'
11
+ require_relative 'zeitwerk/gem_inflector'
12
+ require_relative 'zeitwerk/null_inflector'
13
+ require_relative 'zeitwerk/error'
14
+ require_relative 'zeitwerk/version'
15
+
16
+ require_relative 'zeitwerk/core_ext/kernel'
17
+ require_relative 'zeitwerk/core_ext/module'
18
+
19
+ # This is a dangerous method.
20
+ #
21
+ # @experimental
22
+ #: () -> void
23
+ def self.with_loader
24
+ loader = Zeitwerk::Loader.new
25
+ yield loader
26
+ ensure
27
+ loader.unregister
28
+ end
12
29
  end
metadata CHANGED
@@ -1,19 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2019-10-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: |2
14
13
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
15
14
  and application may have their own independent autoloader, with its own
16
- configuration, inflector, and logger. Supports autoloading, preloading,
15
+ configuration, inflector, and logger. Supports autoloading,
17
16
  reloading, and eager loading.
18
17
  email: fxn@hashref.com
19
18
  executables: []
@@ -23,21 +22,38 @@ files:
23
22
  - MIT-LICENSE
24
23
  - README.md
25
24
  - lib/zeitwerk.rb
25
+ - lib/zeitwerk/core_ext/kernel.rb
26
+ - lib/zeitwerk/core_ext/module.rb
27
+ - lib/zeitwerk/cref.rb
28
+ - lib/zeitwerk/cref/map.rb
26
29
  - lib/zeitwerk/error.rb
27
- - lib/zeitwerk/explicit_namespace.rb
28
30
  - lib/zeitwerk/gem_inflector.rb
31
+ - lib/zeitwerk/gem_loader.rb
29
32
  - lib/zeitwerk/inflector.rb
30
- - lib/zeitwerk/kernel.rb
33
+ - lib/zeitwerk/internal.rb
31
34
  - lib/zeitwerk/loader.rb
32
35
  - lib/zeitwerk/loader/callbacks.rb
36
+ - lib/zeitwerk/loader/config.rb
37
+ - lib/zeitwerk/loader/constant_path_validator.rb
38
+ - lib/zeitwerk/loader/eager_load.rb
39
+ - lib/zeitwerk/loader/file_system.rb
40
+ - lib/zeitwerk/loader/helpers.rb
41
+ - lib/zeitwerk/null_inflector.rb
33
42
  - lib/zeitwerk/real_mod_name.rb
34
43
  - lib/zeitwerk/registry.rb
44
+ - lib/zeitwerk/registry/autoloads.rb
45
+ - lib/zeitwerk/registry/explicit_namespaces.rb
46
+ - lib/zeitwerk/registry/inceptions.rb
47
+ - lib/zeitwerk/registry/loaders.rb
35
48
  - lib/zeitwerk/version.rb
36
49
  homepage: https://github.com/fxn/zeitwerk
37
50
  licenses:
38
51
  - MIT
39
- metadata: {}
40
- post_install_message:
52
+ metadata:
53
+ homepage_uri: https://github.com/fxn/zeitwerk
54
+ changelog_uri: https://github.com/fxn/zeitwerk/blob/main/CHANGELOG.md
55
+ source_code_uri: https://github.com/fxn/zeitwerk
56
+ bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
41
57
  rdoc_options: []
42
58
  require_paths:
43
59
  - lib
@@ -45,15 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
45
61
  requirements:
46
62
  - - ">="
47
63
  - !ruby/object:Gem::Version
48
- version: 2.4.4
64
+ version: '3.2'
49
65
  required_rubygems_version: !ruby/object:Gem::Requirement
50
66
  requirements:
51
67
  - - ">="
52
68
  - !ruby/object:Gem::Version
53
69
  version: '0'
54
70
  requirements: []
55
- rubygems_version: 3.0.3
56
- signing_key:
71
+ rubygems_version: 4.0.3
57
72
  specification_version: 4
58
73
  summary: Efficient and thread-safe constant autoloader
59
74
  test_files: []
@@ -1,80 +0,0 @@
1
- module Zeitwerk
2
- # Centralizes the logic for the trace point used to detect the creation of
3
- # explicit namespaces, needed to descend into matching subdirectories right
4
- # after the constant has been defined.
5
- #
6
- # The implementation assumes an explicit namespace is managed by one loader.
7
- # Loaders that reopen namespaces owned by other projects are responsible for
8
- # loading their constant before setup. This is documented.
9
- module ExplicitNamespace # :nodoc: all
10
- class << self
11
- include RealModName
12
-
13
- # Maps constant paths that correspond to explicit namespaces according to
14
- # the file system, to the loader responsible for them.
15
- #
16
- # @private
17
- # @return [{String => Zeitwerk::Loader}]
18
- attr_reader :cpaths
19
-
20
- # @private
21
- # @return [Mutex]
22
- attr_reader :mutex
23
-
24
- # @private
25
- # @return [TracePoint]
26
- attr_reader :tracer
27
-
28
- # Asserts `cpath` corresponds to an explicit namespace for which `loader`
29
- # is responsible.
30
- #
31
- # @private
32
- # @param cpath [String]
33
- # @param loader [Zeitwerk::Loader]
34
- # @return [void]
35
- def register(cpath, loader)
36
- mutex.synchronize do
37
- cpaths[cpath] = loader
38
- # We check enabled? because, looking at the C source code, enabling an
39
- # enabled tracer does not seem to be a simple no-op.
40
- tracer.enable unless tracer.enabled?
41
- end
42
- end
43
-
44
- # @private
45
- # @param loader [Zeitwerk::Loader]
46
- # @return [void]
47
- def unregister(loader)
48
- cpaths.delete_if { |_cpath, l| l == loader }
49
- disable_tracer_if_unneeded
50
- end
51
-
52
- def disable_tracer_if_unneeded
53
- mutex.synchronize do
54
- tracer.disable if cpaths.empty?
55
- end
56
- end
57
-
58
- def tracepoint_class_callback(event)
59
- # If the class is a singleton class, we won't do anything with it so we
60
- # can bail out immediately. This is several orders of magnitude faster
61
- # than accessing its name.
62
- return if event.self.singleton_class?
63
-
64
- # Note that it makes sense to compute the hash code unconditionally,
65
- # because the trace point is disabled if cpaths is empty.
66
- if loader = cpaths.delete(real_mod_name(event.self))
67
- loader.on_namespace_loaded(event.self)
68
- disable_tracer_if_unneeded
69
- end
70
- end
71
- end
72
-
73
- @cpaths = {}
74
- @mutex = Mutex.new
75
-
76
- # We go through a method instead of defining a block mainly to have a better
77
- # label when profiling.
78
- @tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
79
- end
80
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kernel
4
- module_function
5
-
6
- # We cannot decorate with prepend + super because Kernel has already been
7
- # included in Object, and changes in ancestors don't get propagated into
8
- # already existing ancestor chains.
9
- alias_method :zeitwerk_original_require, :require
10
-
11
- # @param path [String]
12
- # @return [Boolean]
13
- def require(path)
14
- if loader = Zeitwerk::Registry.loader_for(path)
15
- if path.end_with?(".rb")
16
- zeitwerk_original_require(path).tap do |required|
17
- loader.on_file_autoloaded(path) if required
18
- end
19
- else
20
- loader.on_dir_autoloaded(path)
21
- end
22
- else
23
- zeitwerk_original_require(path).tap do |required|
24
- if required
25
- realpath = $LOADED_FEATURES.last
26
- if loader = Zeitwerk::Registry.loader_for(realpath)
27
- loader.on_file_autoloaded(realpath)
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end