Carp now handles objects with string overloads.
authorDarin McBride <dmcbride@cpan.org>
Fri, 9 Aug 2013 06:17:08 +0000 (16:17 +1000)
committerTony Cook <tony@develop-help.com>
Fri, 9 Aug 2013 06:34:34 +0000 (16:34 +1000)
It also allows objects to specify how they appear in the stack dump with
a CARP_TRACE method, and also allows the user to specify their own
formatter for objects without CARP_TRACE as well as other references.
[perl #92446]

Minor fix, commit message reformatting and manifest update by Tony Cook.

MANIFEST
dist/Carp/lib/Carp.pm
dist/Carp/lib/Carp/Heavy.pm
dist/Carp/t/Carp_overload.t [new file with mode: 0644]

index b7da05e..68925b7 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -2905,6 +2905,7 @@ dist/bignum/t/scope_r.t                   See if no bigrat works
 dist/Carp/lib/Carp/Heavy.pm    Error message retired workhorse
 dist/Carp/lib/Carp.pm          Error message extension
 dist/Carp/Makefile.PL          makefile writer for Carp
+dist/Carp/t/Carp_overload.t            See if Carp handles overloads
 dist/Carp/t/Carp.t             See if Carp works
 dist/Carp/t/heavy.t            See if Carp::Heavy works
 dist/Carp/t/stash_deletion.t           See if Carp handles stash deletion
index bb557dd..a9b8f29 100644 (file)
@@ -24,13 +24,14 @@ BEGIN {
     }
 }
 
-our $VERSION = '1.30';
+our $VERSION = '1.31';
 
 our $MaxEvalLen = 0;
 our $Verbose    = 0;
 our $CarpLevel  = 0;
 our $MaxArgLen  = 64;    # How much of each argument to print. 0 = all.
 our $MaxArgNums = 8;     # How many arguments to print. 0 = all.
+our $RefArgFormatter = undef; # allow caller to format reference arguments
 
 require Exporter;
 our @ISA       = ('Exporter');
@@ -185,10 +186,44 @@ sub caller_info {
 }
 
 # Transform an argument to a function into a string.
