Skip to content

Commit

Permalink
Define a convention for descendants and subclasses.
Browse files Browse the repository at this point in the history
The former should be symmetric with ancestors and include all children. However, it should not include self since ancestors + descendants should not have duplicated. The latter is symmetric to superclass in the sense it only includes direct children.

By adopting a convention, we expect to have less conflict with other frameworks, as Datamapper. For this moment, to ensure ActiveModel::Validations can be used with Datamapper, we should always call ActiveSupport::DescendantsTracker.descendants(self) internally instead of self.descendants avoiding conflicts.
  • Loading branch information
josevalim committed Jul 5, 2010
1 parent 5bf3294 commit a5dda97
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 192 deletions.
6 changes: 0 additions & 6 deletions actionpack/lib/action_controller/base.rb
Expand Up @@ -60,17 +60,11 @@ def self.without_modules(*modules)
include ActionController::Compatibility

def self.inherited(klass)
::ActionController::Base.subclasses << klass.to_s
super
klass.helper :all
end

def self.subclasses
@subclasses ||= []
end

config_accessor :asset_host, :asset_path

ActiveSupport.run_load_hooks(:action_controller, self)
end
end
Expand Down
1 change: 0 additions & 1 deletion actionpack/lib/action_controller/railtie.rb
Expand Up @@ -2,7 +2,6 @@
require "action_controller"
require "action_dispatch/railtie"
require "action_view/railtie"
require "active_support/core_ext/class/subclasses"
require "active_support/deprecation/proxy_wrappers"
require "active_support/deprecation"

Expand Down
4 changes: 2 additions & 2 deletions actionpack/lib/action_dispatch/routing/deprecated_mapper.rb
Expand Up @@ -19,8 +19,8 @@ def controller_constraints

def in_memory_controller_namespaces
namespaces = Set.new
ActionController::Base.subclasses.each do |klass|
controller_name = klass.underscore
ActionController::Base.descendants.each do |klass|
controller_name = klass.name.underscore
namespaces << controller_name.split('/')[0...-1].join('/')
end
namespaces.delete('')
Expand Down
6 changes: 3 additions & 3 deletions activerecord/lib/active_record/observer.rb
Expand Up @@ -94,7 +94,7 @@ class Observer < ActiveModel::Observer

def initialize
super
observed_subclasses.each { |klass| add_observer!(klass) }
observed_descendants.each { |klass| add_observer!(klass) }
end

def self.method_added(method)
Expand All @@ -108,8 +108,8 @@ def self.method_added(method)

protected

def observed_subclasses
observed_classes.sum([]) { |klass| klass.send(:descendants) }
def observed_descendants
observed_classes.sum([]) { |klass| klass.descendants }
end

def observe_callbacks?
Expand Down
4 changes: 2 additions & 2 deletions activesupport/lib/active_support/callbacks.rb
Expand Up @@ -432,7 +432,7 @@ def __update_callbacks(name, filters = [], block = nil) #:nodoc:
options = filters.last.is_a?(Hash) ? filters.pop : {}
filters.unshift(block) if block

