zeitwerk 2.1.6 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43750607caaaa5d4fff5bd52204cafb98fbd5109b51a24d9099ba83c6a2a4990
4
- data.tar.gz: 8ad3251fd93af9e66a9102c13556beff5805a38660d69e4c5de37e2808e76d8f
3
+ metadata.gz: 2af50b4d74832ccd3c08e3445c09a941ed56758c57acd20e8d88436f79e16ea2
4
+ data.tar.gz: 23f693ca2b9fc44870c26991cd91c587031825e9d29c90dd5285f2c7a3731f6a
5
5
  SHA512:
6
- metadata.gz: e6e87877104078f564686a15b81882a8e20a560a36999e0f8b9916aa520dd46fe1a2b7f65cacdb7b6c62d252472b6ce1370939054f026a335e21652881820814
7
- data.tar.gz: 3641d412ab4b9176861b9ed9b28ea7964b6a814ebb72804c96967e511562fb87f424c48d7780345be8ccd2c53bdacfe095e3e5173e166a4185bf389740258ca6
6
+ metadata.gz: 1d47e1a735c5314ec56f9c16d66695222209109dddb7da3a663969b852295eed82d8eee56fc48a48cb6b0604e508940f474533e98c7d824175c344420a64baec
7
+ data.tar.gz: be275743889771c745fc90d300a4cbd01911b5b4547c41ea1df6429af1357b9328fb69293a03a282677295f27528220f180f1ca943f43b5567677b856baa3e06
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2019–ω Xavier Noria
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
  [![Gem Version](https://img.shields.io/gem/v/zeitwerk.svg?style=for-the-badge)](https://rubygems.org/gems/zeitwerk)
6
- [![Build Status](https://img.shields.io/travis/com/fxn/zeitwerk.svg?style=for-the-badge&branch=master)](https://travis-ci.com/fxn/zeitwerk)
6
+ [![Build Status](https://img.shields.io/travis/com/fxn/zeitwerk/master?style=for-the-badge)](https://travis-ci.com/fxn/zeitwerk)
7
7
 
8
8
  <!-- TOC -->
9
9
 
@@ -17,7 +17,6 @@
17
17
  - [Setup](#setup)
18
18
  - [Reloading](#reloading)
19
19
  - [Eager loading](#eager-loading)
20
- - [Preloading](#preloading)
21
20
  - [Inflection](#inflection)
22
21
  - [Zeitwerk::Inflector](#zeitwerkinflector)
23
22
  - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
@@ -29,6 +28,8 @@
29
28
  - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
30
29
  - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
31
30
  - [Edge cases](#edge-cases)
31
+ - [Rules of thumb](#rules-of-thumb)
32
+ - [Autoloading, explicit namespaces, and debuggers](#autoloading-explicit-namespaces-and-debuggers)
32
33
  - [Pronunciation](#pronunciation)
33
34
  - [Supported Ruby versions](#supported-ruby-versions)
34
35
  - [Motivation](#motivation)
@@ -42,15 +43,13 @@
42
43
 
43
44
  Zeitwerk is an efficient and thread-safe code loader for Ruby.
44
45
 
45
- Given a conventional file structure, Zeitwerk loads your project's classes and modules on demand. You don't need to write `require` calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
46
+ Given a [conventional file structure](#file-structure), Zeitwerk is able to load your project's classes and modules on demand (autoloading), or upfront (eager loading). You don't need to write `require` calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
46
47
 
47
- Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`. Furthermore, by design, Zeitwerk does only one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used.
48
+ Zeitwerk is also able to reload code, which may be handy while developing web applications. Coordination is needed to reload in a thread-safe manner. The documentation below explains how to do this.
48
49
 
49
- The library is designed so that each gem and application can have their own loader, independent of each other. Each loader has its own configuration, inflector, and optional logger.
50
+ The gem is designed so that any project, gem dependency, application, etc. can have their own independent loader, coexisting in the same process, managing their own project trees, and independent of each other. Each loader has its own configuration, inflector, and optional logger.
50
51
 
51
- Zeitwerk is also able to reload code, which may be handy for web applications. Coordination is needed to reload in a thread-safe manner. The documentation below explains how to do this.
52
-
53
- Finally, in some production setups it may be optimal to eager load all code upfront. Zeitwerk is able to do that too.
52
+ Internally, Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`. Furthermore, Zeitwerk does only one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used.
54
53
 
55
54
  <a id="markdown-synopsis" name="synopsis"></a>
56
55
  ## Synopsis
@@ -63,11 +62,12 @@ Main interface for gems:
63
62
  require "zeitwerk"
64
63
  loader = Zeitwerk::Loader.for_gem
65
64
  loader.setup # ready!
66
- # loader.eager_load, optionally
67
65
 
68
66
  module MyGem
69
67
  # ...
70
68
  end
69
+
70
+ loader.eager_load # optionally
71
71
  ```
72
72
 
73
73
  Main generic interface:
@@ -200,12 +200,28 @@ loader.setup
200
200
 
201
201
  The loader returned by `Zeitwerk::Loader.for_gem` has the directory of the caller pushed, normally that is the absolute path of `lib`. In that sense, `for_gem` can be used also by projects with a gem structure, even if they are not technically gems. That is, you don't need a gemspec or anything.
202
202
 
203
+ If the main module of a library references project constants at the top-level, Zeitwerk has to be ready to load them. Their definitions, in turn, may reference other project constants. And this is recursive. Therefore, it is important that the `setup` call happens above the main module definition:
204
+
205
+ ```ruby
206
+ # lib/my_gem.rb (main file)
207
+
208
+ require "zeitwerk"
209
+ loader = Zeitwerk::Loader.for_gem
210
+ loader.setup
211
+
212
+ module MyGem
213
+ # Since the setup has been performed, at this point we are already able
214
+ # to reference project constants, in this case MyGem::MyLogger.
215
+ include MyLogger
216
+ end
217
+ ```
218
+
203
219
  Zeitwerk works internally only with absolute paths to avoid costly file searches in `$LOAD_PATH`. Indeed, the root directories do not even need to belong to `$LOAD_PATH`, everything just works by design if they don't.
204
220
 
205
221
  <a id="markdown-reloading" name="reloading"></a>
206
222
  ### Reloading
207
223
 
208
- Zeitwer is able to reload code, but you need to enable this feature:
224
+ Zeitwerk is able to reload code, but you need to enable this feature:
209
225
 
210
226
  ```ruby
211
227
  loader = Zeitwerk::Loader.new
@@ -222,7 +238,7 @@ Enabling reloading after setup raises `Zeitwerk::Error`. Similarly, calling `rel
222
238
 
223
239
  Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading. If reloading is not enabled, Zeitwerk is able to use less memory.
224
240
 
225
- Reloading removes the currently loaded classes and modules, resets the loader so that it will pick whatever is in the file system now, and runs preloads if there are any.
241
+ Reloading removes the currently loaded classes and modules and resets the loader so that it will pick whatever is in the file system now.
226
242
 
227
243
  It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
228
244
 
@@ -248,6 +264,8 @@ loader.setup
248
264
  loader.eager_load # won't eager load the database adapters
249
265
  ```
250
266
 
267
+ In gems, the method needs to be invoked after the main namespace has been defined, as shown in [Synopsis](https://github.com/fxn/zeitwerk#synopsis).
268
+
251
269
  Eager loading is synchronized and idempotent.
252
270
 
253
271
  If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
@@ -260,22 +278,6 @@ This may be handy in top-level services, like web applications.
260
278
 
261
279
  Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
262
280
 
263
- <a id="markdown-preloading" name="preloading"></a>
264
- ### Preloading
265
-
266
- Zeitwerk instances are able to preload files and directories.
267
-
268
- ```ruby
269
- loader.preload("app/models/videogame.rb")
270
- loader.preload("app/models/book.rb")
271
- ```
272
-
273
- The call can happen before `setup` (preloads during setup), or after `setup` (preloads on the spot). Each reload preloads too.
274
-
275
- This is a feature specifically thought for STIs in Rails, preloading the leafs of a STI tree ensures all classes are known when doing a query.
276
-
277
- The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
278
-
279
281
  <a id="markdown-inflection" name="inflection"></a>
280
282
  ### Inflection
281
283
 
@@ -292,14 +294,34 @@ users_controller -> UsersController
292
294
  html_parser -> HtmlParser
293
295
  ```
294
296
 
297
+ The camelize logic can be overridden easily for individual basenames:
298
+
299
+ ```ruby
300
+ loader.inflector.inflect(
301
+ "html_parser" => "HTMLParser",
302
+ "mysql_adapter" => "MySQLAdapter"
303
+ )
304
+ ```
305
+
306
+ The `inflect` method can be invoked several times if you prefer this other style:
307
+
308
+ ```ruby
309
+ loader.inflector.inflect "html_parser" => "HTMLParser"
310
+ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
311
+ ```
312
+
313
+ Overrides need to be configured before calling `setup`.
314
+
295
315
  There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
296
316
 
297
- This is the default inflector.
317
+ Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
298
318
 
299
319
  <a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
300
320
  #### Zeitwerk::GemInflector
301
321
 
302
- The loader instantiated behind the scenes by `Zeitwerk::Loader.for_gem` gets assigned by default an inflector that is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
322
+ This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
323
+
324
+ Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
303
325
 
304
326
  <a id="markdown-custom-inflector" name="custom-inflector"></a>
305
327
  #### Custom inflector
@@ -310,12 +332,9 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
310
332
  # frozen_string_literal: true
311
333
 
312
334
  class MyInflector < Zeitwerk::Inflector
313
- def camelize(basename, _abspath)
314
- case basename
315
- when "api"
316
- "API"
317
- when "mysql_adapter"
318
- "MySQLAdapter"
335
+ def camelize(basename, abspath)
336
+ if basename =~ /\Ahtml_(.*)/
337
+ "HTML" + super($1, abspath)
319
338
  else
320
339
  super
321
340
  end
@@ -335,12 +354,55 @@ loader.inflector = MyInflector.new
335
354
 
336
355
  This needs to be done before calling `setup`.
337
356
 
357
+ If a custom inflector definition in a gem takes too much space in the main file, you can extract it. For example, this is a simple pattern:
358
+
359
+ ```ruby
360
+ # lib/my_gem/inflector.rb
361
+ module MyGem
362
+ class Inflector < Zeitwerk::GemInflector
363
+ ...
364
+ end
365
+ end
366
+
367
+ # lib/my_gem.rb
368
+ require "zeitwerk"
369
+ require_relative "my_gem/inflector"
370
+
371
+ loader = Zeitwerk::Loader.for_gem
372
+ loader.inflector = MyGem::Inflector.new(__FILE__)
373
+ loader.setup
374
+
375
+ module MyGem
376
+ # ...
377
+ end
378
+ ```
379
+
380
+ Since `MyGem` is referenced before the namespace is defined in the main file, it is important to use this style:
381
+
382
+ ```ruby
383
+ # Correct, effectively defines MyGem.
384
+ module MyGem
385
+ class Inflector < Zeitwerk::GemInflector
386
+ # ...
387
+ end
388
+ end
389
+ ```
390
+
391
+ instead of:
392
+
393
+ ```ruby
394
+ # Raises uninitialized constant MyGem (NameError).
395
+ class MyGem::Inflector < Zeitwerk::GemInflector
396
+ # ...
397
+ end
398
+ ```
399
+
338
400
  <a id="markdown-logging" name="logging"></a>
339
401
  ### Logging
340
402
 
341
403
  Zeitwerk is silent by default, but you can ask loaders to trace their activity. Logging is meant just for troubleshooting, shouldn't normally be enabled.
342
404
 
343
- The `log!` mehtod is a quick shortcut to let the loader log to `$stdout`:
405
+ The `log!` method is a quick shortcut to let the loader log to `$stdout`:
344
406
 
345
407
  ```
346
408
  loader.log!
@@ -400,7 +462,7 @@ Zeitwerk ignores automatically any file or directory whose name starts with a do
400
462
 
401
463
  However, sometimes it might still be convenient to tell Zeitwerk to completely ignore some particular Ruby file or directory. That is possible with `ignore`, which accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
402
464
 
403
- You can ignore file names, directory names, and glob patterns. Glob patterns are expanded on setup and on reload.
465
+ You can ignore file names, directory names, and glob patterns. Glob patterns are expanded when they are added and again on each reload.
404
466
 
405
467
  Let's see some use cases.
406
468
 
@@ -461,6 +523,8 @@ The chosen adapter, then, has to be loaded by hand somehow:
461
523
  require "my_gem/db_adapters/#{config[:db_adapter]}"
462
524
  ```
463
525
 
526
+ Note that since the directory is ignored, the required adapter can instantiate another loader to manage its subtree, if desired. Such loader would coexist with the main one just fine.
527
+
464
528
  <a id="markdown-use-case-test-files-mixed-with-implementation-files" name="use-case-test-files-mixed-with-implementation-files"></a>
465
529
  #### Use case: Test files mixed with implementation files
466
530
 
@@ -501,6 +565,30 @@ Trip = Struct.new { ... } # NOT SUPPORTED
501
565
 
502
566
  This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
503
567
 
568
+ <a id="markdown-rules-of-thumb" name="rules-of-thumb"></a>
569
+ ### Rules of thumb
570
+
571
+ 1. Different loaders should manage different directory trees. It is an error condition to configure overlapping root directories in different loaders.
572
+
573
+ 2. Think the mere existence of a file is effectively like writing a `require` call for them, which is executed on demand (autoload) or upfront (eager load).
574
+
575
+ 3. In that line, if two loaders manage files that translate to the same constant in the same namespace, the first one wins, the rest are ignored. Similar to what happens with `require` and `$LOAD_PATH`, only the first occurrence matters.
576
+
577
+ 4. Projects that reopen a namespace defined by some dependency have to ensure said namespace is loaded before setup. That is, the project has to make sure it reopens, rather than define. This is often accomplished just loading the dependency.
578
+
579
+ 5. Objects stored in reloadable constants should not be cached in places that are not reloaded. For example, non-reloadable classes should not subclass a reloadable class, or mixin a reloadable module. Otherwise, after reloading, those classes or module objects would become stale. Referring to constants in dynamic places like method calls or lambdas is fine.
580
+
581
+ 6. In a given process, ideally, there should be at most one loader with reloading enabled. Technically, you can have more, but it may get tricky if one refers to constants managed by the other one. Do that only if you know what you are doing.
582
+
583
+ <a id="markdown-autoloading-explicit-namespaces-and-debuggers" name="autoloading-explicit-namespaces-and-debuggers"></a>
584
+ ### Autoloading, explicit namespaces, and debuggers
585
+
586
+ As of this writing, Zeitwerk is unable to autoload classes or modules that belong to [explicit namespaces](#explicit-namespaces) inside debugger sessions. You'll get a `NameError`.
587
+
588
+ The root cause is that debuggers set trace points, and Zeitwerk does too to support explicit namespaces. A debugger session happens inside a trace point handler, and Ruby does not invoke other handlers from within a running handler. Therefore, the code that manages explicit namespaces in Zeitwerk does not get called by the interpreter. See [this issue](https://github.com/deivid-rodriguez/byebug/issues/564#issuecomment-499413606) for further details.
589
+
590
+ As a workaround, you can eager load. Zeitwerk tries hard to succeed or fail consistently both autoloading and eager loading, so switching to eager loading should not introduce any interference in your debugging logic, generally speaking.
591
+
504
592
  <a id="markdown-pronunciation" name="pronunciation"></a>
505
593
  ## Pronunciation
506
594
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
+ require_relative "zeitwerk/real_mod_name"
4
5
  require_relative "zeitwerk/loader"
5
6
  require_relative "zeitwerk/registry"
6
7
  require_relative "zeitwerk/explicit_namespace"
@@ -4,4 +4,7 @@ module Zeitwerk
4
4
 
5
5
  class ReloadingDisabledError < Error
6
6
  end
7
+
8
+ class NameError < ::NameError
9
+ end
7
10
  end
@@ -8,6 +8,8 @@ module Zeitwerk
8
8
  # loading their constant before setup. This is documented.
9
9
  module ExplicitNamespace # :nodoc: all
10
10
  class << self
11
+ include RealModName
12
+
11
13
  # Maps constant paths that correspond to explicit namespaces according to
12
14
  # the file system, to the loader responsible for them.
13
15
  #
@@ -52,21 +54,27 @@ module Zeitwerk
52
54
  tracer.disable if cpaths.empty?
53
55
  end
54
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
55
71
  end
56
72
 
57
73
  @cpaths = {}
58
74
  @mutex = Mutex.new
59
- @tracer = TracePoint.new(:class) do |event|
60
- # If the class is a singleton class, we won't do anything with it so we can bail out immediately.
61
- # This is several orders of magnitude faster than accessing `Module#name`.
62
- next if event.self.singleton_class?
63
75
 
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(event.self.name)
67
- loader.on_namespace_loaded(event.self)
68
- disable_tracer_if_unneeded
69
- end
70
- end
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))
71
79
  end
72
80
  end
@@ -13,7 +13,7 @@ module Zeitwerk
13
13
  # @param abspath [String]
14
14
  # @return [String]
15
15
  def camelize(basename, abspath)
16
- (basename == "version" && abspath == @version_file) ? "VERSION" : super
16
+ abspath == @version_file ? "VERSION" : super
17
17
  end
18
18
  end
19
19
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- class Inflector # :nodoc:
4
+ class Inflector
5
5
  # Very basic snake case -> camel case conversion.
6
6
  #
7
7
  # inflector = Zeitwerk::Inflector.new
@@ -9,11 +9,41 @@ module Zeitwerk
9
9
  # inflector.camelize("users_controller", ...) # => "UsersController"
10
10
  # inflector.camelize("api", ...) # => "Api"
11
11
  #
12
+ # Takes into account hard-coded mappings configured with `inflect`.
13
+ #
12
14
  # @param basename [String]
13
15
  # @param _abspath [String]
14
16
  # @return [String]
15
17
  def camelize(basename, _abspath)
16
- basename.split('_').map!(&:capitalize!).join
18
+ overrides[basename] || basename.split('_').map!(&:capitalize).join
19
+ end
20
+
21
+ # Configures hard-coded inflections:
22
+ #
23
+ # inflector = Zeitwerk::Inflector.new
24
+ # inflector.inflect(
25
+ # "html_parser" => "HTMLParser",
26
+ # "mysql_adapter" => "MySQLAdapter"
27
+ # )
28
+ #
29
+ # inflector.camelize("html_parser", abspath) # => "HTMLParser"
30
+ # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
+ # inflector.camelize("users_controller", abspath) # => "PostsController"
32
+ #
33
+ # @param inflections [{String => String}]
34
+ # @return [void]
35
+ def inflect(inflections)
36
+ overrides.merge!(inflections)
37
+ end
38
+
39
+ private
40
+
41
+ # Hard-coded basename to constant name user maps that override the default
42
+ # inflection logic.
43
+ #
44
+ # @return [{String => String}]
45
+ def overrides
46
+ @overrides ||= {}
17
47
  end
18
48
  end
19
49
  end
@@ -7,6 +7,7 @@ module Zeitwerk
7
7
  class Loader
8
8
  require_relative "loader/callbacks"
9
9
  include Callbacks
10
+ include RealModName
10
11
 
11
12
  # @return [String]
12
13
  attr_reader :tag
@@ -43,7 +44,7 @@ module Zeitwerk
43
44
  #
44
45
  # @private
45
46
  # @return [Set<String>]
46
- attr_reader :ignored
47
+ attr_reader :ignored_glob_patterns
47
48
 
48
49
  # The actual collection of absolute file and directory names at the time the
49
50
  # ignored glob patterns were expanded. Computed on setup, and recomputed on
@@ -53,23 +54,6 @@ module Zeitwerk
53
54
  # @return [Set<String>]
54
55
  attr_reader :ignored_paths
55
56
 
56
- # A _shadowed file_ is a file managed by this loader that is ignored when
57
- # setting autoloads because its matching constant is taken. Either the
58
- # constant is already defined, or there exists an autoload for it.
59
- #
60
- # Think $LOAD_PATH and require, only the first occurrence of a given
61
- # relative name is loaded.
62
- #
63
- # This set keeps track of the absolute path of shadowed files, to be able to
64
- # skip them while eager loading.
65
- #
66
- # Note this cannot be implemented with do_not_eager_load because these
67
- # files are not autoloadable.
68
- #
69
- # @private
70
- # @return [Set<String>]
71
- attr_reader :shadowed_files
72
-
73
57
  # Maps real absolute paths for which an autoload has been set ---and not
74
58
  # executed--- to their corresponding parent class or module and constant
75
59
  # name.
@@ -149,13 +133,12 @@ module Zeitwerk
149
133
 
150
134
  @root_dirs = {}
151
135
  @preloads = []
152
- @ignored = Set.new
136
+ @ignored_glob_patterns = Set.new
153
137
  @ignored_paths = Set.new
154
138
  @autoloads = {}
155
139
  @autoloaded_dirs = []
156
140
  @to_unload = {}
157
141
  @lazy_subdirs = {}
158
- @shadowed_files = Set.new
159
142
  @eager_load_exclusions = Set.new
160
143
 
161
144
  # TODO: find a better name for these mutexes.
@@ -242,16 +225,12 @@ module Zeitwerk
242
225
  #
243
226
  # @param paths [<String, Pathname, <String, Pathname>>]
244
227
  # @return [void]
245
- def ignore(*paths)
246
- mutex.synchronize { ignored.merge(expand_paths(paths)) }
247
- end
248
-
249
- # @private
250
- # @return [void]
251
- def expand_ignored_glob_patterns
252
- # Note that Dir.glob works with regular file names just fine. That is,
253
- # glob patterns technically need no wildcards.
254
- ignored_paths.replace(ignored.flat_map { |path| Dir.glob(path) })
228
+ def ignore(*glob_patterns)
229
+ glob_patterns = expand_paths(glob_patterns)
230
+ mutex.synchronize do
231
+ ignored_glob_patterns.merge(glob_patterns)
232
+ ignored_paths.merge(expand_glob_patterns(glob_patterns))
233
+ end
255
234
  end
256
235
 
257
236
  # Sets autoloads in the root namespace and preloads files, if any.
@@ -261,7 +240,6 @@ module Zeitwerk
261
240
  mutex.synchronize do
262
241
  break if @setup
263
242
 
264
- expand_ignored_glob_patterns
265
243
  actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
266
244
  do_preload
267
245
 
@@ -324,7 +302,6 @@ module Zeitwerk
324
302
  autoloaded_dirs.clear
325
303
  to_unload.clear
326
304
  lazy_subdirs.clear
327
- shadowed_files.clear
328
305
 
329
306
  Registry.on_unload(self)
330
307
  ExplicitNamespace.unregister(self)
@@ -344,6 +321,7 @@ module Zeitwerk
344
321
  def reload
345
322
  if reloading_enabled?
346
323
  unload
324
+ recompute_ignored_paths
347
325
  setup
348
326
  else
349
327
  raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
@@ -361,20 +339,24 @@ module Zeitwerk
361
339
  break if @eager_loaded
362
340
 
363
341
  queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
364
- while dir = queue.shift
365
- ls(dir) do |_basename, abspath|
342
+ queue.map! { |dir| [Object, dir] }
343
+ while to_eager_load = queue.shift
344
+ namespace, dir = to_eager_load
345
+
346
+ ls(dir) do |basename, abspath|
366
347
  next if eager_load_exclusions.member?(abspath)
367
348
 
368
349
  if ruby?(abspath)
369
- require abspath unless shadowed_files.member?(abspath)
370
- elsif dir?(abspath)
371
- queue << abspath
350
+ if cref = autoloads[File.realpath(abspath)]
351
+ cref[0].const_get(cref[1], false)
352
+ end
353
+ elsif dir?(abspath) && !root_dirs.key?(abspath)
354
+ cname = inflector.camelize(basename, abspath)
355
+ queue << [namespace.const_get(cname, false), abspath]
372
356
  end
373
357
  end
374
358
  end
375
359
 
376
- shadowed_files.clear
377
-
378
360
  autoloaded_dirs.each do |autoloaded_dir|
379
361
  Registry.unregister_autoload(autoloaded_dir)
380
362
  end
@@ -417,6 +399,22 @@ module Zeitwerk
417
399
  @logger = ->(msg) { puts msg }
418
400
  end
419
401
 
402
+ # @private
403
+ # @param dir [String]
404
+ # @return [Boolean]
405
+ def manages?(dir)
406
+ dir = dir + "/"
407
+ ignored_paths.each do |ignored_path|
408
+ return false if dir.start_with?(ignored_path + "/")
409
+ end
410
+
411
+ root_dirs.each_key do |root_dir|
412
+ return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
413
+ end
414
+
415
+ false
416
+ end
417
+
420
418
  # --- Class methods ---------------------------------------------------------------------------
421
419
 
422
420
  class << self
@@ -476,21 +474,38 @@ module Zeitwerk
476
474
  # @return [void]
477
475
  def set_autoloads_in_dir(dir, parent)
478
476
  ls(dir) do |basename, abspath|
479
- if ruby?(basename)
480
- basename.slice!(-3, 3)
481
- cname = inflector.camelize(basename, abspath).to_sym
482
- autoload_file(parent, cname, abspath)
483
- elsif dir?(abspath)
484
- # In a Rails application, `app/models/concerns` is a subdirectory of
485
- # `app/models`, but both of them are root directories.
486
- #
487
- # To resolve the ambiguity file name -> constant path this introduces,
488
- # the `app/models/concerns` directory is totally ignored as a namespace,
489
- # it counts only as root. The guard checks that.
490
- unless root_dirs.key?(abspath)
477
+ begin
478
+ if ruby?(basename)
479
+ basename.slice!(-3, 3)
491
480
  cname = inflector.camelize(basename, abspath).to_sym
492
- autoload_subdir(parent, cname, abspath)
481
+ autoload_file(parent, cname, abspath)
482
+ elsif dir?(abspath)
483
+ # In a Rails application, `app/models/concerns` is a subdirectory of
484
+ # `app/models`, but both of them are root directories.
485
+ #
486
+ # To resolve the ambiguity file name -> constant path this introduces,
487
+ # the `app/models/concerns` directory is totally ignored as a namespace,
488
+ # it counts only as root. The guard checks that.
489
+ unless root_dirs.key?(abspath)
490
+ cname = inflector.camelize(basename, abspath).to_sym
491
+ autoload_subdir(parent, cname, abspath)
492
+ end
493
493
  end
494
+ rescue ::NameError => error
495
+ path_type = ruby?(abspath) ? "file" : "directory"
496
+
497
+ raise NameError, <<~MESSAGE
498
+ #{error.message} inferred by #{inflector.class} from #{path_type}
499
+
500
+ #{abspath}
501
+
502
+ Possible ways to address this:
503
+
504
+ * Tell Zeitwerk to ignore this particular #{path_type}.
505
+ * Tell Zeitwerk to ignore one of its parent directories.
506
+ * Rename the #{path_type} to comply with the naming conventions.
507
+ * Modify the inflector to handle this case.
508
+ MESSAGE
494
509
  end
495
510
  end
496
511
  end
@@ -526,7 +541,6 @@ module Zeitwerk
526
541
  if autoload_path = autoload_for?(parent, cname)
527
542
  # First autoload for a Ruby file wins, just ignore subsequent ones.
528
543
  if ruby?(autoload_path)
529
- shadowed_files.add(file)
530
544
  log("file #{file} is ignored because #{autoload_path} has precedence") if logger
531
545
  else
532
546
  promote_namespace_from_implicit_to_explicit(
@@ -537,7 +551,6 @@ module Zeitwerk
537
551
  )
538
552
  end
539
553
  elsif cdef?(parent, cname)
540
- shadowed_files.add(file)
541
554
  log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
542
555
  else
543
556
  set_autoload(parent, cname, file)
@@ -653,7 +666,7 @@ module Zeitwerk
653
666
  # @param cname [Symbol]
654
667
  # @return [String]
655
668
  def cpath(parent, cname)
656
- parent.equal?(Object) ? cname.to_s : "#{parent.name}::#{cname}"
669
+ parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
657
670
  end
658
671
 
659
672
  # @param dir [String]
@@ -682,7 +695,20 @@ module Zeitwerk
682
695
  # @param paths [<String, Pathname, <String, Pathname>>]
683
696
  # @return [<String>]
684
697
  def expand_paths(paths)
685
- Array(paths).flatten.map! { |path| File.expand_path(path) }
698
+ paths.flatten.map! { |path| File.expand_path(path) }
699
+ end
700
+
701
+ # @param glob_patterns [<String>]
702
+ # @return [<String>]
703
+ def expand_glob_patterns(glob_patterns)
704
+ # Note that Dir.glob works with regular file names just fine. That is,
705
+ # glob patterns technically need no wildcards.
706
+ glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
707
+ end
708
+
709
+ # @return [void]
710
+ def recompute_ignored_paths
711
+ ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
686
712
  end
687
713
 
688
714
  # @param message [String]
@@ -703,16 +729,12 @@ module Zeitwerk
703
729
  def raise_if_conflicting_directory(dir)
704
730
  self.class.mutex.synchronize do
705
731
  Registry.loaders.each do |loader|
706
- next if loader == self
707
-
708
- loader.dirs.each do |already_managed_dir|
709
- if dir.start_with?(already_managed_dir) || already_managed_dir.start_with?(dir)
710
- require "pp"
711
- raise Error,
712
- "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
713
- " which is already managed by\n\n#{loader.pretty_inspect}\n"
714
- EOS
715
- end
732
+ if loader != self && loader.manages?(dir)
733
+ require "pp"
734
+ raise Error,
735
+ "loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
736
+ " which is already managed by\n\n#{loader.pretty_inspect}\n"
737
+ EOS
716
738
  end
717
739
  end
718
740
  end
@@ -1,4 +1,6 @@
1
1
  module Zeitwerk::Loader::Callbacks
2
+ include Zeitwerk::RealModName
3
+
2
4
  # Invoked from our decorated Kernel#require when a managed file is autoloaded.
3
5
  #
4
6
  # @private
@@ -60,7 +62,7 @@ module Zeitwerk::Loader::Callbacks
60
62
  # @param namespace [Module]
61
63
  # @return [void]
62
64
  def on_namespace_loaded(namespace)
63
- if subdirs = lazy_subdirs.delete(namespace.name)
65
+ if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
64
66
  subdirs.each do |subdir|
65
67
  set_autoloads_in_dir(subdir, namespace)
66
68
  end
@@ -0,0 +1,15 @@
1
+ module Zeitwerk::RealModName
2
+ UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
3
+ private_constant :UNBOUND_METHOD_MODULE_NAME
4
+
5
+ # Returns the real name of the class or module, as set after the first
6
+ # constant to which it was assigned (or nil).
7
+ #
8
+ # The name method can be overridden, hence the indirection in this method.
9
+ #
10
+ # @param mod [Class, Module]
11
+ # @return [String, nil]
12
+ def real_mod_name(mod)
13
+ UNBOUND_METHOD_MODULE_NAME.bind(mod).call
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.1.6"
4
+ VERSION = "2.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.6
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xavier Noria
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-30 00:00:00.000000000 Z
11
+ date: 2019-10-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -20,6 +20,7 @@ executables: []
20
20
  extensions: []
21
21
  extra_rdoc_files: []
22
22
  files:
23
+ - MIT-LICENSE
23
24
  - README.md
24
25
  - lib/zeitwerk.rb
25
26
  - lib/zeitwerk/error.rb
@@ -29,6 +30,7 @@ files:
29
30
  - lib/zeitwerk/kernel.rb
30
31
  - lib/zeitwerk/loader.rb
31
32
  - lib/zeitwerk/loader/callbacks.rb
33
+ - lib/zeitwerk/real_mod_name.rb
32
34
  - lib/zeitwerk/registry.rb
33
35
  - lib/zeitwerk/version.rb
34
36
  homepage: https://github.com/fxn/zeitwerk
@@ -50,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
52
  - !ruby/object:Gem::Version
51
53
  version: '0'
52
54
  requirements: []
53
- rubygems_version: 3.0.1
55
+ rubygems_version: 3.0.3
54
56
  signing_key:
55
57
  specification_version: 4
56
58
  summary: Efficient and thread-safe constant autoloader