1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Fix singleton class cloning

Before this commit, `clone` gave different results depending on whether the original object
had an attached singleton class or not.

Consider the following setup:
```
class Foo; end
Foo.singleton_class.define_method(:foo) {}

obj = Foo.new

obj.singleton_class if $call_singleton

clone = obj.clone
```

When `$call_singleton = false`, neither `obj.singleton_class.singleton_class` nor
`clone.singleton_class.singleton_class` own any methods.

However, when `$call_singleton = true`, `clone.singleton_class.singleton_class` would own a copy of
`foo` from `Foo.singleton_class`, even though `obj.singleton_class.singleton_class` does not.

The latter case is unexpected and results in a visibly different clone, depending on if the original object
had an attached class or not.

Co-authored-by: Ufuk Kayserilioglu <ufuk.kayserilioglu@shopify.com>
This commit is contained in:
Alan Wu 2020-11-11 16:38:03 -05:00
parent 084e7e31b2
commit ebb96fa880
Notes: git 2020-11-17 07:41:42 +09:00
2 changed files with 69 additions and 9 deletions

31
class.c
View file

@ -40,6 +40,9 @@
#define id_attached id__attached__
#define METACLASS_OF(k) RBASIC(k)->klass
#define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls)
void
rb_class_subclass_add(VALUE super, VALUE klass)
{
@ -457,22 +460,35 @@ rb_singleton_class_clone(VALUE obj)
return rb_singleton_class_clone_and_attach(obj, Qundef);
}
// Clone and return the singleton class of `obj` if it has been created and is attached to `obj`.
VALUE
rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach)
{
const VALUE klass = RBASIC(obj)->klass;
if (!FL_TEST(klass, FL_SINGLETON))
return klass;
// Note that `rb_singleton_class()` can create situations where `klass` is
// attached to an object other than `obj`. In which case `obj` does not have
// a material singleton class attached yet and there is no singleton class
// to clone.
if (!(FL_TEST(klass, FL_SINGLETON) && rb_attr_get(klass, id_attached) == obj)) {
// nothing to clone
return klass;
}
else {
/* copy singleton(unnamed) class */
bool klass_of_clone_is_new;
VALUE clone = class_alloc(RBASIC(klass)->flags, 0);
if (BUILTIN_TYPE(obj) == T_CLASS) {
klass_of_clone_is_new = true;
RBASIC_SET_CLASS(clone, clone);
}
else {
RBASIC_SET_CLASS(clone, rb_singleton_class_clone(klass));
VALUE klass_metaclass_clone = rb_singleton_class_clone(klass);
// When `METACLASS_OF(klass) == klass_metaclass_clone`, it means the
// recursive call did not clone `METACLASS_OF(klass)`.
klass_of_clone_is_new = (METACLASS_OF(klass) != klass_metaclass_clone);
RBASIC_SET_CLASS(clone, klass_metaclass_clone);
}
RCLASS_SET_SUPER(clone, RCLASS_SUPER(klass));
@ -496,7 +512,9 @@ rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach)
arg.new_klass = clone;
rb_id_table_foreach(RCLASS_M_TBL(klass), clone_method_i, &arg);
}
rb_singleton_class_attached(RBASIC(clone)->klass, clone);
if (klass_of_clone_is_new) {
rb_singleton_class_attached(RBASIC(clone)->klass, clone);
}
FL_SET(clone, FL_SINGLETON);
return clone;
@ -518,11 +536,6 @@ rb_singleton_class_attached(VALUE klass, VALUE obj)
}
}
#define METACLASS_OF(k) RBASIC(k)->klass
#define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls)
/*!
* whether k is a meta^(n)-class of Class class
* @retval 1 if \a k is a meta^(n)-class of Class class (n >= 0)

View file

@ -483,6 +483,53 @@ class TestClass < Test::Unit::TestCase
assert_equal(:foo, d.foo)
end
def test_clone_singleton_class_exists
klass = Class.new do
def self.bar; :bar; end
end
o = klass.new
o.singleton_class
clone = o.clone
assert_empty(o.singleton_class.instance_methods(false))
assert_empty(clone.singleton_class.instance_methods(false))
assert_empty(o.singleton_class.singleton_class.instance_methods(false))
assert_empty(clone.singleton_class.singleton_class.instance_methods(false))
end
def test_clone_when_singleton_class_of_singleton_class_exists
klass = Class.new do
def self.bar; :bar; end
end
o = klass.new
o.singleton_class.singleton_class
clone = o.clone
assert_empty(o.singleton_class.instance_methods(false))
assert_empty(clone.singleton_class.instance_methods(false))
assert_empty(o.singleton_class.singleton_class.instance_methods(false))
assert_empty(clone.singleton_class.singleton_class.instance_methods(false))
end
def test_clone_when_method_exists_on_singleton_class_of_singleton_class
klass = Class.new do
def self.bar; :bar; end
end
o = klass.new
o.singleton_class.singleton_class.define_method(:s2_method) { :s2 }
clone = o.clone
assert_empty(o.singleton_class.instance_methods(false))
assert_empty(clone.singleton_class.instance_methods(false))
assert_equal(:s2, o.singleton_class.s2_method)
assert_equal(:s2, clone.singleton_class.s2_method)
assert_equal([:s2_method], o.singleton_class.singleton_class.instance_methods(false))
assert_equal([:s2_method], clone.singleton_class.singleton_class.instance_methods(false))
end
def test_singleton_class_p
feature7609 = '[ruby-core:51087] [Feature #7609]'
assert_predicate(self.singleton_class, :singleton_class?, feature7609)