diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 28be5bea94fee..78a8da245a117 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,13 +1,30 @@ # frozen_string_literal: true -module Enumerable - INDEX_WITH_DEFAULT = Object.new - private_constant :INDEX_WITH_DEFAULT +module ActiveSupport + module EnumerableCoreExt # :nodoc: + module Constants + private + def const_missing(name) + if name == :SoleItemExpectedError + ::ActiveSupport::EnumerableCoreExt::SoleItemExpectedError + else + super + end + end + end + end +end +module Enumerable # Error generated by +sole+ when called on an enumerable that doesn't have # exactly one item. class SoleItemExpectedError < StandardError; end + # HACK: For performance reasons, Enumerable shouldn't have any constants of its own. + # So we move SoleItemExpectedError into ActiveSupport::EnumerableCoreExt. + ActiveSupport::EnumerableCoreExt::SoleItemExpectedError = remove_const(:SoleItemExpectedError) + singleton_class.prepend(ActiveSupport::EnumerableCoreExt::Constants) + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements # when we omit an identity. @@ -106,17 +123,17 @@ def index_by # # %i( created_at updated_at ).index_with(Time.now) # # => { created_at: 2020-03-09 22:31:47, updated_at: 2020-03-09 22:31:47 } - def index_with(default = INDEX_WITH_DEFAULT) + def index_with(default = (no_default = true)) if block_given? result = {} each { |elem| result[elem] = yield(elem) } result - elsif default != INDEX_WITH_DEFAULT + elsif no_default + to_enum(:index_with) { size if respond_to?(:size) } + else result = {} each { |elem| result[elem] = default } result - else - to_enum(:index_with) { size if respond_to?(:size) } end end @@ -240,8 +257,8 @@ def in_order_of(key, series) def sole case count when 1 then return first # rubocop:disable Style/RedundantReturn - when 0 then raise SoleItemExpectedError, "no item found" - when 2.. then raise SoleItemExpectedError, "multiple items found" + when 0 then raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "no item found" + when 2.. then raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "multiple items found" end end end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 202da47321fe9..01b9d1c879516 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -360,4 +360,13 @@ def test_sole assert_raise(expected_raise) { GenericEnumerable.new([1, 2]).sole } assert_raise(expected_raise) { GenericEnumerable.new([1, nil]).sole } end + + def test_doesnt_bust_constant_cache + skip "Only applies to MRI" unless defined?(RubyVM.stat) && RubyVM.stat(:global_constant_state) + + object = Object.new + assert_no_difference -> { RubyVM.stat(:global_constant_state) } do + object.extend(Enumerable) + end + end end