zeitwerk 2.7.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '06683acd85eeb772fdb9757ae20bfb05e42ec465acfff3edcada546a399d9131'
4
- data.tar.gz: 80e3cdede4e05607ef5c58c60a3f7824c6750dd1d19d8786352356025bde1d65
3
+ metadata.gz: bc67ff7496feaaf2e3598809e67733b9be0f92f69820736e3471ae0487c39d85
4
+ data.tar.gz: cecf1045923045d9d11d4f5e6c214a9e6dd14abd742fc1065c6f23a8aec82e13
5
5
  SHA512:
6
- metadata.gz: b37beb740e461d73a1bd1ae21ec4f2bf63bb5022f3a193e2718bb8e5d4f172bf9c1222ff2a33acc47fb390a34e50b8b4b65b5b762dc5f71c1971b25720081437
7
- data.tar.gz: 02c85e17a3ab801c925157239386c9a5a105cc1b8b9d735f64c27c89d3a0371613f2cb3f27d49647d98bf3ad8220d3a3538c2bdae215a5890145136374eb1d3e
6
+ metadata.gz: 739fe00bce3542defa99e3c7a9014d7c4e00d0aa5a062e996c00dc2c9139e26de055e5aee806807c6da614504275bf2c790904d323d47213c031c291713daad6
7
+ data.tar.gz: 85acfebcb1de71e4c54e94803fb3be1c804960d3ba3754cdb6903a32a61e52f139cc06f7672f2cdcabb189dc9d66975479fc20a6733152e58e3131797fc151e4
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)
@@ -98,7 +100,7 @@ Main interface for gems:
98
100
  ```ruby
99
101
  # lib/my_gem.rb (main file)
100
102
 
101
- require "zeitwerk"
103
+ require 'zeitwerk'
102
104
  loader = Zeitwerk::Loader.for_gem
103
105
  loader.setup # ready!
104
106
 
@@ -181,7 +183,7 @@ end
181
183
  The first example needs a custom [inflection](#inflection) rule:
182
184
 
183
185
  ```ruby
184
- loader.inflector.inflect("max_retries" => "MAX_RETRIES")
186
+ loader.inflector.inflect('max_retries' => 'MAX_RETRIES')
185
187
  ```
186
188
 
187
189
  Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
@@ -220,8 +222,8 @@ Although `Object` is the most common root namespace, you have the flexibility to
220
222
  For example, given:
221
223
 
222
224
  ```ruby
223
- require "active_job"
224
- require "active_job/queue_adapters"
225
+ require 'active_job'
226
+ require 'active_job/queue_adapters'
225
227
  loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
226
228
  ```
227
229
 
@@ -264,16 +266,23 @@ To trigger this behavior, the directory must contain non-ignored Ruby files with
264
266
  <a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
265
267
  ### Explicit namespaces
266
268
 
267
- 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:
268
275
 
269
276
  ```
270
277
  app/models/hotel.rb -> Hotel
271
278
  app/models/hotel/pricing.rb -> Hotel::Pricing
272
279
  ```
273
280
 
274
- 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`.
275
282
 
276
- The classes and modules from the namespace are already available in the body of the class or module defining it:
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.
284
+
285
+ The classes and modules from an explicit namespace are already available in the body of the class or module that defines it:
277
286
 
278
287
  ```ruby
279
288
  class Hotel < ApplicationRecord
@@ -284,7 +293,48 @@ end
284
293
 
285
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.
286
295
 
287
- 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 conventions are not exclusive project-wide. Some parts 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
+ Non-ignored files whose basename is equal to the nsfile are always considered to be nsfiles. 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`.
288
338
 
289
339
  <a id="markdown-collapsing-directories" name="collapsing-directories"></a>
290
340
  ### Collapsing directories
@@ -372,9 +422,9 @@ Conceptually, `for_gem` translates to:
372
422
  ```ruby
373
423
  # lib/my_gem.rb
374
424
 
375
- require "zeitwerk"
425
+ require 'zeitwerk'
376
426
  loader = Zeitwerk::Loader.new
377
- loader.tag = File.basename(__FILE__, ".rb")
427
+ loader.tag = File.basename(__FILE__, '.rb')
378
428
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
379
429
  loader.push_dir(File.dirname(__FILE__))
380
430
  ```
