diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index 62df7d8b82..f2ca9c7cc9 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/inheritable_attributes' require 'active_support/core_ext/class/delegating_attributes' +require 'active_support/core_ext/class/subclasses' diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb new file mode 100644 index 0000000000..c166ce8079 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/object/blank' + +class Class #:nodoc: + # Returns an array with the names of the subclasses of +self+ as strings. + # + # Integer.subclasses # => ["Bignum", "Fixnum"] + def subclasses + Class.subclasses_of(self).map { |o| o.to_s } + end + + def reachable? #:nodoc: + eval("defined?(::#{self}) && ::#{self}.equal?(self)") + end + + # Rubinius + if defined?(Class.__subclasses__) + def descendents + subclasses = [] + __subclasses__.each {|k| subclasses << k; subclasses.concat k.descendents } + subclasses + end + else + # MRI + begin + ObjectSpace.each_object(Class.new) {} + + def descendents + subclasses = [] + ObjectSpace.each_object(class << self; self; end) do |k| + subclasses << k unless k == self + end + subclasses + end + # JRuby + rescue StandardError + def descendents + subclasses = [] + ObjectSpace.each_object(Class) do |k| + subclasses << k if k < self + end + subclasses.uniq! + subclasses + end + end + end + + # Exclude this class unless it's a subclass of our supers and is defined. + # We check defined? in case we find a removed class that has yet to be + # garbage collected. This also fails for anonymous classes -- please + # submit a patch if you have a workaround. + def self.subclasses_of(*superclasses) #:nodoc: + subclasses = [] + superclasses.each do |klass| + subclasses.concat klass.descendents.select {|k| k.name.blank? || k.reachable?} + end + subclasses + end +end diff --git a/activesupport/lib/active_support/core_ext/object/extending.rb b/activesupport/lib/active_support/core_ext/object/extending.rb new file mode 100644 index 0000000000..c4c37b6a2a --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/extending.rb @@ -0,0 +1,11 @@ +require 'active_support/core_ext/class/subclasses' + +class Object + # Exclude this class unless it's a subclass of our supers and is defined. + # We check defined? in case we find a removed class that has yet to be + # garbage collected. This also fails for anonymous classes -- please + # submit a patch if you have a workaround. + def subclasses_of(*superclasses) #:nodoc: + Class.subclasses_of(*superclasses) + end +end diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb new file mode 100644 index 0000000000..b7f3dd9930 --- /dev/null +++ b/activesupport/test/core_ext/class_test.rb @@ -0,0 +1,29 @@ +require 'abstract_unit' +require 'active_support/core_ext/class' + +class A +end + +module X + class B + end +end + +module Y + module Z + class C + end + end +end + +class ClassTest < Test::Unit::TestCase + def test_retrieving_subclasses + @parent = eval("class D; end; D") + @sub = eval("class E < D; end; E") + @subofsub = eval("class F < E; end; F") + assert_equal 2, @parent.subclasses.size + assert_equal [@subofsub.to_s], @sub.subclasses + assert_equal [], @subofsub.subclasses + assert_equal [@sub.to_s, @subofsub.to_s].sort, @parent.subclasses.sort + end +end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index 0b2a9c418e..f31e7774e9 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'active_support/core_ext/object' +require 'active_support/core_ext/class/subclasses' class ClassA; end class ClassB < ClassA; end @@ -39,6 +40,55 @@ class Foo include Bar end +class ClassExtTest < Test::Unit::TestCase + def test_subclasses_of_should_find_nested_classes + assert Class.subclasses_of(ClassK).include?(Nested::ClassL) + end + + def test_subclasses_of_should_not_return_removed_classes + # First create the removed class + old_class = Nested.class_eval { remove_const :ClassL } + new_class = Class.new(ClassK) + Nested.const_set :ClassL, new_class + assert_equal "Nested::ClassL", new_class.name # Sanity check + + subclasses = Class.subclasses_of(ClassK) + assert subclasses.include?(new_class) + assert ! subclasses.include?(old_class) + ensure + Nested.const_set :ClassL, old_class unless defined?(Nested::ClassL) + end + + def test_subclasses_of_should_not_trigger_const_missing + const_missing = false + Nested.on_const_missing { const_missing = true } + + subclasses = Class.subclasses_of ClassK + assert !const_missing + assert_equal [ Nested::ClassL ], subclasses + + removed = Nested.class_eval { remove_const :ClassL } # keep it in memory + subclasses = Class.subclasses_of ClassK + assert !const_missing + assert subclasses.empty? + ensure + Nested.const_set :ClassL, removed unless defined?(Nested::ClassL) + end + + def test_subclasses_of_with_multiple_roots + classes = Class.subclasses_of(ClassI, ClassK) + assert_equal %w(ClassJ Nested::ClassL), classes.collect(&:to_s).sort + end + + def test_subclasses_of_doesnt_find_anonymous_classes + assert_equal [], Class.subclasses_of(Foo) + bar = Class.new(Foo) + assert_nothing_raised do + assert_equal [bar], Class.subclasses_of(Foo) + end + end +end + class ObjectTests < Test::Unit::TestCase class DuckTime def acts_like_time?