([self] + self.descendants).each do |target|
([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target|
chain = target.send("_#{name}_callbacks")
yield chain, type, filters, options
target.__define_runner(name)
Expand Down Expand Up @@ -506,7 +506,7 @@ def skip_callback(name, *filter_list, &block)
def reset_callbacks(symbol)
callbacks = send("_#{symbol}_callbacks")

self.descendants.each do |target|
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
chain = target.send("_#{symbol}_callbacks")
callbacks.each { |c| chain.delete(c) }
target.__define_runner(symbol)
Expand Down
57 changes: 26 additions & 31 deletions activesupport/lib/active_support/core_ext/class/subclasses.rb
Expand Up @@ -2,54 +2,49 @@
require 'active_support/core_ext/module/reachable'

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

# Rubinius
if defined?(Class.__subclasses__)
alias :subclasses :__subclasses__

def descendants
subclasses = []
__subclasses__.each {|k| subclasses << k; subclasses.concat k.descendants }
subclasses
descendants = []
__subclasses__.each do |k|
descendants << k
descendants.concat k.descendants
end
descendants
end
else
# MRI
else # MRI
begin
ObjectSpace.each_object(Class.new) {}

def descendants
subclasses = []
descendants = []
ObjectSpace.each_object(class << self; self; end) do |k|
subclasses << k unless k == self
descendants.unshift k unless k == self
end
subclasses
descendants
end
# JRuby
rescue StandardError
rescue StandardError # JRuby
def descendants
subclasses = []
descendants = []
ObjectSpace.each_object(Class) do |k|
subclasses << k if k < self
descendants.unshift k if k < self
end
subclasses.uniq!
subclasses
descendants.uniq!
descendants
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.descendants.select {|k| k.anonymous? || k.reachable?}
# Returns an array with the direct children of +self+.
#
# Integer.subclasses # => [Bignum, Fixnum]
def subclasses
subclasses, chain = [], descendants
chain.each do |k|
subclasses << k unless chain.any? { |c| c > k }
end
subclasses
end
subclasses
end
end
1 change: 0 additions & 1 deletion activesupport/lib/active_support/core_ext/object.rb
Expand Up @@ -6,7 +6,6 @@
require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/instance_variables'
require 'active_support/core_ext/object/misc'
require 'active_support/core_ext/object/extending'

require 'active_support/core_ext/object/returning'
require 'active_support/core_ext/object/to_json'
Expand Down
11 changes: 0 additions & 11 deletions activesupport/lib/active_support/core_ext/object/extending.rb

This file was deleted.

24 changes: 14 additions & 10 deletions activesupport/lib/active_support/descendants_tracker.rb
Expand Up @@ -4,16 +4,23 @@ module ActiveSupport
# This module provides an internal implementation to track descendants
# which is faster than iterating through ObjectSpace.
module DescendantsTracker
@@descendants = Hash.new { |h, k| h[k] = [] }
@@direct_descendants = Hash.new { |h, k| h[k] = [] }

def self.descendants
@@descendants
def self.direct_descendants(klass)
@@direct_descendants[klass]
end

def self.descendants(klass)
@@direct_descendants[klass].inject([]) do |descendants, klass|
descendants << klass
descendants.concat klass.descendants
end
end

def self.clear
@@descendants.each do |klass, descendants|
@@direct_descendants.each do |klass, descendants|
if ActiveSupport::Dependencies.autoloaded?(klass)
@@descendants.delete(klass)
@@direct_descendants.delete(klass)
else
descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) }
end
Expand All @@ -26,14 +33,11 @@ def inherited(base)
end

def direct_descendants
@@descendants[self]
DescendantsTracker.direct_descendants(self)
end

def descendants
@@descendants[self].inject([]) do |descendants, klass|
descendants << klass
descendants.concat klass.descendants
end
DescendantsTracker.descendants(self)
end
end
end
40 changes: 19 additions & 21 deletions activesupport/test/core_ext/class_test.rb
@@ -1,29 +1,27 @@
require 'abstract_unit'
require 'active_support/core_ext/class'

class A
end
class ClassTest < Test::Unit::TestCase
class Parent; end
class Foo < Parent; end
class Bar < Foo; end
class Baz < Bar; end

module X
class B
end
end
class A < Parent; end
class B < A; end
class C < B; end

module Y
module Z
class C
end
def test_descendants
assert_equal [Foo, Bar, Baz, A, B, C], Parent.descendants
assert_equal [Bar, Baz], Foo.descendants
assert_equal [Baz], Bar.descendants
assert_equal [], Baz.descendants
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
def test_subclasses
assert_equal [Foo, A], Parent.subclasses
assert_equal [Bar], Foo.subclasses
assert_equal [Baz], Bar.subclasses
assert_equal [], Baz.subclasses
end
end
end
49 changes: 0 additions & 49 deletions activesupport/test/core_ext/object_and_class_ext_test.rb
Expand Up @@ -40,55 +40,6 @@ 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 < ActiveSupport::TestCase
class DuckTime
def acts_like_time?
Expand Down
8 changes: 5 additions & 3 deletions activesupport/test/descendants_tracker_test.rb
Expand Up @@ -37,7 +37,9 @@ def test_direct_descendants
def test_clear_with_autoloaded_parent_children_and_granchildren
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
assert ActiveSupport::DescendantsTracker.descendants.slice(*ALL).empty?
ALL.each do |k|
assert ActiveSupport::DescendantsTracker.descendants(k).empty?
end
end
end

Expand All @@ -64,12 +66,12 @@ def mark_as_autoloaded(*klasses)
old_autoloaded = ActiveSupport::Dependencies.autoloaded_constants.dup
ActiveSupport::Dependencies.autoloaded_constants = klasses.map(&:name)

old_descendants = ActiveSupport::DescendantsTracker.descendants.dup
old_descendants = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup
old_descendants.each { |k, v| old_descendants[k] = v.dup }

yield
ensure
ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded
ActiveSupport::DescendantsTracker.descendants.replace(old_descendants)
ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(old_descendants)
end
end

1 comment on commit a5dda97

@fxn
Copy link
Member

@fxn fxn commented on a5dda97 Jul 5, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/me applauds the guide patch :)

Please sign in to comment.