Annotate gem does not support Rails autoloader collapsed directories
Annotate gem does not support Rails autoloader collapsed directories i.e.
pry(AnnotateModels)> Rails.autoloaders.main
=> #<Zeitwerk::Loader:0x00007f6305898b58
@autoloaded_dirs=[],
@autoloads={},
@collapse_dirs=
#<Set: {"/home/developer/my_app/app/components/my_namespace/models"}>,
@collapse_glob_patterns=#<Set: {/home/developer/my_app/app/components/*/models", "/home/developer/my_app/app/components/my_namespace/jobs"}>,
@eager_load_exclusions=#<Set: {}>,
@eager_loaded=true,
@ignored_glob_patterns=
#<Set: {"/home/developer/my_app/lib/rails",
"/home/developer/my_app/lib/patches",
"//home/developer/my_app/lib/rails/generators/job",
...
pry(AnnotateModels)> Rails.autoloaders.main.collapsed_dirs
=> #<Set: {"/home/developer/my_app/app/components/my_namespace/models"}>
In config/application.rb:
Rails.autoloaders.main.collapse("app/components/*/models")
I've obfuscated the model names and namespace, but the end result is still the same.
Commands
$ bundle exec annotate
Unable to annotate app/components/my_namespace/models/my_model.rb: file doesn't contain a valid model class
Unable to annotate app/components/my_namespace/models/my_second_model.rb: file doesn't contain a valid model class
Unable to annotate app/components/my_namespace/models/my_third_model.rb: file doesn't contain a valid model class
$
Version
- annotate (3.2.0 96831c1)
- rails (7.0.3.1)
- ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
Here's a monkey patch we are using that find classes based on zeitwerk loader. Our use case is for loading classes within namespaces. I'm not sure if it works with collapsed dirs, but maybe you can modify it to work.
Put in an initializer:
# frozen_string_literal: true
if Rails.env.development?
require 'annotate'
module AnnotateModelsWithZeitwerk
def get_model_class(file)
loader = Rails.autoloaders.main
root_dirs = loader.dirs(namespaces: true) # or `root_dirs = loader.root_dirs` with zeitwerk < 2.6.1
expanded_file = File.expand_path(file)
root_dir, namespace = root_dirs.find do |dir, _|
expanded_file.start_with?(dir)
end
_, filepath_relative_to_root_dir = expanded_file.split(root_dir)
filepath_relative_to_root_dir = filepath_relative_to_root_dir[1..].sub(/\.rb$/, '')
camelize = loader.inflector.camelize(filepath_relative_to_root_dir, nil)
namespace.const_get(camelize)
end
end
module AnnotateModels
class << self
prepend AnnotateModelsWithZeitwerk
end
end
end
I also ran into this issue earlier and had to adjust the example above slightly.
We're using collapsed directories as such:
# within config/application.rb
Rails.autoloaders.main.collapse("#{Rails.root}/app/domains/domain_name/models")
Rails.autoloaders.main.collapse("#{Rails.root}/app/domains/domain_name/controllers")
Rails.autoloaders.main.collapse("#{Rails.root}/app/domains/domain_name/lib")
Based on this, I adjusted the sample above to address const loading issues:
module AnnotateModelsWithZeitwerk
def get_model_class(file)
loader = Rails.autoloaders.main
root_dirs = loader.dirs(namespaces: true) # or `root_dirs = loader.root_dirs` with zeitwerk < 2.6.1
expanded_file = File.expand_path(file)
root_dir, namespace = root_dirs.find do |dir, _|
expanded_file.start_with?(dir)
end
_, filepath_relative_to_root_dir = expanded_file.split(root_dir)
filepath_relative_to_root_dir = filepath_relative_to_root_dir[1..].sub(/\.rb$/, "")
# changed
# once we have the filepath_relative_to_root_dir, we need to see if it
# falls within one of our Zeitwerk "collapsed" paths.
if loader.collapse.any? { |path| path.include?(root_dir) && file.include?(path.split(root_dir)[1]) }
# if the file is within a collapsed path, we then need to, for each
# collapsed path, remove the root dir
collapsed = loader.collapse.map { |path| path.split(root_dir)[1].sub(/^\//, "") }.to_set
collapsed.each do |collapse|
# next, we split the collapsed directory, e.g. `domain_name/models`, by
# slash, and discard the domain_name
_, *collapsed_namespace = collapse.split("/")
# if there are any collapsed namespaces, e.g. `models`, we then remove
# that from `filepath_relative_to_root_dir`.
#
# This would result in:
#
# previous filepath_relative_to_root_dir: domain_name/models/model_name
# new filepath_relative_to_root_dir: domain_name/model_name
if collapsed_namespace.any?
filepath_relative_to_root_dir.sub!("/#{collapsed_namespace.last}", "")
end
end
end
# end changed
camelize = loader.inflector.camelize(filepath_relative_to_root_dir, nil)
namespace.const_get(camelize)
end
end
I found that fork of this gem has solved those issues: https://github.com/drwl/annotaterb
I understand fork was done since this gem is unmaintained quite a long time now (2 years without release): https://www.reddit.com/r/rails/comments/13keyfm/i_spent_the_past_3_months_working_on_a_fork_of/
Nearly drop in replacement, but configuration file needs to be created instead of configuring in rake task.
@joshuaclayton @bazay, I took a stab at adding zeitwerk support in my fork, if you want to know more: https://github.com/drwl/annotaterb/issues/82