@@ -384,7 +434,7 @@ If the main module references project constants at the top-level, Zeitwerk has t
384
434
  ```ruby
385
435
  # lib/my_gem.rb (main file)
386
436
 
387
- require "zeitwerk"
437
+ require 'zeitwerk'
388
438
  loader = Zeitwerk::Loader.for_gem
389
439
  loader.setup
390
440
 
@@ -430,7 +480,7 @@ Let's suppose you are writing a gem to extend `Net::HTTP` with some niche featur
430
480
  The top-level file mentioned in the last point is optional. In particular, from
431
481
 
432
482
  ```ruby
433
- gem "net-http-niche_feature"
483
+ gem 'net-http-niche_feature'
434
484
  ```
435
485
 
436
486
  if the hyphenated file does not exist, Bundler notes the conventional hyphenated pattern and issues a `require` for `net/http/niche_feature`.
@@ -443,13 +493,13 @@ The structure of the gem would be like this:
443
493
  # lib/net-http-niche_feature.rb (optional)
444
494
 
445
495
  # For technical reasons, this cannot be require_relative.
446
- require "net/http/niche_feature"
496
+ require 'net/http/niche_feature'
447
497
 
448
498
 
449
499
  # lib/net/http/niche_feature.rb
450
500
 
451
- require "net/http"
452
- require "zeitwerk"
501
+ require 'net/http'
502
+ require 'zeitwerk'
453
503
 
454
504
  loader = Zeitwerk::Loader.for_gem_extension(Net::HTTP)
455
505
  loader.setup
@@ -464,7 +514,7 @@ end
464
514
  # lib/net/http/niche_feature/version.rb
465
515
 
466
516
  module Net::HTTP::NicheFeature
467
- VERSION = "1.0.0"
517
+ VERSION = '1.0.0'
468
518
  end
469
519
  ```
470
520
 
@@ -486,7 +536,7 @@ Let's revisit the example above:
486
536
  ```ruby
487
537
  # lib/my_gem.rb (main file)
488
538
 
489
- require "zeitwerk"
539
+ require 'zeitwerk'
490
540
  loader = Zeitwerk::Loader.for_gem
491
541
  loader.setup
492
542
 
@@ -495,7 +545,7 @@ module MyGem
495
545
  end
496
546
  ```
497
547
 
498
- That works, and there is no `require "my_gem/my_logger"`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
548
+ That works, and there is no `require 'my_gem/my_logger'`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
499
549
 
500
550
  If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
501
551
 
@@ -682,7 +732,7 @@ In order to reload safely, no other thread can be autoloading or reloading concu
682
732
  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:
683
733
 
684
734
  ```ruby
685
- require "concurrent/atomic/read_write_lock"
735
+ require 'concurrent/atomic/read_write_lock'
686
736
 
687
737
  MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
688
738
  ```
@@ -727,22 +777,22 @@ The camelize logic can be overridden easily for individual basenames:
727
777
 
728
778
  ```ruby
729
779
  loader.inflector.inflect(
730
- "html_parser" => "HTMLParser",
731
- "mysql_adapter" => "MySQLAdapter"
780
+ 'html_parser' => 'HTMLParser',
781
+ 'mysql_adapter' => 'MySQLAdapter'
732
782
  )
733
783
  ```
734
784
 
735
785
  The `inflect` method can be invoked several times if you prefer this other style:
736
786
 
737
787
  ```ruby
738
- loader.inflector.inflect "html_parser" => "HTMLParser"
739
- loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
788
+ loader.inflector.inflect 'html_parser' => 'HTMLParser'
789
+ loader.inflector.inflect 'mysql_adapter' => 'MySQLAdapter'
740
790
  ```
741
791
 
742
792
  Overrides have to match exactly directory or file (without extension) _basenames_. For example, if you configure
743
793
 
744
794
  ```ruby
745
- loader.inflector.inflect("xml" => "XML")
795
+ loader.inflector.inflect('xml' => 'XML')
746
796
  ```
747
797
 
748
798
  then the following constants are expected:
@@ -757,8 +807,8 @@ As you see, any directory whose basename is exactly `xml`, and any file whose ba
757
807
 
758
808
  ```ruby
759
809
  loader.inflector.inflect(
760
- "xml" => "XML",
761
- "xml_parser" => "XMLParser"
810
+ 'xml' => 'XML',
811
+ 'xml_parser' => 'XMLParser'
762
812
  )
763
813
  ```
764
814
 
@@ -813,7 +863,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
813
863
  class MyInflector < Zeitwerk::Inflector
814
864
  def camelize(basename, abspath)
815
865
  if basename =~ /\Ahtml_(.*)/
816
- "HTML" + super($1, abspath)
866
+ 'HTML' + super($1, abspath)
817
867
  else
818
868
  super
819
869
  end
@@ -844,8 +894,8 @@ module MyGem
844
894
  end
