Add 'PERL_VERSION' foldable constant, switch lib-ish things over
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / _Util.pm
index 31f038f..76f9b35 100644 (file)
@@ -6,7 +6,7 @@ use DBIx::Class::StartupCheck;  # load es early as we can, usually a noop
 use warnings;
 use strict;
 
-use constant SPURIOUS_VERSION_CHECK_WARNINGS => ( "$]" < 5.010 ? 1 : 0);
+my $mro_recursor_stack;
 
 BEGIN {
   package # hide from pause
@@ -15,15 +15,23 @@ BEGIN {
   use Config;
 
   use constant {
+    PERL_VERSION => "$]",
+    OS_NAME => "$^O",
+  };
+
+  use constant {
 
     # but of course
-    BROKEN_FORK => ($^O eq 'MSWin32') ? 1 : 0,
+    BROKEN_FORK => (OS_NAME eq 'MSWin32') ? 1 : 0,
 
-    BROKEN_GOTO => ( "$]" < 5.008003 ) ? 1 : 0,
+    BROKEN_GOTO => ( PERL_VERSION < 5.008003 ) ? 1 : 0,
+
+    # perl -MScalar::Util=weaken -e 'weaken( $hash{key} = \"value" )'
+    BROKEN_WEAK_SCALARREF_VALUES => ( PERL_VERSION < 5.008003 ) ? 1 : 0,
 
     HAS_ITHREADS => $Config{useithreads} ? 1 : 0,
 
-    UNSTABLE_DOLLARAT => ( "$]" < 5.013002 ) ? 1 : 0,
+    UNSTABLE_DOLLARAT => ( PERL_VERSION < 5.013002 ) ? 1 : 0,
 
     ( map
       #
@@ -42,17 +50,85 @@ BEGIN {
     ),
 
     IV_SIZE => $Config{ivsize},
-
-    OS_NAME => $^O,
   };
 
-  if ( "$]" < 5.009_005) {
+  if ( PERL_VERSION < 5.009_005) {
     require MRO::Compat;
     constant->import( OLD_MRO => 1 );
+
+    #
+    # Yes, I know this is a rather PHP-ish name, but please first read
+    # https://metacpan.org/source/BOBTFISH/MRO-Compat-0.12/lib/MRO/Compat.pm#L363-368
+    #
+    # Even if we are using Class::C3::XS it still won't work, as doing
+    #   defined( *{ "SubClass::"->{$_} }{CODE} )
+    # will set pkg_gen to the same value for SubClass and *ALL PARENTS*
+    #
+    *DBIx::Class::_Util::get_real_pkg_gen = sub ($) {
+      require Digest::MD5;
+      require Math::BigInt;
+
+      # the non-assign-unless-there-is-a-hash is deliberate
+      ( $mro_recursor_stack->{cache} || {} )->{$_[0]}{gen} ||= (
+        Math::BigInt->new( '0x' . ( Digest::MD5::md5_hex( join "\0", map {
+
+          ( $mro_recursor_stack->{cache} || {} )->{$_}{methlist} ||= do {
+
+            my $class = $_;
+            no strict 'refs';
+
+            # RV to be hashed up and turned into a number
+            join "\0", (
+              $class,
+              map
+                {(
+                  # stringification should be sufficient, ignore names/refaddr entirely
+                  $_,
+                  attributes::get( $_ ),
+                )}
+                map
+                  {(
+                    # skip dummy C::C3 helper crefs
+                    ! ( ( $Class::C3::MRO{$class} || {} )->{methods}{$_} )
+                      and
+                    (
+                      ref(\ "${class}::"->{$_} ) ne 'GLOB'
+                        or
+                      defined( *{ "${class}::"->{$_} }{CODE} )
+                    )
+                  )
+                    ? ( \&{"${class}::$_"} )
+                    : ()
+                  }
+                  keys %{ "${class}::" }
+            );
+          }
+        } (
+
+          @{
+            ( $mro_recursor_stack->{cache} || {} )->{$_[0]}{linear_isa}
+              ||=
+            mro::get_linear_isa($_[0])
+          },
+
+          ((
+            ( $mro_recursor_stack->{cache} || {} )->{$_[0]}{is_universal}
+              ||=
+            mro::is_universal($_[0])
+          ) ? () : @{
+            ( $mro_recursor_stack->{cache} || {} )->{UNIVERSAL}{linear_isa}
+              ||=
+            mro::get_linear_isa("UNIVERSAL")
+          } ),
+
+        ) ) ) )
+      );
+    };
   }
   else {
     require mro;
     constant->import( OLD_MRO => 0 );
+    *DBIx::Class::_Util::get_real_pkg_gen = \&mro::get_pkg_gen;
   }
 
   # Both of these are no longer used for anything. However bring
@@ -74,6 +150,8 @@ BEGIN {
   sub PEEPEENESS () { &$sigh }
 }
 
+use constant SPURIOUS_VERSION_CHECK_WARNINGS => ( DBIx::Class::_ENV_::PERL_VERSION < 5.010 ? 1 : 0);
+
 # FIXME - this is not supposed to be here
 # Carp::Skip to the rescue soon
 use DBIx::Class::Carp '^DBIx::Class|^DBICTest';
@@ -84,6 +162,7 @@ use Storable 'nfreeze';
 use Scalar::Util qw(weaken blessed reftype refaddr);
 use Sub::Quote qw(qsub);
 use Sub::Name ();
+use attributes ();
 
 # Already correctly prototyped: perlbrew exec perl -MStorable -e 'warn prototype \&Storable::dclone'
 BEGIN { *deep_clone = \&Storable::dclone }
@@ -92,26 +171,20 @@ use base 'Exporter';
 our @EXPORT_OK = qw(
   sigwarn_silencer modver_gt_or_eq modver_gt_or_eq_and_lt
   fail_on_internal_wantarray fail_on_internal_call
-  refdesc refcount hrefaddr set_subname
+  refdesc refcount hrefaddr set_subname describe_class_methods
   scope_guard detected_reinvoked_destructor
-  is_exception dbic_internal_try
-  quote_sub qsub perlstring serialize deep_clone dump_value
+  is_exception dbic_internal_try visit_namespaces
+  quote_sub qsub perlstring serialize deep_clone dump_value uniq
   parent_dir mkdir_p
   UNRESOLVABLE_CONDITION
 );
 
 use constant UNRESOLVABLE_CONDITION => \ '1 = 0';
 
-BEGIN {
-  # add preliminary attribute support
-  # FIXME FIXME FIXME
-  # To be revisited when Moo with proper attr support ships
-  Sub::Quote->VERSION(2.002);
-  require attributes;
-}
 # Override forcing no_defer, and adding naming consistency checks
+our %refs_closed_over_by_quote_sub_installed_crefs;
 sub quote_sub {
-  Carp::confess( "Anonymous quoting not supported by the DBIC sub_quote override - supply a sub name" ) if
+  Carp::confess( "Anonymous quoting not supported by the DBIC quote_sub override - supply a sub name" ) if
     @_ < 2
       or
     ! defined $_[1]
@@ -119,16 +192,27 @@ sub quote_sub {
     length ref $_[1]
   ;
 
-  Carp::confess( "The DBIC sub_quote override expects sub name '$_[0]' to be fully qualified" )
-    unless $_[0] =~ /::/;
+  Carp::confess( "The DBIC quote_sub override expects sub name '$_[0]' to be fully qualified" )
+    unless (my $stash) = $_[0] =~ /^(.+)::/;
+
+  Carp::confess(
+    "The DBIC sub_quote override does not support 'no_install'"
+  ) if (
+    $_[3]
+      and
+    $_[3]->{no_install}
+  );
 
-  Carp::confess( "The DBIC sub_quote override expects the sub name '$_[0]' to match the supplied 'package' argument" ) if
+  Carp::confess(
+    'The DBIC quote_sub override expects the namespace-part of sub name '
+  . "'$_[0]' to match the supplied package argument '$_[3]->{package}'"
+  ) if (
     $_[3]
       and
     defined $_[3]->{package}
       and
-    index( $_[0], $_[3]->{package} ) != 0
-  ;
+    $stash ne $_[3]->{package}
+  );
 
   my @caller = caller(0);
   my $sq_opts = {
@@ -142,28 +226,23 @@ sub quote_sub {
     no_defer => 1,
   };
 
-  my $cref = Sub::Quote::quote_sub( $_[0], $_[1], $_[2]||{}, $sq_opts );
-
-  # FIXME FIXME FIXME
-  # To be revisited when Moo with proper attr support ships
-  if(
-    # external application does not work on things like :prototype(...), :lvalue, etc
-    my @attrs = grep {
-      $_ !~ /^[a-z]/
+  weaken (
+    # just use a growing counter, no need to perform neither compaction
+    # nor any special ithread-level handling
+    $refs_closed_over_by_quote_sub_installed_crefs
+     { scalar keys %refs_closed_over_by_quote_sub_installed_crefs }
+      = $_
+  ) for grep {
+    length ref $_
+      and
+    (
+      ! DBIx::Class::_ENV_::BROKEN_WEAK_SCALARREF_VALUES
         or
-      Carp::confess( "The DBIC sub_quote override does not support applying of reserved attribute '$_'" )
-    } @{ $sq_opts->{attributes} || []}
-  ) {
-    Carp::confess( "The DBIC sub_quote override does not allow mixing 'attributes' with 'no_install'" )
-      if $sq_opts->{no_install};
-
-    # might be different from $sq_opts->{package};
-    my ($install_into) = $_[0] =~ /(.+)::[^:]+$/;
-
-    attributes->import( $install_into, $cref, @attrs );
-  }
+      ref $_ ne 'SCALAR'
+    )
+  } values %{ $_[2] || {} };
 
-  $cref;
+  Sub::Quote::quote_sub( $_[0], $_[1], $_[2]||{}, $sq_opts );
 }
 
 sub sigwarn_silencer ($) {
@@ -200,6 +279,36 @@ sub refcount ($) {
   B::svref_2object($_[0])->REFCNT;
 }
 
+sub visit_namespaces {
+  my $args = { (ref $_[0]) ? %{$_[0]} : @_ };
+
+  my $visited_count = 1;
+
+  # A package and a namespace are subtly different things
+  $args->{package} ||= 'main';
+  $args->{package} = 'main' if $args->{package} =~ /^ :: (?: main )? $/x;
+  $args->{package} =~ s/^:://;
+
+  if ( $args->{action}->($args->{package}) ) {
+    my $ns =
+      ( ($args->{package} eq 'main') ? '' :  $args->{package} )
+        .
+      '::'
+    ;
+
+    $visited_count += visit_namespaces( %$args, package => $_ ) for
+      grep
+        # this happens sometimes on %:: traversal
+        { $_ ne '::main' }
+        map
+          { $_ =~ /^(.+?)::$/ ? "$ns$1" : () }
+          do { no strict 'refs'; keys %$ns }
+    ;
+  }
+
+  $visited_count;
+}
+
 # FIXME In another life switch this to a polyfill like the one in namespace::clean
 sub set_subname ($$) {
 
@@ -215,6 +324,15 @@ sub serialize ($) {
   nfreeze($_[0]);
 }
 
+sub uniq {
+  my( %seen, $seen_undef, $numeric_preserving_copy );
+  grep { not (
+    defined $_
+      ? $seen{ $numeric_preserving_copy = $_ }++
+      : $seen_undef++
+  ) } @_;
+}
+
 my $dd_obj;
 sub dump_value ($) {
   local $Data::Dumper::Indent = 1
@@ -437,7 +555,7 @@ sub is_exception ($) {
 {
   my $destruction_registry = {};
 
-  sub CLONE {
+  sub DBIx::Class::__Util_iThreads_handler__::CLONE {
     %$destruction_registry = map {
       (defined $_)
         ? ( refaddr($_) => $_ )
@@ -536,6 +654,166 @@ sub modver_gt_or_eq_and_lt ($$$) {
   ) ? 1 : 0;
 }
 
+{
+  # FIXME - should be a private my(), but I'm too uncertain whether
+  # all bases are covered
+  our $describe_class_query_cache;
+
+  sub describe_class_methods {
+    my ($class, $requested_mro) = @_;
+
+    croak "Expecting a class name"
+      if not defined $class or $class !~ $module_name_rx;
+
+    $requested_mro ||= mro::get_mro($class);
+
+    # mro::set_mro() does not bump pkg_gen - WHAT THE FUCK?!
+    my $query_cache_key = "$class|$requested_mro";
+
+    my $stack_cache_key =
+      ( mro::get_mro($class) eq $requested_mro )
+        ? $class
+        : $query_cache_key
+    ;
+
+    # use a cache on old MRO, since while we are recursing in this function
+    # nothing can possibly change (the speedup is immense)
+    # (yes, people could be tie()ing the stash and adding methods on access
+    # but there is a limit to how much crazy can be supported here)
+    #
+    # we use the cache for linear_isa lookups on new MRO as well - it adds
+    # a *tiny* speedup, and simplifies the code a lot
+    #
+    local $mro_recursor_stack->{cache} = {}
+      unless $mro_recursor_stack->{cache};
+
+    my $my_gen = 0;
+
+    $my_gen += get_real_pkg_gen($_) for ( my @full_ISA = (
+
+      @{
+        $mro_recursor_stack->{cache}{$stack_cache_key}{linear_isa}
+          ||=
+        mro::get_linear_isa($class, $requested_mro)
+      },
+
+      ((
+        $mro_recursor_stack->{cache}{$class}{is_universal}
+          ||=
+        mro::is_universal($class)
+      ) ? () : @{
+        $mro_recursor_stack->{cache}{UNIVERSAL}{linear_isa}
+          ||=
+        mro::get_linear_isa("UNIVERSAL")
+      }),
+
+    ));
+
+    my $slot = $describe_class_query_cache->{$query_cache_key} ||= {};
+
+    unless ( ($slot->{cumulative_gen}||0) == $my_gen ) {
+
+      # remove ourselves from ISA
+      shift @full_ISA;
+
+      # reset
+      %$slot = (
+        class => $class,
+        isa => [
+          @{ $mro_recursor_stack->{cache}{$stack_cache_key}{linear_isa} }
+            [ 1 .. $#{$mro_recursor_stack->{cache}{$stack_cache_key}{linear_isa}} ]
+        ],
+        mro => {
+          type => $requested_mro,
+          is_c3 => ( ($requested_mro eq 'c3') ? 1 : 0 ),
+        },
+        cumulative_gen => $my_gen,
+      );
+
+      # ensure the cache is populated for the parents, code below can then
+      # efficiently operate over the query_cache directly
+      describe_class_methods($_) for reverse @full_ISA;
+
+      no strict 'refs';
+
+      # combine full ISA-order inherited and local method list into a
+      # "shadowing stack"
+
+      (
+        unshift @{ $slot->{methods}{$_->{name}} }, $_
+
+          and
+
+        (
+          $_->{via_class} ne $class
+            or
+          $slot->{methods_defined_in_class}{$_->{name}} = $_
+        )
+
+          and
+
+        @{ $slot->{methods}{$_->{name}} } > 1
+
+          and
+
+        $slot->{methods_with_supers}{$_->{name}} = $slot->{methods}{$_->{name}}
+
+      ) for (
+
+        # what describe_class_methods for @full_ISA produced above
+        ( map { values %{
+          $describe_class_query_cache->{$_}{methods_defined_in_class} || {}
+        } } map { "$_|" . mro::get_mro($_) } reverse @full_ISA ),
+
+        # our own non-cleaned subs + their attributes
+        ( map {
+          (
+            # need to account for dummy helper crefs under OLD_MRO
+            (
+              ! DBIx::Class::_ENV_::OLD_MRO
+                or
+              ! ( ( $Class::C3::MRO{$class} || {} )->{methods}{$_} )
+            )
+              and
+            # these 2 OR-ed checks are sufficient for 5.10+
+            (
+              ref(\ "${class}::"->{$_} ) ne 'GLOB'
+                or
+              defined( *{ "${class}::"->{$_} }{CODE} )
+            )
+          ) ? {
+              via_class => $class,
+              name => $_,
+              attributes => {
+                map { $_ => 1 } attributes::get( \&{"${class}::${_}"} )
+              },
+            }
+            : ()
+        } keys %{"${class}::"} )
+      );
+
+
+      # recalculate the pkg_gen on newer perls under Taint mode,
+      # because of shit like:
+      # perl -T -Mmro -e 'package Foo; sub bar {}; defined( *{ "Foo::"->{bar}}{CODE} ) and warn mro::get_pkg_gen("Foo") for (1,2,3)'
+      #
+      if (
+        ! DBIx::Class::_ENV_::OLD_MRO
+          and
+        ${^TAINT}
+      ) {
+
+        $slot->{cumulative_gen} = 0;
+        $slot->{cumulative_gen} += get_real_pkg_gen($_)
+          for $class, @full_ISA;
+      }
+    }
+
+    # RV
+    +{ %$slot };
+  }
+}
+
 
 #
 # Why not just use some higher-level module or at least File::Spec here?
@@ -647,12 +925,29 @@ sub fail_on_internal_call {
     ;
   };
 
+  my @fr2;
+  # need to make allowance for a proxy-yet-direct call
+  my $check_fr = (
+    $fr->[0] eq 'DBIx::Class::ResultSourceProxy'
+      and
+    @fr2 = (CORE::caller(2))
+      and
+    (
+      ( $fr->[3] =~ /([^:])+$/ )[0]
+        eq
+      ( $fr2[3] =~ /([^:])+$/ )[0]
+    )
+  )
+    ? \@fr2
+    : $fr
+  ;
+
   if (
     $argdesc
       and
-    $fr->[0] =~ /^(?:DBIx::Class|DBICx::)/
+    $check_fr->[0] =~ /^(?:DBIx::Class|DBICx::)/
       and
-    $fr->[1] !~ /\b(?:CDBICompat|ResultSetProxy)\b/  # no point touching there
+    $check_fr->[1] !~ /\b(?:CDBICompat|ResultSetProxy)\b/  # no point touching there
   ) {
     DBIx::Class::Exception->throw( sprintf (
       "Illegal internal call of indirect proxy-method %s() with argument '%s': examine the last lines of the proxy method deparse below to determine what to call directly instead at %s on line %d\n\n%s\n\n    Stacktrace starts",