annotate_models icon indicating copy to clipboard operation
annotate_models copied to clipboard

Annotate gem does not support Rails autoloader collapsed directories

Open bazay opened this issue 3 years ago • 4 comments

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]

bazay avatar Oct 04 '22 14:10 bazay

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

guigs avatar Oct 11 '22 09:10 guigs

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

joshuaclayton avatar Dec 21 '22 22:12 joshuaclayton

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.

ilvez avatar Feb 07 '24 17:02 ilvez

@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

drwl avatar Feb 13 '24 05:02 drwl