845
895
 
846
896
  # lib/my_gem.rb
847
- require "zeitwerk"
848
- require_relative "my_gem/inflector"
897
+ require 'zeitwerk'
898
+ require_relative 'my_gem/inflector'
849
899
 
850
900
  loader = Zeitwerk::Loader.for_gem
851
901
  loader.inflector = MyGem::Inflector.new(__FILE__)
@@ -913,13 +963,13 @@ With `on_load`, it is easy to schedule code at boot time that initializes `endpo
913
963
 
914
964
  ```ruby
915
965
  # config/environments/development.rb
916
- loader.on_load("SomeApiClient") do |klass, _abspath|
917
- klass.endpoint = "https://api.dev"
966
+ loader.on_load('SomeApiClient') do |klass, _abspath|
967
+ klass.endpoint = 'https://api.dev'
918
968
  end
919
969
 
920
970
  # config/environments/production.rb
921
- loader.on_load("SomeApiClient") do |klass, _abspath|
922
- klass.endpoint = "https://api.prod"
971
+ loader.on_load('SomeApiClient') do |klass, _abspath|
972
+ klass.endpoint = 'https://api.prod'
923
973
  end
924
974
  ```
925
975
 
@@ -963,7 +1013,7 @@ When reloading is enabled, you may occasionally need to execute something before
963
1013
  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:
964
1014
 
965
1015
  ```ruby
966
- loader.on_unload("Country") do |klass, _abspath|
1016
+ loader.on_unload('Country') do |klass, _abspath|
967
1017
  klass.clear_cache
968
1018
  end
969
1019
  ```
@@ -1048,7 +1098,7 @@ Zeitwerk@9fa54b: autoload set for User, to be loaded from ...
1048
1098
  By default, a random tag like the one above is assigned, but you can change it:
1049
1099
 
1050
1100
  ```
1051
- loader.tag = "grep_me"
1101
+ loader.tag = 'grep_me'
1052
1102
  ```
1053
1103
 
1054
1104
  The tag of a loader returned by `for_gem` is the basename of the root file without extension:
@@ -1104,7 +1154,7 @@ loader.setup
1104
1154
  Now, that file has to be loaded manually with `require` or `require_relative`:
1105
1155
 
1106
1156
  ```ruby
1107
- require_relative "my_gem/core_ext/kernel"
1157
+ require_relative 'my_gem/core_ext/kernel'
1108
1158
  ```
1109
1159
 
1110
1160
  and you can do that anytime, before configuring the loader, or after configuring the loader, does not matter.
@@ -1118,7 +1168,7 @@ Let's imagine your project talks to databases, supports several, and has adapter
1118
1168
 
1119
1169
  ```ruby
1120
1170
  # my_gem/db_adapters/postgresql.rb
1121
- require "pg"
1171
+ require 'pg'
1122
1172
  ```
1123
1173
 
1124
1174
  but you don't want your users to install them all, only the one they are going to use.
@@ -1156,7 +1206,7 @@ loader.setup
1156
1206
  In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
1157
1207
 
1158
1208
  ```ruby
1159
- require "foo"
1209
+ require 'foo'
1160
1210
  ```
1161
1211
 
1162
1212
  the first one found gets loaded, and the rest are ignored.
@@ -1225,10 +1275,10 @@ In order to do so, you need to make sure those modules are loaded before calling
1225
1275
 
1226
1276
  ```ruby
1227
1277
  # Ensure these namespaces are reopened, not defined.
1228
- require "active_job"
1229
- require "active_job/queue_adapters"
1278
+ require 'active_job'
1279
+ require 'active_job/queue_adapters'
1230
1280
 
1231
- require "zeitwerk"
1281
+ require 'zeitwerk'
1232
1282
  # By passing the flag, we acknowledge the extra directory lib/active_job
1233
1283
  # has to be managed by the loader and no warning has to be issued for it.
1234
1284
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
@@ -1247,17 +1297,17 @@ The method `Zeitwerk::Loader#dirs` returns an array with the absolute paths of t
1247
1297
 
1248
1298
  ```ruby
1249
1299
  loader = Zeitwerk::Loader.new
1250
- loader.push_dir(Pathname.new("/foo"))
1251
- loader.dirs # => ["/foo"]
1300
+ loader.push_dir(Pathname.new('/foo'))
1301
+ loader.dirs # => ['/foo']
1252
1302
  ```
1253
1303
 
1254
1304
  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:
1255
1305
 
