Skip to content

Commit

Permalink
Make zeitwerk work with autoextend
Browse files Browse the repository at this point in the history
fixes FOO-2517

Change-Id: I510e4640d9ad0db9fd5f5db65611adcc8b60ef58
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/277075
Tested-by: Service Cloud Jenkins <[email protected]>
Reviewed-by: Ethan Vizitei <[email protected]>
QA-Review: Jacob Burroughs <[email protected]>
Product-Review: Jacob Burroughs <[email protected]>
  • Loading branch information
maths22 committed Oct 29, 2021
1 parent 041f8ae commit d6f5683
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 18 deletions.
1 change: 1 addition & 0 deletions gems/autoextend/autoextend.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ Gem::Specification.new do |spec|

spec.add_development_dependency "activesupport"
spec.add_development_dependency "byebug"
spec.add_development_dependency "railties"
spec.add_development_dependency "rspec", "~> 3.5.0"
end
53 changes: 38 additions & 15 deletions gems/autoextend/lib/autoextend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,34 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.

require_relative 'autoextend/extension'
require_relative "autoextend/railtie" if defined?(Rails::Railtie)

module Autoextend
class << self
def const_added(const, source:)
def const_added(const, source:, recursive: false)
const_name = const.is_a?(String) ? const : const.name
return [] unless const_name

extensions_list = extensions_hash.fetch(const_name.to_sym, [])
sorted_extensions(extensions_list).each do |extension|
ret = sorted_extensions(extensions_list).each do |extension|
if const == const_name
const = Object.const_get(const_name, false)
end
extension.extend(const, source: source)
end

if recursive
const.constants(false).each do |child|
next if const.autoload?(child)

child_const = const.const_get(child, false)
next unless child_const.is_a?(Module)

const_added(child_const, source: source, recursive: true)
end
end

ret
end

# Add a hook to automatically extend a class or module with a module,
Expand Down Expand Up @@ -94,7 +108,13 @@ def hook(const_name,
end

# immediately extend the class if it's already defined
if Object.const_defined?(const_name.to_s, false)
# If autoload? is true, don't use const_defined? as it is set up to be autoloaded but hasn't been loaded yet
module_chain = const_name.to_s.split('::').inject([]) { |all, val| all + [[(all.last ? "#{all.last.first}::#{all.last[1]}" : nil), val]] }
exists_and_not_to_autoload = module_chain.all? do |mod, name|
mod = mod.nil? ? Object : Object.const_get(mod)
!mod.autoload?(name) && mod.const_defined?(name.to_s, false)
end
if exists_and_not_to_autoload
extension.before.each do |before_module|
if const_extensions.any? { |ext| ext.module_name == before_module }
raise "Already included #{before_module}; cannot include #{module_name} first"
Expand All @@ -114,6 +134,20 @@ def extensions
extensions_hash.values.flatten
end

def inject_into_zetwerk
return unless Object.const_defined?(:Zeitwerk)

Zeitwerk::Registry.loaders.each do |loader|
loader.on_load do |_cpath, value, abspath|
# Skip autovivified modules from directories
next unless abspath.end_with?('.rb')
next unless value.is_a?(Module)

Autoextend.const_added(value, source: :Zeitwerk, recursive: true)
end
end
end

private

def sorted_extensions(extensions_list)
Expand Down Expand Up @@ -174,23 +208,12 @@ def included(klass)

module Autoextend::ActiveSupport
module Dependencies
def notify_autoextend_of_new_constant(constant)
Autoextend.const_added(constant, source: :'ActiveSupport::Dependencies')
# check for nested constants
constant.constants(false).each do |child|
child_const = constant.const_get(child, false)
next unless child_const.is_a?(Module)

notify_autoextend_of_new_constant(child_const)
end
end

def new_constants_in(*_descs)
super.each do |constant_name|
constant = Object.const_get(constant_name, false)
next unless constant.is_a?(Module)

notify_autoextend_of_new_constant(constant)
Autoextend.const_added(constant, source: :'ActiveSupport::Dependencies', recursive: true)
end
end

Expand Down
27 changes: 27 additions & 0 deletions gems/autoextend/lib/autoextend/railtie.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

module Autoextend
class Railtie < Rails::Railtie
# CANVAS_RAILS6_1 this method will need changing for a post-rails 6.1 world
initializer "inject autoextend hooks" do
::Autoextend.inject_into_zetwerk
end
end
end
49 changes: 46 additions & 3 deletions gems/autoextend/spec/autoextend_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,30 @@
# this is a weird thing we have to do to avoid a weird circular
# require problem
_x = ActiveSupport::Deprecation
ActiveSupport::Dependencies.autoload_paths << File.expand_path("..", __FILE__)
ActiveSupport::Dependencies.autoload_paths << File.expand_path("../autoload", __FILE__)
ActiveSupport::Dependencies.hook!

require 'autoextend'

# CANVAS_RAILS6_1 this pattern will need reworking in a rails >= 7.0 world
if ENV['WITH_ZEITWERK']
require 'zeitwerk'
require 'rails'
Rails.application = Class.new do
def self.config
Class.new do
def self.autoloader
:zeitwerk
end
end
end
end
require "active_support/dependencies/zeitwerk_integration"
ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: true)
# In an actual rails app this is handled by an initializer through railties
Autoextend.inject_into_zetwerk
end

describe Autoextend do
before do
module AutoextendSpec
Expand Down Expand Up @@ -55,6 +74,16 @@ def self.prepended(klass)
PrependHelper.register_prepend(klass, 3)
end
end

module PrependExistingMethod
def self.prepended(klass)
klass.a_method
end

def b_method
true
end
end
end
end

Expand Down Expand Up @@ -176,12 +205,26 @@ module AutoextendSpec::MyExtension; end
Autoextend.hook(:"AutoextendSpec::TestModule::Nested") do
hooked += 1
end
expect(defined?(AutoextendSpec::TestModule)).to equal(nil)
if ENV['WITH_ZEITWERK']
expect(AutoextendSpec.autoload?(:TestModule)).not_to be_nil
else
expect(defined?(AutoextendSpec::TestModule)).to be_nil
end
expect(hooked).to equal(0)
_x = AutoextendSpec::TestModule
# this could have only been detected by Rails' autoloading
expect(defined?(AutoextendSpec::TestModule)).to eq('constant')
if ENV['WITH_ZEITWERK']
expect(AutoextendSpec.autoload?(:TestModule)).to be_nil
else
expect(defined?(AutoextendSpec::TestModule)).to eq('constant')
end
expect(hooked).to equal(2)
end

it "hooks an autoloaded module after_load" do
# This method will call an existing method on load
Autoextend.hook(:"AutoextendSpec::TestLaterMethod", :"AutoextendSpec::PrependExistingMethod", method: :prepend, after_load: true)
expect(AutoextendSpec::TestLaterMethod.new.b_method).to eq(true)
end
end
end
24 changes: 24 additions & 0 deletions gems/autoextend/spec/autoload/autoextend_spec/test_later_method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

class AutoextendSpec::TestLaterMethod
def self.a_method
true
end
end
1 change: 1 addition & 0 deletions gems/autoextend/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ set -e

bundle check || bundle install
bundle exec rspec spec
WITH_ZEITWERK=true bundle exec rspec spec

0 comments on commit d6f5683

Please sign in to comment.