more correct metaclass compat checking and fixing
Jesse Luehrs [Thu, 17 Sep 2009 23:58:17 +0000 (18:58 -0500)]
Changes
lib/Class/MOP/Class.pm
t/010_self_introspection.t
t/041_metaclass_incompatibility.t

diff --git a/Changes b/Changes
index 41e39c0..c86218c 100644 (file)
--- a/Changes
+++ b/Changes
@@ -5,6 +5,13 @@ Revision history for Perl extension Class-MOP.
   * Packages and modules no longer have methods - this functionality was
     moved back up into Class::MOP::Class (doy).
 
+  [ENHANCEMENTS]
+
+  * Metaclass incompatibility checking now checks all metaclass types. (doy)
+  * Class::MOP can now do simple metaclass incompatibility fixing: if your
+    class's metaclass is a subclass of your parent class's metaclass, it will
+    just use the parent class's metaclass directly. (doy)
+
 1.01 Thu, May 26, 2010
 
   [NEW FEATURES]
index cbbc26d..939050c 100644 (file)
@@ -14,6 +14,7 @@ use Scalar::Util 'blessed', 'reftype', 'weaken';
 use Sub::Name    'subname';
 use Devel::GlobalDestruction 'in_global_destruction';
 use Try::Tiny;
+use List::MoreUtils 'all';
 
 our $VERSION   = '1.01';
 $VERSION = eval $VERSION;
@@ -167,40 +168,200 @@ sub update_package_cache_flag {
     $self->{'_package_cache_flag'} = Class::MOP::check_package_cache_flag($self->name);    
 }
 
