diff --git a/lib/tapioca/runtime/reflection.rb b/lib/tapioca/runtime/reflection.rb index 237112470..74aefbc6b 100644 --- a/lib/tapioca/runtime/reflection.rb +++ b/lib/tapioca/runtime/reflection.rb @@ -33,6 +33,15 @@ def constant_defined?(constant) # @without_runtime #: (String symbol, ?inherit: bool, ?namespace: Module[top]) -> BasicObject def constantize(symbol, inherit: false, namespace: Object) + # Rails deliberately avoids eager loading Configurable because loading it emits a deprecation. + # Leave it available for explicit use, and reflect on it normally if the application loaded it. + if (namespace == Object || symbol.start_with?("::")) && + symbol.match?(/\A(?:::)?ActiveSupport::Configurable(?:::|\z)/) && + defined?(::ActiveSupport) && ::ActiveSupport.autoload?(:Configurable) && + ::ActiveSupport.gem_version >= ::Gem::Version.new("8.1") + return UNDEFINED_CONSTANT + end + namespace.const_get(symbol, inherit) rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError UNDEFINED_CONSTANT diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index 70199bb38..5c62fe9e9 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1135,6 +1135,52 @@ class FakeRake::TaskLib; end RBI end + it "does not load unused ActiveSupport::Configurable when generating gem RBIs" do + skip("Configurable is not deprecated before Rails 8.1") if ActiveSupport.gem_version < ::Gem::Version.new("8.1") + + @project.require_real_gem("activesupport", ActiveSupport.gem_version.to_s, require: "active_support") + @project.write!("sorbet/tapioca/require.rb", <<~RB) + ActiveSupport.deprecator.behavior = :raise + configurable_autoload = ActiveSupport.autoload?(:Configurable) + at_exit do + raise "Configurable autoload changed" unless ActiveSupport.autoload?(:Configurable) == configurable_autoload + end + RB + @project.bundle_install! + + result = @project.tapioca("gem activesupport") + + assert_empty_stderr(result) + assert_success_status(result) + assert_project_file_match("sorbet/rbi/gems/activesupport@*.rbi") + assert_project_file_includes( + "sorbet/rbi/gems/activesupport@#{ActiveSupport.gem_version}.rbi", + "class ActiveSupport::BacktraceCleaner", + ) + end + + it "still warns and generates RBIs when the application uses ActiveSupport::Configurable" do + skip("ActiveSupport::Configurable was removed") unless ActiveSupport.const_defined?(:Configurable, false) + + @project.require_real_gem("activesupport", ActiveSupport.gem_version.to_s, require: "active_support") + @project.write!("sorbet/tapioca/require.rb", <<~RB) + ActiveSupport.deprecator.behavior = :stderr + ActiveSupport::Configurable + RB + @project.bundle_install! + + result = @project.tapioca("gem activesupport") + + assert_success_status(result) + if ActiveSupport.gem_version >= ::Gem::Version.new("8.1") + assert_stderr_includes(result, "ActiveSupport::Configurable is deprecated without replacement") + end + assert_project_file_includes( + "sorbet/rbi/gems/activesupport@#{ActiveSupport.gem_version}.rbi", + "module ActiveSupport::Configurable", + ) + end + it "uses the correct autoload, even when a gem redefines it via alias-method-chain" do foo = mock_gem("foo", "0.0.1") do write!("lib/foo.rb", <<~RB)