Skip to content

Commit

Permalink
Ignore directories with no Ruby files
Browse files Browse the repository at this point in the history
  • Loading branch information
fxn committed May 13, 2022
1 parent 30650ed commit 68976b6
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 6 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## 2.5.5 (Unreleased)

* For a directory to implicitly define a namespace, it has now to contain at
least one non-ignored Ruby file, either directly or recursively. For example,
`tasks/newsletter.rake` defined a namespace `Tasks` before, now it doesn't.
This is also handy for project layouts that contain a mix of directories with
Ruby files, and directories with templates or other auxiliary resources.

If you need an empty module, please define it in a file.

* On setup, loaders returned by `Zeitwerk::Loader.for_gem` issue warnings if
`lib` has extra non-ignored Ruby files or directories. So if this is all:

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ app/controllers/admin/users_controller.rb -> Admin::UsersController

`Admin` is autovivified as a module on demand, you do not need to define an `Admin` class or module in an `admin.rb` file explicitly.

Directories define namespaces regardless of their contents, even if they are empty. If they are not meant to represent namespaces, please tell the loader to [ignore](#ignoring-parts-of-the-project) them.
For a directory to implicitly define a namespace, it has to contain at least one non-ignored Ruby file, either directly or recursively. For example, `tasks/newsletter.rake` does not define a `Tasks` namespace. If you'd like the loader to not even inspect the contents of some of these, remember you can manually [ignore them](#ignoring-parts-of-the-project).

If you need an empty module, please define it in a file.

<a id="markdown-explicit-namespaces" name="explicit-namespaces"></a>
### Explicit namespaces
Expand Down
10 changes: 7 additions & 3 deletions lib/zeitwerk/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,13 @@ def autoload_subdir(parent, cname, subdir)
# subdirectory has to be visited if the namespace is used.
lazy_subdirs[cpath] << subdir
elsif !cdef?(parent, cname)
# First time we find this namespace, set an autoload for it.
lazy_subdirs[cpath(parent, cname)] << subdir
set_autoload(parent, cname, subdir)
if has_at_least_one_ruby_file?(subdir)
# First time we find this namespace, set an autoload for it.
lazy_subdirs[cpath(parent, cname)] << subdir
set_autoload(parent, cname, subdir)
else
log("Directory #{subdir} has no Ruby files, ignoring") if logger
end
else
# For whatever reason the constant that corresponds to this namespace has
# already been defined, we have to recurse.
Expand Down
16 changes: 16 additions & 0 deletions lib/zeitwerk/loader/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ def ls(dir)
end
end

def has_at_least_one_ruby_file?(dir)
to_visit = [dir]

while dir = to_visit.shift
ls(dir) do |_basename, abspath|
if ruby?(abspath)
return true
elsif dir?(abspath)
to_visit << abspath
end
end
end

false
end

# @sig (String) -> bool
def ruby?(path)
path.end_with?(".rb")
Expand Down
50 changes: 50 additions & 0 deletions test/lib/zeitwerk/test_autovivification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,54 @@ def Admin.const_set(cname, mod)
assert $test_admin_const_set_queue.empty?
end
end

test "defines no namespace for empty directories" do
with_files([]) do
FileUtils.mkdir("foo")
loader.push_dir(".")
loader.setup
assert !Object.autoload?(:Foo)
end
end

test "defines no namespace for empty directories (recursively)" do
with_files([]) do
FileUtils.mkdir_p("foo/bar/baz")
loader.push_dir(".")
loader.setup
assert !Object.autoload?(:Foo)
end
end

test "defines no namespace for directories whose files are all non-Ruby" do
with_setup([["tasks/newsletter.rake", ""], ["assets/.keep", ""]]) do
assert !Object.autoload?(:Tasks)
assert !Object.autoload?(:Assets)
end
end

test "defines no namespace for directories whose files are all non-Ruby (recursively)" do
with_setup([["tasks/product/newsletter.rake", ""], ["assets/css/.keep", ""]]) do
assert !Object.autoload?(:Tasks)
assert !Object.autoload?(:Assets)
end
end

test "defines no namespace for directories whose Ruby files are all ignored" do
with_files([["foo/bar/baz.rb", ""]]) do
loader.push_dir(".")
loader.ignore("foo/bar/baz.rb")
loader.setup
assert !Object.autoload?(:Foo)
end
end

test "defines no namespace for directories that have Ruby files below ignored directories" do
with_files([["foo/bar/baz.rb", ""]]) do
loader.push_dir(".")
loader.ignore("foo/bar")
loader.setup
assert !Object.autoload?(:Foo)
end
end
end
10 changes: 10 additions & 0 deletions test/lib/zeitwerk/test_logging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,14 @@ def logger.debug(message)
end
end
end

test "logs directories that are ignored because they do not contain Ruby files" do
with_files([]) do
FileUtils.mkdir_p("foo/bar/baz")
loader.push_dir(".")
assert_logged(%r(Directory #{File.expand_path("foo")} has no Ruby files, ignoring)) do
loader.setup
end
end
end
end
3 changes: 1 addition & 2 deletions test/lib/zeitwerk/test_ruby_compatibility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ class C; end
# While unloading constants we leverage this property to avoid lookups in
# $LOADED_FEATURES for strings that we know are not going to be there.
test "directories are not included in $LOADED_FEATURES" do
with_files([]) do
FileUtils.mkdir("admin")
with_files(["admin/users_controller.rb"]) do
loader.push_dir(".")
loader.setup

Expand Down

0 comments on commit 68976b6

Please sign in to comment.