zeitwerk 2.7.4 → 2.8.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c652e42a3b3a3a92704f1924bac194dc9b9c2db94641e6a9a3daf192480e7c7e
4
- data.tar.gz: f24017f36733460e4c09ebc7d31de8ee95efda8c7e4f6aede34d4824f4922c3f
3
+ metadata.gz: 071170ec551e69be67c517af6e04ae2788d6f81f6451a7da0dfab49be8f0c52b
4
+ data.tar.gz: 96fd655043bee86c5f57567a01b5f5282e315b72bde7e5ca370ee8fd5e7917fb
5
5
  SHA512:
6
- metadata.gz: d1a853bd6e7b638dc60203e6ba515646260913925ff8d30aa7edcb817253e61a534beadca02159ee7dffa78204d5c0dff652a056d2be2d688b9ec52fc1f9ac91
7
- data.tar.gz: 26824d767e2f18ef128b66b2b354a6fc7bc16754734eb27bc3c6bf20c8a3ed36a06e2d20c1cbed6b932a327cbb70f4aa58cedd88ed81bf3f8074ebdf90a52e27
6
+ metadata.gz: 2b74298014af2753fbb19ad36f2a5d484cfbe364beb6aa583fe43d1f2cdf2fc3135bd5aaa16bedeb45a4a4ecbae95fc9d2e62aae5b59fd3df7cf5422693a947f
7
+ data.tar.gz: 242710104b8d2fba905cb65d1382080d098f4ff6c3d8b982d1cf53e9ea877612b22ee07b9d3f8f155a17b354e8412f2f350d835e7418f6eef7cf33a324a5ea4a
data/README.md CHANGED
@@ -19,6 +19,8 @@
19
19
  - [Nested root directories](#nested-root-directories)
20
20
  - [Implicit namespaces](#implicit-namespaces)
21
21
  - [Explicit namespaces](#explicit-namespaces)
22
+ - [Explicit namespaces defined in ordinary files](#explicit-namespaces-defined-in-ordinary-files)
23
+ - [Explicit namespaces defined in nsfiles](#explicit-namespaces-defined-in-nsfiles)
22
24
  - [Collapsing directories](#collapsing-directories)
23
25
  - [Testing compliance](#testing-compliance)
24
26
  - [Usage](#usage)
@@ -58,6 +60,7 @@
58
60
  - [Reopening third-party namespaces](#reopening-third-party-namespaces)
59
61
  - [Introspection](#introspection)
60
62
  - [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
63
+ - [Autoloaded Constants](#autoloaded-constants)
61
64
  - [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
62
65
  - [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
63
66
  - [Encodings](#encodings)
@@ -97,7 +100,7 @@ Main interface for gems:
97
100
  ```ruby
98
101
  # lib/my_gem.rb (main file)
99
102
 
100
- require "zeitwerk"
103
+ require 'zeitwerk'
101
104
  loader = Zeitwerk::Loader.for_gem
102
105
  loader.setup # ready!
103
106
 
@@ -180,7 +183,7 @@ end
180
183
  The first example needs a custom [inflection](#inflection) rule:
181
184
 
182
185
  ```ruby
183
- loader.inflector.inflect("max_retries" => "MAX_RETRIES")
186
+ loader.inflector.inflect('max_retries' => 'MAX_RETRIES')
184
187
  ```
185
188
 
186
189
  Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
@@ -219,8 +222,8 @@ Although `Object` is the most common root namespace, you have the flexibility to
219
222
  For example, given:
220
223
 
221
224
  ```ruby
222
- require "active_job"
223
- require "active_job/queue_adapters"
225
+ require 'active_job'
226
+ require 'active_job/queue_adapters'
224
227
  loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
225
228
  ```
226
229
 
@@ -263,16 +266,23 @@ To trigger this behavior, the directory must contain non-ignored Ruby files with
263
266
  <a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
264
267
  ### Explicit namespaces
265
268
 
266
- Classes and modules that act as namespaces can also be explicitly defined, though. For instance, consider
269
+ Classes and modules that act as namespaces can also be explicitly defined in a file. This can be done with ordinary files named after the corresponding constant path, or with special namespace files, or _nsfiles_ for short.
270
+
271
+ <a id="markdown-explicit-namespaces-defined-in-ordinary-files" name="explicit-namespaces-defined-in-ordinary-files"></a>
272
+ #### Explicit namespaces defined in ordinary files
273
+
274
+ Let's consider:
267
275
 
268
276
  ```
269
277
  app/models/hotel.rb -> Hotel
270
278
  app/models/hotel/pricing.rb -> Hotel::Pricing
271
279
  ```
272
280
 
273
- There, `app/models/hotel.rb` defines `Hotel`, and thus Zeitwerk does not autovivify a module.
281
+ Since there is a file `app/models/hotel.rb` and also a directory `app/models/hotel`, Zeitwerk realizes `Hotel` is a namespace that is defined in `app/models/hotel.rb`.
282
+
283
+ In order to realize this, the directory or directories conforming the namespace do not need to be next to the file, as in the example, they could be in some other root directory.
274
284
 
275
- The classes and modules from the namespace are already available in the body of the class or module defining it:
285
+ The classes and modules from an explicit namespace are already available in the body of the class or module that defines it:
276
286
 
277
287
  ```ruby
278
288
  class Hotel < ApplicationRecord
@@ -283,7 +293,54 @@ end
283
293
 
284
294
  When autoloaded, Zeitwerk verifies the expected constant (`Hotel` in the example) stores a class or module object. If it doesn't, `Zeitwerk::Error` is raised.
285
295
 
286
- An explicit namespace must be managed by one single loader. Loaders that reopen namespaces owned by other projects are responsible for loading their constants before setup.
296
+ <a id="markdown-explicit-namespaces-defined-in-nsfiles" name="explicit-namespaces-defined-in-nsfiles"></a>
297
+ #### Explicit namespaces defined in nsfiles
298
+
299
+ If the loader has an nsfile configured (defaults to `nil`):
300
+
301
+ ```ruby
302
+ loader.nsfile = 'ns.rb' # must be set before setup
303
+ ```
304
+
305
+ you can alternatively define the explicit namespace inside its directory:
306
+
307
+ ```
308
+ my_component/ns.rb # MyComponent
309
+ my_component/widget.rb # MyComponent::Widget
310
+ ```
311
+
312
+ This may be handy for self-contained units for which a `my_component.rb` file in the parent directory would feel unnatural.
313
+
314
+ A loader's nsfile has to be a non-hidden basename with a `.rb` extension, as in the example above. Nsfiles are not inflected, so as long as those conditions hold, they may contain leading underscores, hyphens, etc.
315
+
316
+ Collapsed directories work as expected. For example, if we assume that `src` is collapsed, and that `assets` and `tests` are ignored, you could have the code organized this way:
317
+
318
+ ```
319
+ my_component/src/ns.rb # MyComponent
320
+ my_component/src/widget.rb # MyComponent::Widget
321
+ my_component/assets/widget.js
322
+ my_component/tests/test_widget.rb
323
+ ```
324
+
325
+ Loaders with an nsfile configured also support explicit namespaces defined in ordinary files. The styles are not exclusive. Some parts of the project may be component-oriented, while in other parts ordinary files may feel more natural. That works.
326
+
327
+ However, attempting to define the same namespace using an ordinary file and an nsfile is an error condition that raises `Zeitwerk::ConflictingNamespaceDefinitionError`.
328
+
329
+ Nsfiles in root directories raise `Zeitwerk::ConflictingNamespaceDefinitionError` too, since the namespace in a root directory is externally defined.
330
+
331
+ A project file whose basename is equal to the nsfile is always considered to be an nsfile. You cannot opt out. Therefore, if we have:
332
+
333
+ ```ruby
334
+ loader.nsfile = 'index.rb'
335
+ ```
336
+
337
+ there is no way `foo/index.rb` can define `Foo::Index` in any part of the project, it must define `Foo`.
338
+
339
+ While configurable, `ns.rb` is the recommended convention:
340
+
341
+ * `ns.rb` is short.
342
+ * `ns.rb` suggests "namespace".
343
+ * Needing an `Ns` constant is unlikely.
287
344
 
288
345
  <a id="markdown-collapsing-directories" name="collapsing-directories"></a>
289
346
  ### Collapsing directories
@@ -371,9 +428,9 @@ Conceptually, `for_gem` translates to:
371
428
  ```ruby
372
429
  # lib/my_gem.rb
373
430
 
374
- require "zeitwerk"
431
+ require 'zeitwerk'
375
432
  loader = Zeitwerk::Loader.new
376
- loader.tag = File.basename(__FILE__, ".rb")
433
+ loader.tag = File.basename(__FILE__, '.rb')
377
434
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
378
435
  loader.push_dir(File.dirname(__FILE__))
379
436
  ```
@@ -383,7 +440,7 @@ If the main module references project constants at the top-level, Zeitwerk has t
383
440
  ```ruby
384
441
  # lib/my_gem.rb (main file)
385
442
 
386
- require "zeitwerk"
443
+ require 'zeitwerk'
387
444
  loader = Zeitwerk::Loader.for_gem
388
445
  loader.setup
389
446
 
@@ -429,7 +486,7 @@ Let's suppose you are writing a gem to extend `Net::HTTP` with some niche featur
429
486
  The top-level file mentioned in the last point is optional. In particular, from
430
487
 
431
488
  ```ruby
432
- gem "net-http-niche_feature"
489
+ gem 'net-http-niche_feature'
433
490
  ```
434
491
 
435
492
  if the hyphenated file does not exist, Bundler notes the conventional hyphenated pattern and issues a `require` for `net/http/niche_feature`.
@@ -442,13 +499,13 @@ The structure of the gem would be like this:
442
499
  # lib/net-http-niche_feature.rb (optional)
443
500
 
444
501
  # For technical reasons, this cannot be require_relative.
445
- require "net/http/niche_feature"
502
+ require 'net/http/niche_feature'
446
503
 
447
504
 
448
505
  # lib/net/http/niche_feature.rb
449
506
 
450
- require "net/http"
451
- require "zeitwerk"
507
+ require 'net/http'
508
+ require 'zeitwerk'
452
509
 
453
510
  loader = Zeitwerk::Loader.for_gem_extension(Net::HTTP)
454
511
  loader.setup
@@ -463,7 +520,7 @@ end
463
520
  # lib/net/http/niche_feature/version.rb
464
521
 
465
522
  module Net::HTTP::NicheFeature
466
- VERSION = "1.0.0"
523
+ VERSION = '1.0.0'
467
524
  end
468
525
  ```
469
526
 
@@ -485,7 +542,7 @@ Let's revisit the example above:
485
542
  ```ruby
486
543
  # lib/my_gem.rb (main file)
487
544
 
488
- require "zeitwerk"
545
+ require 'zeitwerk'
489
546
  loader = Zeitwerk::Loader.for_gem
490
547
  loader.setup
491
548
 
@@ -494,7 +551,7 @@ module MyGem
494
551
  end
495
552
  ```
496
553
 
497
- That works, and there is no `require "my_gem/my_logger"`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
554
+ That works, and there is no `require 'my_gem/my_logger'`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
498
555
 
499
556
  If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
500
557
 
@@ -681,7 +738,7 @@ In order to reload safely, no other thread can be autoloading or reloading concu
681
738
  For example, a web framework that serves each request in its own thread and has reloading enabled could create a read-write lock on boot like this:
682
739
 
683
740
  ```ruby
684
- require "concurrent/atomic/read_write_lock"
741
+ require 'concurrent/atomic/read_write_lock'
685
742
 
686
743
  MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
687
744
  ```
@@ -726,22 +783,22 @@ The camelize logic can be overridden easily for individual basenames:
726
783
 
727
784
  ```ruby
728
785
  loader.inflector.inflect(
729
- "html_parser" => "HTMLParser",
730
- "mysql_adapter" => "MySQLAdapter"
786
+ 'html_parser' => 'HTMLParser',
787
+ 'mysql_adapter' => 'MySQLAdapter'
731
788
  )
732
789
  ```
733
790
 
734
791
  The `inflect` method can be invoked several times if you prefer this other style:
735
792
 
736
793
  ```ruby
737
- loader.inflector.inflect "html_parser" => "HTMLParser"
738
- loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
794
+ loader.inflector.inflect 'html_parser' => 'HTMLParser'
795
+ loader.inflector.inflect 'mysql_adapter' => 'MySQLAdapter'
739
796
  ```
740
797
 
741
798
  Overrides have to match exactly directory or file (without extension) _basenames_. For example, if you configure
742
799
 
743
800
  ```ruby
744
- loader.inflector.inflect("xml" => "XML")
801
+ loader.inflector.inflect('xml' => 'XML')
745
802
  ```
746
803
 
747
804
  then the following constants are expected:
@@ -756,8 +813,8 @@ As you see, any directory whose basename is exactly `xml`, and any file whose ba
756
813
 
757
814
  ```ruby
758
815
  loader.inflector.inflect(
759
- "xml" => "XML",
760
- "xml_parser" => "XMLParser"
816
+ 'xml' => 'XML',
817
+ 'xml_parser' => 'XMLParser'
761
818
  )
762
819
  ```
763
820
 
@@ -812,7 +869,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
812
869
  class MyInflector < Zeitwerk::Inflector
813
870
  def camelize(basename, abspath)
814
871
  if basename =~ /\Ahtml_(.*)/
815
- "HTML" + super($1, abspath)
872
+ 'HTML' + super($1, abspath)
816
873
  else
817
874
  super
818
875
  end
@@ -843,8 +900,8 @@ module MyGem
843
900
  end
844
901
 
845
902
  # lib/my_gem.rb
846
- require "zeitwerk"
847
- require_relative "my_gem/inflector"
903
+ require 'zeitwerk'
904
+ require_relative 'my_gem/inflector'
848
905
 
849
906
  loader = Zeitwerk::Loader.for_gem
850
907
  loader.inflector = MyGem::Inflector.new(__FILE__)
@@ -912,13 +969,13 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
912
969
 
913
970
  ```ruby
914
971
  # config/environments/development.rb
915
- loader.on_load("SomeApiClient") do |klass, _abspath|
916
- klass.endpoint = "https://api.dev"
972
+ loader.on_load('SomeApiClient') do |klass, _abspath|
973
+ klass.endpoint = 'https://api.dev'
917
974
  end
918
975
 
919
976
  # config/environments/production.rb
920
- loader.on_load("SomeApiClient") do |klass, _abspath|
921
- klass.endpoint = "https://api.prod"
977
+ loader.on_load('SomeApiClient') do |klass, _abspath|
978
+ klass.endpoint = 'https://api.prod'
922
979
  end
923
980
  ```
924
981
 
@@ -962,7 +1019,7 @@ When reloading is enabled, you may occasionally need to execute something before
962
1019
  For example, let's imagine that a `Country` class fetches a list of countries and caches them when it is loaded. You might want to clear that cache if unloaded:
963
1020
 
964
1021
  ```ruby
965
- loader.on_unload("Country") do |klass, _abspath|
1022
+ loader.on_unload('Country') do |klass, _abspath|
966
1023
  klass.clear_cache
967
1024
  end
968
1025
  ```
@@ -1047,7 +1104,7 @@ Zeitwerk@9fa54b: autoload set for User, to be loaded from ...
1047
1104
  By default, a random tag like the one above is assigned, but you can change it:
1048
1105
 
1049
1106
  ```
1050
- loader.tag = "grep_me"
1107
+ loader.tag = 'grep_me'
1051
1108
  ```
1052
1109
 
1053
1110
  The tag of a loader returned by `for_gem` is the basename of the root file without extension:
@@ -1103,7 +1160,7 @@ loader.setup
1103
1160
  Now, that file has to be loaded manually with `require` or `require_relative`:
1104
1161
 
1105
1162
  ```ruby
1106
- require_relative "my_gem/core_ext/kernel"
1163
+ require_relative 'my_gem/core_ext/kernel'
1107
1164
  ```
1108
1165
 
1109
1166
  and you can do that anytime, before configuring the loader, or after configuring the loader, does not matter.
@@ -1117,7 +1174,7 @@ Let's imagine your project talks to databases, supports several, and has adapter
1117
1174
 
1118
1175
  ```ruby
1119
1176
  # my_gem/db_adapters/postgresql.rb
1120
- require "pg"
1177
+ require 'pg'
1121
1178
  ```
1122
1179
 
1123
1180
  but you don't want your users to install them all, only the one they are going to use.
@@ -1155,7 +1212,7 @@ loader.setup
1155
1212
  In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
1156
1213
 
1157
1214
  ```ruby
1158
- require "foo"
1215
+ require 'foo'
1159
1216
  ```
1160
1217
 
1161
1218
  the first one found gets loaded, and the rest are ignored.
@@ -1224,10 +1281,10 @@ In order to do so, you need to make sure those modules are loaded before calling
1224
1281
 
1225
1282
  ```ruby
1226
1283
  # Ensure these namespaces are reopened, not defined.
1227
- require "active_job"
1228
- require "active_job/queue_adapters"
1284
+ require 'active_job'
1285
+ require 'active_job/queue_adapters'
1229
1286
 
1230
- require "zeitwerk"
1287
+ require 'zeitwerk'
1231
1288
  # By passing the flag, we acknowledge the extra directory lib/active_job
1232
1289
  # has to be managed by the loader and no warning has to be issued for it.
1233
1290
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
@@ -1246,23 +1303,35 @@ The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of t
1246
1303
 
1247
1304
  ```ruby
1248
1305
  loader = Zeitwerk::Loader.new
1249
- loader.push_dir(Pathname.new("/foo"))
1250
- loader.dirs # => ["/foo"]
1306
+ loader.push_dir(Pathname.new('/foo'))
1307
+ loader.dirs # => ['/foo']
1251
1308
  ```
1252
1309
 
1253
1310
  This method accepts an optional `namespaces` keyword argument. If truthy, the method returns a hash table instead. Keys are the absolute paths of the root directories as strings. Values are their corresponding namespaces, class or module objects:
1254
1311
 
1255
1312
  ```ruby
1256
1313
  loader = Zeitwerk::Loader.new
1257
- loader.push_dir(Pathname.new("/foo"))
1258
- loader.push_dir(Pathname.new("/bar"), namespace: Bar)
1259
- loader.dirs(namespaces: true) # => { "/foo" => Object, "/bar" => Bar }
1314
+ loader.push_dir(Pathname.new('/foo'))
1315
+ loader.push_dir(Pathname.new('/bar'), namespace: Bar)
1316
+ loader.dirs(namespaces: true) # => { '/foo' => Object, '/bar' => Bar }
1260
1317
  ```
1261
1318
 
1262
1319
  By default, ignored root directories are filtered out. If you want them included, please pass `ignored: true`.
1263
1320
 
1264
1321
  These collections are read-only. Please add to them with `Zeitwerk::Loader#push_dir`.
1265
1322
 
1323
+ <a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
1324
+ #### Autoloaded Constants
1325
+
1326
+ Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
1327
+
1328
+ ```ruby
1329
+ autoloaded_cpaths = []
1330
+ loader.on_load do |cpath, _value, _abspath|
1331
+ autoloaded_cpaths << cpath
1332
+ end
1333
+ ```
1334
+
1266
1335
  <a id="markdown-zeitwerkloadercpath_expected_at" name="zeitwerkloadercpath_expected_at"></a>
1267
1336
  #### `Zeitwerk::Loader#cpath_expected_at`
1268
1337
 
@@ -1271,18 +1340,18 @@ Given a path as a string or `Pathname` object, `Zeitwerk::Loader#cpath_expected_
1271
1340
  Some examples, assuming that `app/models` is a root directory:
1272
1341
 
1273
1342
  ```ruby
1274
- loader.cpath_expected_at("app/models") # => "Object"
1275
- loader.cpath_expected_at("app/models/user.rb") # => "User"
1276
- loader.cpath_expected_at("app/models/hotel") # => "Hotel"
1277
- loader.cpath_expected_at("app/models/hotel/billing.rb") # => "Hotel::Billing"
1343
+ loader.cpath_expected_at('app/models') # => 'Object'
1344
+ loader.cpath_expected_at('app/models/user.rb') # => 'User'
1345
+ loader.cpath_expected_at('app/models/hotel') # => 'Hotel'
1346
+ loader.cpath_expected_at('app/models/hotel/billing.rb') # => 'Hotel::Billing'
1278
1347
  ```
1279
1348
 
1280
1349
  If `collapsed` is a collapsed directory:
1281
1350
 
1282
1351
  ```ruby
1283
- loader.cpath_expected_at("a/b/collapsed/c") # => "A::B::C"
1284
- loader.cpath_expected_at("a/b/collapsed") # => "A::B", edge case
1285
- loader.cpath_expected_at("a/b") # => "A::B"
1352
+ loader.cpath_expected_at('a/b/collapsed/c') # => 'A::B::C'
1353
+ loader.cpath_expected_at('a/b/collapsed') # => 'A::B', edge case
1354
+ loader.cpath_expected_at('a/b') # => 'A::B'
1286
1355
  ```
1287
1356
 
1288
1357
  If the argument corresponds to an [ignored file or directory](#ignoring-parts-of-the-project), the method returns `nil`. Same if the argument is not managed by the loader.
@@ -1290,16 +1359,18 @@ If the argument corresponds to an [ignored file or directory](#ignoring-parts-of
1290
1359
  `Zeitwerk::Error` is raised if the given path does not exist:
1291
1360
 
1292
1361
  ```ruby
1293
- loader.cpath_expected_at("non_existing_file.rb") # => Zeitwerk::Error
1362
+ loader.cpath_expected_at('non_existing_file.rb') # => Zeitwerk::Error
1294
1363
  ```
1295
1364
 
1296
1365
  `Zeitwerk::NameError` is raised if a constant path cannot be derived from it:
1297
1366
 
1298
1367
  ```ruby
1299
- loader.cpath_expected_at("8.rb") # => Zeitwerk::NameError
1368
+ loader.cpath_expected_at('8.rb') # => Zeitwerk::NameError
1300
1369
  ```
1301
1370
 
1302
- This method does not parse file contents and does not guarantee files define the returned constant path. It just says which is the _expected_ one.
1371
+ This method does not parse file contents and does not guarantee files define the returned constant path.
1372
+
1373
+ Similarly, this method does not validate the project tree. If the project has conflicting oridinary and nsfiles for the same namespace, for example, the call will return the expected constant path for each of them without raising.
1303
1374
 
1304
1375
  `Zeitwerk::Loader#cpath_expected_at` is designed to be used with individual paths. If you want to know all the expected constant paths in the project, please use `Zeitwerk::Loader#all_expected_cpaths`, documented next.
1305
1376
 
@@ -1316,10 +1387,10 @@ For example, if `lib` is the root directory of a gem with the following contents
1316
1387
  lib/.DS_Store
1317
1388
  lib/my_gem.rb
1318
1389
  lib/my_gem/version.rb
1319
- lib/my_gem/ignored.rb
1320
1390
  lib/my_gem/drivers/unix.rb
1321
1391
  lib/my_gem/drivers/windows.rb
1322
1392
  lib/my_gem/collapsed/foo.rb
1393
+ lib/my_gem/ignored.rb
1323
1394
  lib/tasks/my_gem.rake
1324
1395
  ```
1325
1396
 
@@ -1327,27 +1398,29 @@ lib/tasks/my_gem.rake
1327
1398
 
1328
1399
  ```ruby
1329
1400
  {
1330
- "/.../lib" => "Object",
1331
- "/.../lib/my_gem.rb" => "MyGem",
1332
- "/.../lib/my_gem" => "MyGem",
1333
- "/.../lib/my_gem/version.rb" => "MyGem::VERSION",
1334
- "/.../lib/my_gem/drivers" => "MyGem::Drivers",
1335
- "/.../lib/my_gem/drivers/unix.rb" => "MyGem::Drivers::Unix",
1336
- "/.../lib/my_gem/drivers/windows.rb" => "MyGem::Drivers::Windows",
1337
- "/.../lib/my_gem/collapsed" => "MyGem",
1338
- "/.../lib/my_gem/collapsed/foo.rb" => "MyGem::Foo"
1401
+ '/.../lib' => 'Object',
1402
+ '/.../lib/my_gem.rb' => 'MyGem',
1403
+ '/.../lib/my_gem' => 'MyGem',
1404
+ '/.../lib/my_gem/version.rb' => 'MyGem::VERSION',
1405
+ '/.../lib/my_gem/drivers' => 'MyGem::Drivers',
1406
+ '/.../lib/my_gem/drivers/unix.rb' => 'MyGem::Drivers::Unix',
1407
+ '/.../lib/my_gem/drivers/windows.rb' => 'MyGem::Drivers::Windows',
1408
+ '/.../lib/my_gem/collapsed' => 'MyGem',
1409
+ '/.../lib/my_gem/collapsed/foo.rb' => 'MyGem::Foo'
1339
1410
  }
1340
1411
  ```
1341
1412
 
1342
1413
  In the previous example we assume `lib/my_gem/ignored.rb` is ignored, and therefore it is not present in the returned hash. Also, `lib/my_gem/collapsed` is a collapsed directory, so the expected namespace at that level is still `MyGem` (this is an edge case).
1343
1414
 
1344
- The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also not present because it contains no files with extension ".rb".
1415
+ The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also excluded because it contains no files with extension ".rb".
1345
1416
 
1346
1417
  Directory paths do not have trailing slashes.
1347
1418
 
1348
1419
  The order of the hash entries is undefined.
1349
1420
 
1350
- This method does not parse or execute file contents and does not guarantee files define the corresponding constant paths. It just says which are the _expected_ ones.
1421
+ This method does not parse file contents and does not guarantee files define the returned constant path.
1422
+
1423
+ Similarly, this method does not validate the project tree. If the project has conflicting oridinary and nsfiles for the same namespace, for example, the call will return the expected constant path for each of them without raising.
1351
1424
 
1352
1425
  <a id="markdown-encodings" name="encodings"></a>
1353
1426
  ### Encodings
@@ -1408,12 +1481,18 @@ To run one particular suite, pass its file name as an argument:
1408
1481
  bin/test test/lib/zeitwerk/test_eager_load.rb
1409
1482
  ```
1410
1483
 
1484
+ That also accepts a line number:
1485
+
1486
+ ```
1487
+ bin/test test/lib/zeitwerk/test_eager_load.rb:52
1488
+ ```
1489
+
1411
1490
  Furthermore, the project has a development dependency on [`minitest-focus`](https://github.com/seattlerb/minitest-focus). To run an individual test mark it with `focus`:
1412
1491
 
1413
1492
  ```ruby
1414
1493
  focus
1415
- test "capitalizes the first letter" do
1416
- assert_equal "User", camelize("user")
1494
+ test 'capitalizes the first letter' do
1495
+ assert_equal 'User', camelize('user')
1417
1496
  end
1418
1497
  ```
1419
1498
 
@@ -1427,7 +1506,7 @@ and run `bin/test`.
1427
1506
 
1428
1507
  Since `require` has global side-effects, and there is no static way to verify that you have issued the `require` calls for code that your file depends on, in practice it is very easy to forget some. That introduces bugs that depend on the load order.
1429
1508
 
1430
- Also, if the project has namespaces, setting things up and getting client code to load things in a consistent way needs discipline. For example, `require "foo/bar"` may define `Foo`, instead of reopen it. That may be a broken window, giving place to superclass mismatches or partially-defined namespaces.
1509
+ Also, if the project has namespaces, setting things up and getting client code to load things in a consistent way needs discipline. For example, `require 'foo/bar'` may define `Foo`, instead of reopen it. That may be a broken window, giving place to superclass mismatches or partially-defined namespaces.
1431
1510
 
1432
1511
  With Zeitwerk, you just name things following conventions and done. Things are available everywhere, and descend is always orderly. Without effort and without broken windows.
1433
1512
 
@@ -22,7 +22,7 @@ module Kernel
22
22
  #: (String) -> bool
23
23
  def require(path)
24
24
  if loader = Zeitwerk::Registry.autoloads.registered?(path)
25
- if path.end_with?(".rb")
25
+ if path.end_with?('.rb')
26
26
  required = zeitwerk_original_require(path)
27
27
  loader.__on_file_autoloaded(path) if required
28
28
  required
@@ -27,7 +27,7 @@
27
27
  # 1. We could also use a 1-level hash whose keys are constant paths. In the
28
28
  # example above it would be:
29
29
  #
30
- # { "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
30
+ # { 'M::X' => 0, 'M::Y' => 1, 'N::Z' => 2 }
31
31
  #
32
32
  # The gem used this approach for several years.
33
33
  #
data/lib/zeitwerk/cref.rb CHANGED
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # The constant may or may not exist in `mod`.
13
13
  class Zeitwerk::Cref
14
- require_relative "cref/map"
14
+ require_relative 'cref/map'
15
15
 
16
16
  include Zeitwerk::RealModName
17
17
 
@@ -66,4 +66,11 @@ class Zeitwerk::Cref
66
66
  def remove
67
67
  @mod.__send__(:remove_const, @cname)
68
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
69
76
  end
@@ -17,7 +17,18 @@ module Zeitwerk
17
17
  class SetupRequired < Error
18
18
  #: () -> void
19
19
  def initialize
20
- super("please, finish your configuration and call Zeitwerk::Loader#setup once all is ready")
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
21
32
  end
22
33
  end
23
34
  end
@@ -4,14 +4,14 @@ module Zeitwerk
4
4
  class GemInflector < Inflector
5
5
  #: (String) -> void
6
6
  def initialize(root_file)
7
- namespace = File.basename(root_file, ".rb")
7
+ namespace = File.basename(root_file, '.rb')
8
8
  root_dir = File.dirname(root_file)
9
- @version_file = File.join(root_dir, namespace, "version.rb")
9
+ @version_file = File.join(root_dir, namespace, 'version.rb')
10
10
  end
11
11
 
12
12
  #: (String, String) -> String
13
13
  def camelize(basename, abspath)
14
- abspath == @version_file ? "VERSION" : super
14
+ abspath == @version_file ? 'VERSION' : super
15
15
  end
16
16
  end
17
17
  end
@@ -19,8 +19,8 @@ module Zeitwerk
19
19
  def initialize(root_file, namespace:, warn_on_extra_files:)
20
20
  super()
21
21
 
22
- @tag = File.basename(root_file, ".rb")
23
- @tag = real_mod_name(namespace) + "-" + @tag unless namespace.equal?(Object)
22
+ @tag = File.basename(root_file, '.rb')
23
+ @tag = real_mod_name(namespace) + '-' + @tag unless namespace.equal?(Object)
24
24
 
25
25
  @inflector = GemInflector.new(root_file)
26
26
  @root_file = File.expand_path(root_file)
@@ -40,14 +40,14 @@ module Zeitwerk
40
40
 
41
41
  #: () -> void
42
42
  def warn_on_extra_files
43
- expected_namespace_dir = @root_file.delete_suffix(".rb")
43
+ expected_namespace_dir = @root_file.delete_suffix('.rb')
44
44
 
45
- ls(@root_dir) do |basename, abspath, ftype|
45
+ @fs.ls(@root_dir) do |basename, abspath, ftype|
46
46
  next if abspath == @root_file
47
47
  next if abspath == expected_namespace_dir
48
48
 
49
- basename_without_ext = basename.delete_suffix(".rb")
50
- cname = inflector.camelize(basename_without_ext, abspath).to_sym
49
+ basename_without_ext = basename.delete_suffix('.rb')
50
+ cname = cname_for(basename_without_ext, abspath)
51
51
 
52
52
  warn(<<~EOS)
53
53
  WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}