1256
1306
  ```ruby
1257
1307
  loader = Zeitwerk::Loader.new
1258
- loader.push_dir(Pathname.new("/foo"))
1259
- loader.push_dir(Pathname.new("/bar"), namespace: Bar)
1260
- loader.dirs(namespaces: true) # => { "/foo" => Object, "/bar" => Bar }
1308
+ loader.push_dir(Pathname.new('/foo'))
1309
+ loader.push_dir(Pathname.new('/bar'), namespace: Bar)
1310
+ loader.dirs(namespaces: true) # => { '/foo' => Object, '/bar' => Bar }
1261
1311
  ```
1262
1312
 
1263
1313
  By default, ignored root directories are filtered out. If you want them included, please pass `ignored: true`.
@@ -1284,18 +1334,18 @@ Given a path as a string or `Pathname` object, `Zeitwerk::Loader#cpath_expected_
1284
1334
  Some examples, assuming that `app/models` is a root directory:
1285
1335
 
1286
1336
  ```ruby
1287
- loader.cpath_expected_at("app/models") # => "Object"
1288
- loader.cpath_expected_at("app/models/user.rb") # => "User"
1289
- loader.cpath_expected_at("app/models/hotel") # => "Hotel"
1290
- loader.cpath_expected_at("app/models/hotel/billing.rb") # => "Hotel::Billing"
1337
+ loader.cpath_expected_at('app/models') # => 'Object'
1338
+ loader.cpath_expected_at('app/models/user.rb') # => 'User'
1339
+ loader.cpath_expected_at('app/models/hotel') # => 'Hotel'
1340
+ loader.cpath_expected_at('app/models/hotel/billing.rb') # => 'Hotel::Billing'
1291
1341
  ```
1292
1342
 
1293
1343
  If `collapsed` is a collapsed directory:
1294
1344
 
1295
1345
  ```ruby
1296
- loader.cpath_expected_at("a/b/collapsed/c") # => "A::B::C"
1297
- loader.cpath_expected_at("a/b/collapsed") # => "A::B", edge case
1298
- loader.cpath_expected_at("a/b") # => "A::B"
1346
+ loader.cpath_expected_at('a/b/collapsed/c') # => 'A::B::C'
1347
+ loader.cpath_expected_at('a/b/collapsed') # => 'A::B', edge case
1348
+ loader.cpath_expected_at('a/b') # => 'A::B'
1299
1349
  ```
1300
1350
 
1301
1351
  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.
@@ -1303,16 +1353,18 @@ If the argument corresponds to an [ignored file or directory](#ignoring-parts-of
1303
1353
  `Zeitwerk::Error` is raised if the given path does not exist:
1304
1354
 
1305
1355
  ```ruby
1306
- loader.cpath_expected_at("non_existing_file.rb") # => Zeitwerk::Error
1356
+ loader.cpath_expected_at('non_existing_file.rb') # => Zeitwerk::Error
1307
1357
  ```
1308
1358
 
1309
1359
  `Zeitwerk::NameError` is raised if a constant path cannot be derived from it:
1310
1360
 
1311
1361
  ```ruby
1312
- loader.cpath_expected_at("8.rb") # => Zeitwerk::NameError
1362
+ loader.cpath_expected_at('8.rb') # => Zeitwerk::NameError
1313
1363
  ```
1314
1364
 
1315
- 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.
1365
+ This method does not parse file contents and does not guarantee files define the returned constant path.
1366
+
1367
+ 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.
1316
1368
 
1317
1369
  `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.
1318
1370
 
@@ -1329,10 +1381,10 @@ For example, if `lib` is the root directory of a gem with the following contents
1329
1381
  lib/.DS_Store
1330
1382
  lib/my_gem.rb
1331
1383
  lib/my_gem/version.rb
1332
- lib/my_gem/ignored.rb
1333
1384
  lib/my_gem/drivers/unix.rb
1334
1385
  lib/my_gem/drivers/windows.rb
1335
1386
  lib/my_gem/collapsed/foo.rb
1387
+ lib/my_gem/ignored.rb
1336
1388
  lib/tasks/my_gem.rake
1337
1389
  ```
1338
1390
 
@@ -1340,27 +1392,29 @@ lib/tasks/my_gem.rake
1340
1392
 
1341
1393
  ```ruby
