# Copyright (c) 2010 Elizabeth Grace Frank-Backman. # All rights reserved. # Liscenced under the "Artistic Liscence" # (see http://dev.perl.org/licenses/artistic.html) use 5.8.8; use strict; use warnings; use overload; package Exception::Lite; our @ISA = qw(Exporter); our @EXPORT_OK=qw(declareExceptionClass isException isChainable onDie onWarn); our %EXPORT_TAGS =( common => [qw(declareExceptionClass isException isChainable)] , all => [@EXPORT_OK] ); my $CLASS='Exception::Lite'; #------------------------------------------------------------------ our $STRINGIFY=3; our $FILTER=1; our $UNDEF=''; our $TAB=3; our $LINE_LENGTH=120; # provide command line control over amount and layout of debugging # information, e.g. perl -mException::Lite=STRINGIFY=4 sub import { Exception::Lite->export_to_level(1, grep { if (/^(\w+)=(.*)$/) { my $k = $1; my $v = $2; if ($k eq 'STRINGIFY') { $STRINGIFY=$v; } elsif ($k eq 'FILTER') { $FILTER=$v; } elsif ($k eq 'LINE_LENGTH') { $LINE_LENGTH=$v; } elsif ($k eq 'TAB') { $TAB=$v; } 0; } else { 1; } } @_); } #------------------------------------------------------------------ # Note to source code divers: DO NOT USE THIS. This is intended for # internal use but must be declared with "our" because we need to # localize it. This is an implementation detail and cannot be relied # on for future releases. our $STACK_OFFSET=0; #------------------------------------------------------------------ use Scalar::Util (); use constant EVAL => '(eval)'; #================================================================== # EXPORTABLE FUNCTIONS #================================================================== sub declareExceptionClass { my ($sClass, $sSuperClass, $xFormatRule, $bCustomizeSubclass) = @_; my $sPath = $sClass; $sPath =~ s/::/\//g; $sPath .= '.pm'; if ($INC{$sPath}) { # we want to start with the caller's frame, not ours local $STACK_OFFSET = $STACK_OFFSET + 1; die 'Exception::Lite::Any'->new("declareExceptionClass failed: " . "$sClass is already defined!"); return undef; } my $sRef=ref($sSuperClass); if ($sRef) { $bCustomizeSubclass = $xFormatRule; $xFormatRule = $sSuperClass; $sSuperClass=undef; } else { $sRef = ref($xFormatRule); if (!$sRef && defined($xFormatRule)) { $bCustomizeSubclass = $xFormatRule; $xFormatRule = undef; } } # set up things dependent on whether or not the class has a # format string or expects a message for each instance my ($sLeadingParams, $sAddOrOmit, $sRethrowMsg, $sMakeMsg); my $sReplaceMsg=''; if ($sRef) { $sLeadingParams='my $e; $e=shift if ref($_[0]);'; $sAddOrOmit='added an unnecessary message or format'; $sRethrowMsg=''; #generate format rule $xFormatRule=$xFormatRule->($sClass) if ($sRef eq 'CODE'); my $sFormat= 'q{' . $xFormatRule->[0] . '}'; if (scalar($xFormatRule) == 1) { $sMakeMsg='my $msg='.$sFormat; } else { my $sSprintf = 'Exception::Lite::_sprintf(' . $sFormat . ', map {defined($_)?$_:\''. $UNDEF .'\'} @$h{qw(' . join(' ', @$xFormatRule[1..$#$xFormatRule]) . ')});'; $sMakeMsg='my $msg='.$sSprintf; $sReplaceMsg='$_[0]->[0]='.$sSprintf; } } else { $sLeadingParams = 'my $e=shift; my $msg;'. 'if(ref($e)) { $msg=shift; $msg=$e->[0] if !defined($msg);}'. 'else { $msg=$e;$e=undef; }'; $sAddOrOmit='omitted a required message'; $sRethrowMsg='my $msg=shift; $_[0]->[0]=$msg if defined($msg);'; $sMakeMsg=''; } # put this in an eval so that it doesn't cause parse errors at # compile time in no-threads versions of Perl my $sTid = eval q{defined(&threads::tid)?'threads->tid':'undef'}; my $sDeclare = "package $sClass;". 'sub new { my $cl=shift;'. $sLeadingParams . 'my $st=Exception::Lite::_cacheStackTrace($e);'. 'my $h= Exception::Lite::_shiftProperties($cl' . ',$st,"'.$sAddOrOmit.'",@_);' . $sMakeMsg . 'my $self=bless([$msg,$h,$st,$$,'.$sTid.',$e,[]],$cl);'; # the remainder depends on the type of subclassing if ($bCustomizeSubclass) { $sDeclare .= '$self->[7]={}; $self->_new(); return $self; }' . 'sub _p_getSubclassData { $_[0]->[7]; }'; } else { $sDeclare .= 'return $self;}'. 'sub replaceProperties {'. 'my $h={%{$_[0]->[1]},%{$_[1]}}; $_[0]->[1]=$h;'.$sReplaceMsg. '}'. 'sub rethrow {' . 'my $self=shift;' . $sRethrowMsg . 'Exception::Lite::_rethrow($self,"'.$sAddOrOmit.'",@_)' . '}'; unless (isExceptionClass($sSuperClass)) { $sDeclare .= 'sub _getInterface { \'Exception::Lite\' }' . 'sub getMessage { $_[0]->[0] };' . 'sub getProperty { $_[0]->[1]->{$_[1]} }' . 'sub isProperty { exists($_[0]->[1]->{$_[1]})?1:0 }' . 'sub getStackTrace { $_[0]->[2] }' . 'sub getFrameCount { scalar(@{$_[0]->[2]}); }' . 'sub getFile { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[0] };' . 'sub getLine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[1] };' . 'sub getSubroutine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[2] };' . 'sub getArgs { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[3] };' . 'sub getPackage {$_[0]->[2]->[-1]->[2] =~ /(\w+)>$/;$1}'. 'sub getPid { $_[0]->[3] }' . 'sub getTid { $_[0]->[4] }' . 'sub getChained { $_[0]->[5] }' . 'sub getPropagation { $_[0]->[6]; }' . 'use overload '. 'q{""} => \&Exception::Lite::_dumpMessage ' . ', q{0+} => \&Exception::Lite::_refaddr, fallback=>1;' . 'sub PROPAGATE { push @{$_[0]->[6]},[$_[1],$_[2]]; $_[0]}'; } } $sDeclare .= 'return 1;'; local $SIG{__WARN__} = sub { my ($p,$f,$l) = caller(2); my $s=$_[0]; $s =~ s/at \(eval \d+\)\s+line\s+\d+\.//m; print STDERR "$s in declareExceptionClass($sClass,...) " ."in file $f, line $l\n"; }; eval $sDeclare or do { my ($p,$f,$l) = caller(1); print STDERR "Can't create class $sClass at file $f, line $l\n"; if ($sClass =~ /\w:\w/) { print STDERR "Bad class name: " ."At least one ':' is not doubled\n"; } elsif ($sClass !~ /^\w+(?:::\w+)*$/) { print STDERR "Bad class name: $sClass\n"; } else { $sDeclare=~s/(sub |use )/\n$1/g; print STDERR "$sDeclare\n"; } }; # this needs to be separate from the eval, otherwise it never # ends up in @INC or @ISA, at least in Perl 5.8.8 $INC{$sPath} = __FILE__; eval "\@${sClass}::ISA=qw($sSuperClass);" if $sSuperClass; return $sClass; } #------------------------------------------------------------------ sub isChainable { return ref($_[0])?1:0; } #------------------------------------------------------------------ sub isException { my ($e, $sClass) = @_; my $sRef=ref($e); return !defined($sClass) ? ($sRef ? isExceptionClass($sRef) : 0) : $sClass eq '' ? ($sRef eq '' ? 1 : 0) : ($sRef eq '') ? 0 : $sRef->isa($sClass) ?1:0; } #------------------------------------------------------------------ sub isExceptionClass { return defined($_[0]) && $_[0]->can('_getInterface') && ($_[0]->_getInterface() eq __PACKAGE__) ? 1 : 0; } #------------------------------------------------------------------ sub onDie { my $iStringify = $_[0]; $SIG{__DIE__} = sub { $Exception::Lite::STRINGIFY=$iStringify; warn 'Exception::Lite::Any'->new('Unexpected death:'.$_[0]) unless $^S || isException($_[0]); }; } #------------------------------------------------------------------ sub onWarn { my $iStringify = $_[0]; $SIG{__WARN__} = sub { $Exception::Lite::STRINGIFY=$iStringify; print STDERR 'Exception::Lite::Any'->new("Warning: $_[0]"); }; } #================================================================== # PRIVATE SUBROUTINES #================================================================== #------------------------------------------------------------------ sub _cacheCall { my $iFrame = $_[0]; my @aCaller; my $aArgs; # caller populates @DB::args if called within DB package eval { # this 2 line wierdness is needed to prevent Module::Build from finding # this and adding it to the provides list. package DB; #get rid of eval and call to _cacheCall @aCaller = caller($iFrame+2); # mark leading undefined elements as maybe shifted away my $iDefined; if ($#aCaller < 0) { @DB::args=@ARGV; } $aArgs = [ map { defined($_) ? do {$iDefined=1; "'$_'" . (overload::Method($_,'""') ? ' ('.overload::StrVal($_).')':'')} : 'undef' . (defined($iDefined) ? '':' (maybe shifted away?)') } @DB::args]; }; return $#aCaller < 0 ? \$aArgs : [ @aCaller[0..3], $aArgs ]; } #------------------------------------------------------------------ sub _cacheStackTrace { my $e=$_[0]; my $st=[]; # set up initial frame my $iFrame= $STACK_OFFSET + 1; # call to new my $aCall = _cacheCall($iFrame++); my ($sPackage, $iFile, $iLine, $sSub, $sArgs) = @$aCall; my $iLineFrame=$iFrame; $aCall = _cacheCall($iFrame++); #context of call to new while (ref($aCall) ne 'REF') { $sSub = $aCall->[3]; # subroutine containing file,line $sArgs = $aCall->[4]; # args used to call $sSub #print STDERR "debug-2: package=$sPackage file=$iFile line=$iLine" # ." sub=$sSub, args=@$sArgs\n"; # in evals we want the line number within the eval, but the # name of the sub in which the eval was located. To get this # we wait to push on the stack until we get an actual sub name # and we avoid overwriting the location information, hence 'ne' if (!$FILTER || ($sSub ne EVAL)) { my $aFrame=[ $iFile, $iLine, $sSub, $sArgs ]; ($sPackage, $iFile, $iLine) = @$aCall; $iLineFrame=$iFrame; my $sRef=ref($FILTER); if ($sRef eq 'CODE') { my $x = $FILTER->(@$aFrame, $iFrame, $iLineFrame); if (ref($x) eq 'ARRAY') { $aFrame=$x; } elsif (!$x) { $aFrame=undef; } } elsif (($sRef eq 'ARRAY') && ! _isIgnored($sSub, $FILTER)) { $aFrame=undef; } elsif (($sRef eq 'Regexp') && !_isIgnored($sSub, [$FILTER])) { $aFrame=undef; } push(@$st, $aFrame) if $aFrame; } $aCall = _cacheCall($iFrame++); } push @$st, [ $iFile, $iLine, "", $$aCall ]; if ($e) { my $n=$#{$e->[2]}-$#$st;$e->[2]=[@{$e->[2]}[0..$n]]}; return $st; } #----------------------------- sub _isIgnored { my ($sSub, $aIgnore) = @_; foreach my $re (@$aIgnore) { return 1 if $sSub =~ $re; } return 0; } #------------------------------------------------------------------ sub _dumpMessage { my ($e, $iDepth) = @_; my $sMsg = $e->getMessage(); return $sMsg unless $STRINGIFY; if (ref($STRINGIFY) eq 'CODE') { return $STRINGIFY->($sMsg); } $iDepth = 0 unless defined($iDepth); my $sIndent = ' ' x ($TAB*$iDepth); $sMsg = "\n${sIndent}Exception! $sMsg"; return $sMsg if $STRINGIFY == 0; my ($sThrow, $sReach); my $sTab = ' ' x $TAB; $sIndent.= $sTab; if ($STRINGIFY > 2) { my $aPropagation = $e->getPropagation(); for (my $i=$#$aPropagation; $i >= 0; $i--) { my ($f,$l) = @{$aPropagation->[$i]}; $sMsg .= "\n${sIndent}rethrown at file $f, line $l"; } $sMsg .= "\n"; $sThrow='thrown '; $sReach='reached '; } else { $sThrow=''; $sReach=''; } my $st=$e->getStackTrace(); my $iTop = scalar @$st; for (my $iFrame=0; $iFrame<$iTop; $iFrame++) { my ($f,$l,$s,$aArgs) = @{$st->[$iFrame]}; if ($iFrame) { #2nd and following stack frame my $sVia="${sIndent}${sReach}via file $f, line $l"; my $sLine="$sVia in $s"; $sMsg .= (length($sLine)>$LINE_LENGTH ? "\n$sVia\n$sIndent${sTab}in $s" : "\n$sLine"); } else { # first stack frame my $tid=$e->getTid(); my $sAt="${sIndent}${sThrow}at file $f, line $l"; my $sLine="$sAt in $s"; $sMsg .= (length($sLine)>$LINE_LENGTH ? "\n$sAt\n$sIndent${sTab}in $s" : "\n$sLine") . ", pid=" . $e->getPid() . (defined($tid)?", tid=$tid":''); return "$sMsg\n" if $STRINGIFY == 1; } if ($STRINGIFY > 3) { my $bTop = ($iFrame+1) == $iTop; my $sVar= ($bTop && !$iDepth) ? '@ARGV' : '@_'; my $bMaybeEatenByGetOpt = $bTop && !scalar(@$aArgs) && exists($INC{'Getopt/Long.pm'}); my $sVarIndent = "\n${sIndent}" . (' ' x $TAB); my $sArgPrefix = "${sVarIndent}".(' ' x length($sVar)).' '; if ($bMaybeEatenByGetOpt) { $sMsg .= $sArgPrefix . $sVar . '() # maybe eaten by Getopt::Long?'; } else { my $sArgs = join($sArgPrefix.',', @$aArgs); $sMsg .= "${sVarIndent}$sVar=($sArgs"; $sMsg .= $sArgs ? "$sArgPrefix)" : ')'; } } } $sMsg.="\n"; return $sMsg if $STRINGIFY == 2; my $eChained = $e->getChained(); if (defined($eChained)) { my $sTrigger = isException($eChained) ? _dumpMessage($eChained, $iDepth+1) : "\n${sIndent}$eChained\n"; $sMsg .= "\n${sIndent}Triggered by...$sTrigger"; } return $sMsg; } #------------------------------------------------------------------ # refaddr has a prototype($) so we can't use it directly as an # overload operator: it complains about being passed 3 parameters # instead of 1. sub _refaddr { Scalar::Util::refaddr($_[0]) }; #------------------------------------------------------------------ sub _rethrow { my $self = shift; my $sAddOrOmit = shift; my ($p,$f,$l)=caller(1); $self->PROPAGATE($f,$l); if (@_%2) { warn sprintf('bad parameter list to %s->rethrow(...)' .'at file %d, line %d: odd number of elements in property-value ' .'list, property value has no property name and will be ' ."discarded (common causes: you have %s string)\n" ,$f, $l, $sAddOrOmit); shift @_; } $self->replaceProperties({@_}) if (@_); return $self; } #------------------------------------------------------------------ # Traps warnings and reworks them so that they tell the user how # to fix the problem rather than obscurely complain about an # invisible sprintf with uninitialized values that seem to come from # no where (and make Exception::Lite look like it is broken) sub _sprintf { my $sMsg; my $sWarn; { local $SIG{__WARN__} = sub { $sWarn=$_[0] if !defined($sWarn) }; # sprintf has prototype ($@) my $sFormat = shift; $sMsg = sprintf($sFormat, @_); } if (defined($sWarn)) { my $sReason=''; my ($f, $l, $s) = (caller(1))[1,2,3]; $s =~ s/::(\w+)\z/->$1/; $sWarn =~ s/sprintf/$s/; $sWarn =~ s/\s+at\s+[\w\/\.]+\s+line\s+\d+\.\s+\z//; if ($sWarn =~ m{^Use of uninitialized value in|^Missing argument}) { my $p=$s; $p =~ s/->\w+\z//; $sReason ="\n Most likely cause: " . "Either you are missing property-value pairs needed to" . "build the message or your exception class's format" . "definition mistakenly has too many placeholders " . "(e.g. %s,%d,etc)\n"; } warn "$sWarn called at file $f, line $l$sReason\n"; } return $sMsg; } #------------------------------------------------------------------ sub _shiftProperties { my $cl= shift; my $st=shift; my $sAddOrOmit = shift; if (@_%2) { $"='|'; warn sprintf('bad parameter list to %s->new(...) at ' .'file %s, line %d: odd number of elements in property-value ' .'list, property value has no property name and will be ' .'discarded (common causes: you have %s string -or- you are ' ."using a string as a chained exception)\n" ,$cl,$st->[0]->[0],$st->[0]->[1], $sAddOrOmit); shift @_; } return {@_}; } #================================================================== # MODULE INITIALIZATION #================================================================== declareExceptionClass(__PACKAGE__ .'::Any'); 1;