+our $no_recurse;
 sub format_arg {
     my $arg = shift;
+    die "recursion\n" if $no_recurse;
+
     if ( ref($arg) ) {
-        $arg = defined($overload::VERSION) ? overload::StrVal($arg) : "$arg";
+        local $SIG{__DIE__} = sub{}; # legitimate, let's not leak it.
+        if (do {
+                local $@; 
+                eval {$arg->can('CARP_TRACE') }
+            })
+        {
+            $arg = $arg->CARP_TRACE();
+        }
+        elsif (do {
+                local $@;
+                eval {$arg = $RefArgFormatter->($arg); 1}
+                })
+        {
+            1;
+        }
+        elsif (defined($overload::VERSION))
+        {
+            do {
+                local $@;
+                eval {
+                    local $no_recurse = 1;
+                    $arg = "$arg";
+                    1;
+                }
+            } or do {
+                $arg = overload::StrVal($arg);
+            };
+        }
+        else
+        {
+            $arg = "$arg";
+        }
     }
     if ( defined($arg) ) {
         $arg =~ s/'/\\'/g;
@@ -565,6 +600,50 @@ environment variable.
 Alternately, you can set the global variable C<$Carp::Verbose> to true.
 See the C<GLOBAL VARIABLES> section below.
 
+=head2 Stack Trace formatting
+
+At each stack level, the subroutine's name is displayed along with
+its parameters.  For simple scalars, this is sufficient.  For complex
+data types, such as objects and other references, this can simply
+display C<'HASH(0x1ab36d8)'>.
+
+Carp gives three ways to control this.
+
+=over 4
+
+=item 1.
+
+For objects, a method, C<CARP_TRACE>, will be called, if it exists.  If
+this method doesn't exist, or it recurses into C<Carp>, or it otherwise
+throws an exception, this is skipped, and Carp moves on to the next option,
+otherwise checking stops and the string returned is used.  It is recommended
+that the object's type is part of the string to make debugging easier.
+
+=item 2.
+
+For any type of reference, C<$Carp::RefArgFormatter> is checked (see below).
+This variable is expected to be a code reference, and the current parameter
+is passed in.  If this function doesn't exist (the variable is undef), or
+it recurses into C<Carp>, or it otherwise throws an exception, this is
+skipped, and Carp moves on to the next option, otherwise checking stops 
+and the string returned is used.
+
+=item 3.
+
+The reference is stringified.  If overloading is being used on the object,
+that overloading is called.  If that overload recurses into C<Carp>, or it
+otherwise throws an exception, this is skipped, and Carp moves on to the next
+option, otherwise checking stops and the string returned is used.
+
+=item 4.
+
+To get this far, L<overload> must be loaded because the object failed
+to stringify normally.  L<overload>::StrVal is called to stringify the
+object without any overloading to produce a value where all of the above
+has failed.
+
+=back
+
 =head1 GLOBAL VARIABLES
 
 =head2 $Carp::MaxEvalLen
@@ -597,6 +676,17 @@ is implemented internally.
 
 Defaults to C<0>.
 
+=head2 $Carp::RefArgFormatter
+
+This variable sets a general argument formatter to display references.
+Plain scalars and objects that implement C<CARP_TRACE> will not go through
+this formatter.  Calling C<Carp> from within this function is not supported.
+
+local $Carp::RefArgFormatter = sub {
+    require Data::Dumper;
+    Data::Dumper::Dump($_[0]); # not necessarily safe
+};
+
 =head2 @CARP_NOT
 
 This variable, I<in your package>, says which packages are I<not> to be
index db8b453..a7a3327 100644 (file)
@@ -2,7 +2,7 @@ package Carp::Heavy;
 
 use Carp ();
 
-our $VERSION = '1.30';
+our $VERSION = '1.31';
 
 1;
 
diff --git a/dist/Carp/t/Carp_overload.t b/dist/Carp/t/Carp_overload.t
new file mode 100644 (file)
index 0000000..98749ed
--- /dev/null
@@ -0,0 +1,84 @@
+use warnings;
+no warnings 'once';
+use Test::More 0.98 tests => 9;
+
+use Carp;
+
+my $o = Stringable->new(key => 'Baz');
+
+my $msg = call(\&with_longmess, $o, {bar => 'buzz'});
+like($msg, qr/'Stringable=Baz'/, "Stringable object stringified");
+like($msg, qr/'HASH\(0x[[:xdigit:]]+\)'/, "HASH *not* stringified");
+
+{
+    my $called;
+
+    local $Carp::RefArgFormatter = sub {
+        $called++;
+        join '', explain $_[0];
+    };
+
+    $msg = call(\&with_longmess, $o, {bar => 'buzz'});
+    ok($called, "Called private formatter");
+    like($msg, qr/bar.*buzz/m, 'HASH stringified');
+}
+
+$o = CarpTracable->new(key => 'Bax');
+$msg = call(\&with_longmess, $o, {bar => 'buzz'});
+ok($o->{called}, "CARP_TRACE called");
+like($msg, qr/'TRACE:CarpTracable=Bax'/, "CARP_TRACE output used") or diag explain $msg;
+like($msg, qr/'HASH\(0x[[:xdigit:]]+\)'/, "HASH not stringified again");
+
+$o = CarpBad->new(key => 'Zoo');
+$msg = call(\&with_longmess, $o, {bar => 'kill'});
+unlike($msg, qr/THIS SHOULD NEVER HAPPEN|Zoo/, "Didn't get the as-string version");
+like($msg, qr/CarpBad=HASH/,"Normal non-overload string conversion");
+diag explain $msg;
+
+sub call
+{
+    my $func = shift;
+    $func->(@_);
+}
+
+sub with_longmess
+{
+    my $g = shift;
+    Carp::longmess("longmess:\n");
+}
+
+package Stringable;
+
+use overload
+    q[""] => 'as_string';
+
+sub new { my $class = shift; return bless {@_}, $class }
+
+sub as_string
+{
+    my $self = shift;
+    join '=', ref $self, $self->{key} || '<no key>';
+}
+
+package CarpTracable;
+
+use parent -norequire => 'Stringable';
+
+sub CARP_TRACE
+{
+    my $self = shift;
+    $self->{called}++;
+    "TRACE:" . $self; # use string overload
+}
+
+package CarpBad;
+
+use parent -norequire => 'Stringable';
+
+sub as_string
+{
+    Carp::cluck("woops, this isn't allowed");
+    "THIS SHOULD NEVER HAPPEN";
+}
+
+