+## Metaclass compatibility
+{
+    my %base_metaclass = (
+        attribute_metaclass      => 'Class::MOP::Attribute',
+        method_metaclass         => 'Class::MOP::Method',
+        wrapped_method_metaclass => 'Class::MOP::Method::Wrapped',
+        instance_metaclass       => 'Class::MOP::Instance',
+        constructor_class        => 'Class::MOP::Method::Constructor',
+        destructor_class         => 'Class::MOP::Method::Destructor',
+    );
+
+    sub _base_metaclasses { %base_metaclass }
+}
+
 sub _check_metaclass_compatibility {
     my $self = shift;
 
-    # this is always okay ...
-    return if ref($self)                eq 'Class::MOP::Class'   &&
-              $self->instance_metaclass eq 'Class::MOP::Instance';
+    if (my @superclasses = $self->superclasses) {
+        $self->_fix_metaclass_incompatibility(@superclasses);
 
-    my @class_list = $self->linearized_isa;
-    shift @class_list; # shift off $self->name
+        my %base_metaclass = $self->_base_metaclasses;
 
-    foreach my $superclass_name (@class_list) {
-        my $super_meta = Class::MOP::get_metaclass_by_name($superclass_name) || next;
+        # this is always okay ...
+        return if ref($self) eq 'Class::MOP::Class'
+            && all {
+                my $meta = $self->$_;
+                !defined($meta) || $meta eq $base_metaclass{$_}
+            } keys %base_metaclass;
 
-        # NOTE:
-        # we need to deal with the possibility
-        # of class immutability here, and then
-        # get the name of the class appropriately
-        my $super_meta_type
-            = $super_meta->is_immutable
-            ? $super_meta->_get_mutable_metaclass_name()
-            : ref($super_meta);
-
-        ($self->isa($super_meta_type))
-            || confess "The metaclass of " . $self->name . " ("
-                       . (ref($self)) . ")" .  " is not compatible with the " .
-                       "metaclass of its superclass, ".$superclass_name . " ("
-                       . ($super_meta_type) . ")";
-        # NOTE:
-        # we also need to check that instance metaclasses
-        # are compatibile in the same the class.
-        ($self->instance_metaclass->isa($super_meta->instance_metaclass))
-            || confess "The instance metaclass for " . $self->name . " (" . ($self->instance_metaclass) . ")" .
-                       " is not compatible with the " .
-                       "instance metaclass of its superclass, " . $superclass_name . " (" . ($super_meta->instance_metaclass) . ")";
+        for my $superclass (@superclasses) {
+            $self->_check_class_metaclass_compatibility($superclass);
+        }
+
+        for my $metaclass_type (keys %base_metaclass) {
+            next unless defined $self->$metaclass_type;
+            for my $superclass (@superclasses) {
+                $self->_check_single_metaclass_compatibility(
+                    $metaclass_type, $superclass
+                );
+            }
+        }
+    }
+}
+
+sub _check_class_metaclass_compatibility {
+    my $self = shift;
+    my ( $superclass_name ) = @_;
+
+    my $super_meta = Class::MOP::get_metaclass_by_name($superclass_name)
+        || return;
+
+    # NOTE:
+    # we need to deal with the possibility
+    # of class immutability here, and then
+    # get the name of the class appropriately
+    my $super_meta_type
+        = $super_meta->is_immutable
+        ? $super_meta->_get_mutable_metaclass_name()
+        : ref($super_meta);
+
+    ($self->isa($super_meta_type))
+        || confess "The metaclass of " . $self->name . " ("
+                 . (ref($self)) . ")" .  " is not compatible with "
+                 . "the metaclass of its superclass, "
+                 . $superclass_name . " (" . ($super_meta_type) . ")";
+}
+
+sub _check_single_metaclass_compatibility {
+    my $self = shift;
+    my ( $metaclass_type, $superclass_name ) = @_;
+
+    my $super_meta = Class::MOP::get_metaclass_by_name($superclass_name)
+        || return;
+
+    # for instance, Moose::Meta::Class has a error_class attribute, but
+    # Class::MOP::Class doesn't - this shouldn't be an error
+    return unless $super_meta->can($metaclass_type);
+    # for instance, Moose::Meta::Class has a destructor_class, but
+    # Class::MOP::Class doesn't - this shouldn't be an error
+    return if defined $self->$metaclass_type
+            && !defined $super_meta->$metaclass_type;
+
+    my $metaclass_type_name = $metaclass_type;
+    $metaclass_type_name =~ s/_(?:meta)?class$//;
+    $metaclass_type_name =~ s/_/ /g;
+    ($self->$metaclass_type->isa($super_meta->$metaclass_type))
+        || confess "The $metaclass_type_name metaclass for "
+                 . $self->name . " (" . ($self->$metaclass_type)
+                 . ")" . " is not compatible with the "
+                 . "$metaclass_type_name metaclass of its "
+                 . "superclass, " . $superclass_name . " ("
+                 . ($super_meta->$metaclass_type) . ")";
+}
+
+sub _can_fix_class_metaclass_incompatibility_by_subclassing {
+    my $self = shift;
+    my ($super_meta) = @_;
+
+    # NOTE:
+    # we need to deal with the possibility
+    # of class immutability here, and then
+    # get the name of the class appropriately
+    my $super_meta_type
+        = $super_meta->is_immutable
+        ? $super_meta->_get_mutable_metaclass_name()
+        : ref($super_meta);
+
+    return $super_meta_type ne blessed($self)
+        && $super_meta->isa(blessed($self));
+}
+
+sub _can_fix_single_metaclass_incompatibility_by_subclassing {
+    my $self = shift;
+    my ($metaclass_type, $super_meta) = @_;
+
+    my $specific_meta = $self->$metaclass_type;
+    return unless $super_meta->can($metaclass_type);
+    my $super_specific_meta = $super_meta->$metaclass_type;
+
+    # for instance, Moose::Meta::Class has a destructor_class, but
+    # Class::MOP::Class doesn't - this shouldn't be an error
+    return if defined $specific_meta
+           && !defined $super_specific_meta;
+
+    return $specific_meta ne $super_specific_meta
+        && $super_specific_meta->isa($specific_meta);
+}
+
+sub _can_fix_metaclass_incompatibility_by_subclassing {
+    my $self = shift;
+    my ($super_meta) = @_;
+
+    return 1 if $self->_can_fix_class_metaclass_incompatibility_by_subclassing($super_meta);
+
+    my %base_metaclass = $self->_base_metaclasses;
+    for my $metaclass_type (keys %base_metaclass) {
+        next unless defined $self->$metaclass_type;
+        return 1 if $self->_can_fix_single_metaclass_incompatibility_by_subclassing($metaclass_type, $super_meta);
+    }
+
+    return;
+}
+
+sub _can_fix_metaclass_incompatibility {
+    my $self = shift;
+    return $self->_can_fix_metaclass_incompatibility_by_subclassing(@_);
+}
+
+sub _fix_metaclass_incompatibility {
+    my $self = shift;
+    my @supers = @_;
+
+    my $necessary = 0;
+    for my $super (map { Class::MOP::Class->initialize($_) } @supers) {
+        $necessary = 1
+            if $self->_can_fix_metaclass_incompatibility($super);
+    }
+    return unless $necessary;
+
+    ($self->is_pristine)
+        || confess "Can't fix metaclass incompatibility for "
+                 . $self->name
+                 . " because it is not pristine.";
+
+    for my $super (map { Class::MOP::Class->initialize($_) } @supers) {
+        $self->_fix_class_metaclass_incompatibility($super);
+    }
+
+    my %base_metaclass = $self->_base_metaclasses;
+    for my $metaclass_type (keys %base_metaclass) {
+        next unless defined $self->$metaclass_type;
+        for my $super (map { Class::MOP::Class->initialize($_) } @supers) {
+            $self->_fix_single_metaclass_incompatibility(
+                $metaclass_type, $super
+            );
+        }
+    }
+}
+
+sub _fix_class_metaclass_incompatibility {
+    my $self = shift;
+    my ( $super_meta ) = @_;
+
+    if ($self->_can_fix_class_metaclass_incompatibility_by_subclassing($super_meta)) {
+        $super_meta->meta->rebless_instance($self);
+    }
+}
+
+sub _fix_single_metaclass_incompatibility {
+    my $self = shift;
+    my ( $metaclass_type, $super_meta ) = @_;
+
+    if ($self->_can_fix_single_metaclass_incompatibility_by_subclassing($metaclass_type, $super_meta)) {
+        $self->{$metaclass_type} = $super_meta->$metaclass_type;
     }
 }
 
index 00188fb..889c9cf 100644 (file)
@@ -67,6 +67,13 @@ my @class_mop_class_methods = qw(
     clone_instance _clone_instance
     rebless_instance rebless_instance_back rebless_instance_away
     check_metaclass_compatibility _check_metaclass_compatibility
+    _check_class_metaclass_compatibility _check_single_metaclass_compatibility
+    _fix_metaclass_incompatibility _fix_class_metaclass_incompatibility
+    _fix_single_metaclass_incompatibility _base_metaclasses
+    _can_fix_class_metaclass_incompatibility_by_subclassing
+    _can_fix_single_metaclass_incompatibility_by_subclassing
+    _can_fix_metaclass_incompatibility_by_subclassing
+    _can_fix_metaclass_incompatibility
 
     add_meta_instance_dependencies remove_meta_instance_dependencies update_meta_instance_dependencies
     add_dependent_meta_instance remove_dependent_meta_instance
index 80f693e..990d55a 100644 (file)
@@ -2,65 +2,148 @@ use strict;
 use warnings;
 
 use Test::More;
+use Test::Exception;
 
 use metaclass;
 
+my %metaclass_attrs = (
+    'Instance'            => 'instance_metaclass',
+    'Attribute'           => 'attribute_metaclass',
+    'Method'              => 'method_metaclass',
+    'Method::Wrapped'     => 'wrapped_method_metaclass',
+    'Method::Constructor' => 'constructor_class',
+);
+
 # meta classes
+for my $suffix ('Class', keys %metaclass_attrs) {
+    Class::MOP::Class->create(
+        "Foo::Meta::$suffix",
+        superclasses => ["Class::MOP::$suffix"]
+    );
+    Class::MOP::Class->create(
+        "Bar::Meta::$suffix",
+        superclasses => ["Class::MOP::$suffix"]
+    );
+    Class::MOP::Class->create(
+        "FooBar::Meta::$suffix",
+        superclasses => ["Foo::Meta::$suffix", "Bar::Meta::$suffix"]
+    );
+}
+
+# checking...
+
+lives_ok {
+    Foo::Meta::Class->create('Foo')
+} '... Foo.meta => Foo::Meta::Class is compatible';
+lives_ok {
+    Bar::Meta::Class->create('Bar')
+} '... Bar.meta => Bar::Meta::Class is compatible';
+
+throws_ok {
+    Bar::Meta::Class->create('Foo::Foo', superclasses => ['Foo'])
+} qr/compatible/, '... Foo::Foo.meta => Bar::Meta::Class is not compatible';
+throws_ok {
+    Foo::Meta::Class->create('Bar::Bar', superclasses => ['Bar'])
+} qr/compatible/, '... Bar::Bar.meta => Foo::Meta::Class is not compatible';
+
+lives_ok {
+    FooBar::Meta::Class->create('FooBar', superclasses => ['Foo'])
+} '... FooBar.meta => FooBar::Meta::Class is compatible';
+lives_ok {
+    FooBar::Meta::Class->create('FooBar2', superclasses => ['Bar'])
+} '... FooBar2.meta => FooBar::Meta::Class is compatible';
+
+Foo::Meta::Class->create(
+    'Foo::All',
+    map { $metaclass_attrs{$_} => "Foo::Meta::$_" } keys %metaclass_attrs,
+);
+
+throws_ok {
+    Bar::Meta::Class->create(
+        'Foo::All::Sub::Class',
+        superclasses => ['Foo::All'],
+        map { $metaclass_attrs{$_} => "Foo::Meta::$_" } keys %metaclass_attrs,
+    )
+} qr/compatible/, 'incompatible Class metaclass';
+for my $suffix (keys %metaclass_attrs) {
+    throws_ok {
+        Foo::Meta::Class->create(
+            "Foo::All::Sub::$suffix",
+            superclasses => ['Foo::All'],
+            (map { $metaclass_attrs{$_} => "Foo::Meta::$_" } keys %metaclass_attrs),
+            $metaclass_attrs{$suffix} => "Bar::Meta::$suffix",
+        )
+    } qr/compatible/, "incompatible $suffix metaclass";
+}
+
+# fixing...
+
+lives_ok {
+    Class::MOP::Class->create('Foo::Foo::CMOP', superclasses => ['Foo'])
+} 'metaclass fixing fixes a cmop metaclass, when the parent has a subclass';
+isa_ok(Foo::Foo::CMOP->meta, 'Foo::Meta::Class');
+lives_ok {
+    Class::MOP::Class->create('Bar::Bar::CMOP', superclasses => ['Bar'])
+} 'metaclass fixing fixes a cmop metaclass, when the parent has a subclass';
+isa_ok(Bar::Bar::CMOP->meta, 'Bar::Meta::Class');
+
+lives_ok {
+    Class::MOP::Class->create(
+        'Foo::All::Sub::CMOP::Class',
+        superclasses => ['Foo::All'],
+        map { $metaclass_attrs{$_} => "Foo::Meta::$_" } keys %metaclass_attrs,
+    )
+} 'metaclass fixing works with other non-default metaclasses';
+isa_ok(Foo::All::Sub::CMOP::Class->meta, 'Foo::Meta::Class');
+
+for my $suffix (keys %metaclass_attrs) {
+    lives_ok {
+        Foo::Meta::Class->create(
+            "Foo::All::Sub::CMOP::$suffix",
+            superclasses => ['Foo::All'],
+            (map { $metaclass_attrs{$_} => "Foo::Meta::$_" } keys %metaclass_attrs),
+            $metaclass_attrs{$suffix} => "Class::MOP::$suffix",
+        )
+    } "$metaclass_attrs{$suffix} fixing works with other non-default metaclasses";
+    for my $suffix2 (keys %metaclass_attrs) {
+        my $method = $metaclass_attrs{$suffix2};
+        isa_ok("Foo::All::Sub::CMOP::$suffix"->meta->$method, "Foo::Meta::$suffix2");
+    }
+}
+
+# initializing...
+
 {
-    package Foo::Meta;
-    use base 'Class::MOP::Class';
+    package Foo::NoMeta;
+}
 
-    package Bar::Meta;
-    use base 'Class::MOP::Class';
+Class::MOP::Class->create('Foo::NoMeta::Sub', superclasses => ['Foo::NoMeta']);
+ok(!Foo::NoMeta->can('meta'), "non-cmop superclass doesn't get methods installed");
+isa_ok(Class::MOP::class_of('Foo::NoMeta'), 'Class::MOP::Class');
+isa_ok(Foo::NoMeta::Sub->meta, 'Class::MOP::Class');
 
-    package FooBar::Meta;
-    use base 'Foo::Meta', 'Bar::Meta';
+{
+    package Foo::NoMeta2;
 }
+Foo::Meta::Class->create('Foo::NoMeta2::Sub', superclasses => ['Foo::NoMeta2']);
+ok(!Foo::NoMeta->can('meta'), "non-cmop superclass doesn't get methods installed");
+isa_ok(Class::MOP::class_of('Foo::NoMeta2'), 'Class::MOP::Class');
+isa_ok(Foo::NoMeta2::Sub->meta, 'Foo::Meta::Class');
+
+# unsafe fixing...
 
-$@ = undef;
-eval {
-    package Foo;
-    metaclass->import('Foo::Meta');
-};
-ok(!$@, '... Foo.meta => Foo::Meta is compatible') || diag $@;
-
-$@ = undef;
-eval {
-    package Bar;
-    metaclass->import('Bar::Meta');
-};
-ok(!$@, '... Bar.meta => Bar::Meta is compatible') || diag $@;
-
-$@ = undef;
-eval {
-    package Foo::Foo;
-    use base 'Foo';
-    metaclass->import('Bar::Meta');
-};
-ok($@, '... Foo::Foo.meta => Bar::Meta is not compatible') || diag $@;
-
-$@ = undef;
-eval {
-    package Bar::Bar;
-    use base 'Bar';
-    metaclass->import('Foo::Meta');
-};
-ok($@, '... Bar::Bar.meta => Foo::Meta is not compatible') || diag $@;
-
-$@ = undef;
-eval {
-    package FooBar;
-    use base 'Foo';
-    metaclass->import('FooBar::Meta');
-};
-ok(!$@, '... FooBar.meta => FooBar::Meta is compatible') || diag $@;
-
-$@ = undef;
-eval {
-    package FooBar2;
-    use base 'Bar';
-    metaclass->import('FooBar::Meta');
-};
-ok(!$@, '... FooBar2.meta => FooBar::Meta is compatible') || diag $@;
+{
+    Class::MOP::Class->create(
+        'Foo::Unsafe',
+        attribute_metaclass => 'Foo::Meta::Attribute',
+    );
+    my $meta = Class::MOP::Class->create(
+        'Foo::Unsafe::Sub',
+    );
+    $meta->add_attribute(foo => reader => 'foo');
+    throws_ok { $meta->superclasses('Foo::Unsafe') }
+              qr/compatibility.*pristine/,
+              "can't switch out the attribute metaclass of a class that already has attributes";
+}
 
 done_testing;