1342
1394
  {
1343
- "/.../lib" => "Object",
1344
- "/.../lib/my_gem.rb" => "MyGem",
1345
- "/.../lib/my_gem" => "MyGem",
1346
- "/.../lib/my_gem/version.rb" => "MyGem::VERSION",
1347
- "/.../lib/my_gem/drivers" => "MyGem::Drivers",
1348
- "/.../lib/my_gem/drivers/unix.rb" => "MyGem::Drivers::Unix",
1349
- "/.../lib/my_gem/drivers/windows.rb" => "MyGem::Drivers::Windows",
1350
- "/.../lib/my_gem/collapsed" => "MyGem",
1351
- "/.../lib/my_gem/collapsed/foo.rb" => "MyGem::Foo"
1395
+ '/.../lib' => 'Object',
1396
+ '/.../lib/my_gem.rb' => 'MyGem',
1397
+ '/.../lib/my_gem' => 'MyGem',
1398
+ '/.../lib/my_gem/version.rb' => 'MyGem::VERSION',
1399
+ '/.../lib/my_gem/drivers' => 'MyGem::Drivers',
1400
+ '/.../lib/my_gem/drivers/unix.rb' => 'MyGem::Drivers::Unix',
1401
+ '/.../lib/my_gem/drivers/windows.rb' => 'MyGem::Drivers::Windows',
1402
+ '/.../lib/my_gem/collapsed' => 'MyGem',
1403
+ '/.../lib/my_gem/collapsed/foo.rb' => 'MyGem::Foo'
1352
1404
  }
1353
1405
  ```
1354
1406
 
1355
1407
  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).
1356
1408
 
1357
- 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".
1409
+ The file `lib/.DS_Store` is hidden, hence excluded. The directory `lib/tasks` is also excluded because it contains no files with extension ".rb".
1358
1410
 
1359
1411
  Directory paths do not have trailing slashes.
1360
1412
 
1361
1413
  The order of the hash entries is undefined.
1362
1414
 
1363
- 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.
1415
+ This method does not parse file contents and does not guarantee files define the returned constant path.
1416
+
1417
+ 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.
1364
1418
 
1365
1419
  <a id="markdown-encodings" name="encodings"></a>
1366
1420
  ### Encodings
@@ -1431,8 +1485,8 @@ Furthermore, the project has a development dependency on [`minitest-focus`](http
1431
1485
 
1432
1486
  ```ruby
1433
1487
  focus
1434
- test "capitalizes the first letter" do
1435
- assert_equal "User", camelize("user")
1488
+ test 'capitalizes the first letter' do
1489
+ assert_equal 'User', camelize('user')
1436
1490
  end
1437
1491
  ```
1438
1492
 
@@ -1446,7 +1500,7 @@ and run `bin/test`.
1446
1500
 
1447
1501
  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.
1448
1502
 
1449
- 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.
1503
+ 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.
1450
1504
 
1451
1505
  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.
1452
1506
 
@@ -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,13 +40,13 @@ 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
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")
49
+ basename_without_ext = basename.delete_suffix('.rb')
50
50
  cname = cname_for(basename_without_ext, abspath)
51
51
 
52
52
  warn(<<~EOS)
@@ -5,9 +5,9 @@ module Zeitwerk
5
5
  # Very basic snake case -> camel case conversion.
6
6
  #
7
7
  # inflector = Zeitwerk::Inflector.new
8
- # inflector.camelize("post", ...) # => "Post"
9
- # inflector.camelize("users_controller", ...) # => "UsersController"
10
- # inflector.camelize("api", ...) # => "Api"
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
  #
@@ -20,13 +20,13 @@ module Zeitwerk
20
20
  #
21
21
  # inflector = Zeitwerk::Inflector.new
22
22
  # inflector.inflect(
23
- # "html_parser" => "HTMLParser",
24
- # "mysql_adapter" => "MySQLAdapter"
23
+ # 'html_parser' => 'HTMLParser',
24
+ # 'mysql_adapter' => 'MySQLAdapter'
25
25
  # )
26
26
  #
27
- # inflector.camelize("html_parser", abspath) # => "HTMLParser"
28
- # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
29
- # inflector.camelize("users_controller", abspath) # => "UsersController"
27
+ # inflector.camelize('html_parser', abspath) # => 'HTMLParser'
28
+ # inflector.camelize('mysql_adapter', abspath) # => 'MySQLAdapter'
29
+ # inflector.camelize('users_controller', abspath) # => 'UsersController'
30
30
  #
31
31
  #: (Hash[String, String]) -> void
32
32
  def inflect(inflections)
@@ -77,7 +77,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
77
77
  internal def on_namespace_loaded(cref, namespace)
78
78
  if dirs = namespace_dirs.delete(cref)
79
79
  dirs.each do |dir|
80
- define_autoloads_for_dir(dir, namespace)
80
+ define_autoloads_for_dir(dir, namespace, external: false)
81
81
  end
82
82
  end
83
83
  end