1 # Copyright (c) 2010 Elizabeth Grace Frank-Backman.
 
   3 # Liscenced under the "Artistic Liscence"
 
   4 # (see http://dev.perl.org/licenses/artistic.html)
 
  11 package Exception::Lite;
 
  12 our @ISA = qw(Exporter);
 
  13 our @EXPORT_OK=qw(declareExceptionClass isException isChainable
 
  16   =( common => [qw(declareExceptionClass isException isChainable)]
 
  19 my $CLASS='Exception::Lite';
 
  21 #------------------------------------------------------------------
 
  29 # provide command line control over amount and layout of debugging
 
  30 # information, e.g. perl -mException::Lite=STRINGIFY=4
 
  33   Exception::Lite->export_to_level(1, grep {
 
  37       if ($k eq 'STRINGIFY')        { $STRINGIFY=$v;
 
  38       } elsif ($k eq 'FILTER')      { $FILTER=$v;
 
  39       } elsif ($k eq 'LINE_LENGTH') { $LINE_LENGTH=$v;
 
  40       } elsif ($k eq 'TAB')         { $TAB=$v;
 
  49 #------------------------------------------------------------------
 
  50 # Note to source code divers: DO NOT USE THIS. This is intended for
 
  51 # internal use but must be declared with "our" because we need to
 
  52 # localize it.  This is an implementation detail and cannot be relied
 
  53 # on for future releases.
 
  57 #------------------------------------------------------------------
 
  60 use constant EVAL => '(eval)';
 
  62 #==================================================================
 
  63 # EXPORTABLE FUNCTIONS
 
  64 #==================================================================
 
  66 sub declareExceptionClass {
 
  67   my ($sClass, $sSuperClass, $xFormatRule, $bCustomizeSubclass) = @_;
 
  68   my $sPath = $sClass; $sPath =~ s/::/\//g; $sPath .= '.pm';
 
  70     # we want to start with the caller's frame, not ours
 
  71     local $STACK_OFFSET = $STACK_OFFSET + 1;
 
  72     die 'Exception::Lite::Any'->new("declareExceptionClass failed: "
 
  73                                     . "$sClass is already defined!");
 
  77   my $sRef=ref($sSuperClass);
 
  79     $bCustomizeSubclass = $xFormatRule;
 
  80     $xFormatRule = $sSuperClass;
 
  83     $sRef = ref($xFormatRule);
 
  84     if (!$sRef && defined($xFormatRule)) {
 
  85       $bCustomizeSubclass = $xFormatRule;
 
  90   # set up things dependent on whether or not the class has a
 
  91   # format string or expects a message for each instance
 
  93   my ($sLeadingParams, $sAddOrOmit, $sRethrowMsg, $sMakeMsg);
 
  97     $sLeadingParams='my $e; $e=shift if ref($_[0]);';
 
  98     $sAddOrOmit='added an unnecessary message or format';
 
 101     #generate format rule
 
 102     $xFormatRule=$xFormatRule->($sClass) if ($sRef eq 'CODE');
 
 104     my $sFormat= 'q{' . $xFormatRule->[0] . '}';
 
 105     if (scalar($xFormatRule) == 1) {
 
 106       $sMakeMsg='my $msg='.$sFormat;
 
 108       my $sSprintf = 'Exception::Lite::_sprintf(' . $sFormat
 
 109         . ', map {defined($_)?$_:\''. $UNDEF .'\'} @$h{qw('
 
 110         . join(' ', @$xFormatRule[1..$#$xFormatRule]) . ')});';
 
 111       $sMakeMsg='my $msg='.$sSprintf;
 
 112       $sReplaceMsg='$_[0]->[0]='.$sSprintf;
 
 116     $sLeadingParams = 'my $e=shift; my $msg;'.
 
 117       'if(ref($e)) { $msg=shift; $msg=$e->[0] if !defined($msg);}'.
 
 118       'else { $msg=$e;$e=undef; }';
 
 119     $sAddOrOmit='omitted a required message';
 
 120     $sRethrowMsg='my $msg=shift; $_[0]->[0]=$msg if defined($msg);';
 
 124   # put this in an eval so that it doesn't cause parse errors at
 
 125   # compile time in no-threads versions of Perl
 
 127   my $sTid = eval q{defined(&threads::tid)?'threads->tid':'undef'};
 
 129   my $sDeclare = "package $sClass;".
 
 130     'sub new { my $cl=shift;'.  $sLeadingParams .
 
 131       'my $st=Exception::Lite::_cacheStackTrace($e);'.
 
 132       'my $h= Exception::Lite::_shiftProperties($cl' .
 
 133          ',$st,"'.$sAddOrOmit.'",@_);' . $sMakeMsg .
 
 134       'my $self=bless([$msg,$h,$st,$$,'.$sTid.',$e,[]],$cl);';
 
 136   # the remainder depends on the type of subclassing
 
 138   if ($bCustomizeSubclass) {
 
 139     $sDeclare .= '$self->[7]={}; $self->_new(); return $self; }'
 
 140       . 'sub _p_getSubclassData { $_[0]->[7]; }';
 
 142     $sDeclare .= 'return $self;}'.
 
 143     'sub replaceProperties {'.
 
 144        'my $h={%{$_[0]->[1]},%{$_[1]}}; $_[0]->[1]=$h;'.$sReplaceMsg.
 
 147       'my $self=shift;' . $sRethrowMsg .
 
 148       'Exception::Lite::_rethrow($self,"'.$sAddOrOmit.'",@_)' .
 
 151     unless (isExceptionClass($sSuperClass)) {
 
 153         'sub _getInterface { \'Exception::Lite\' }' .
 
 154         'sub getMessage { $_[0]->[0] };' .
 
 155         'sub getProperty { $_[0]->[1]->{$_[1]} }' .
 
 156         'sub isProperty { exists($_[0]->[1]->{$_[1]})?1:0 }' .
 
 157         'sub getStackTrace { $_[0]->[2] }' .
 
 158         'sub getFrameCount { scalar(@{$_[0]->[2]}); }' .
 
 159         'sub getFile { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[0] };' .
 
 160         'sub getLine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[1] };' .
 
 161         'sub getSubroutine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[2] };' .
 
 162         'sub getArgs { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[3] };' .
 
 163         'sub getPackage {$_[0]->[2]->[-1]->[2] =~ /(\w+)>$/;$1}'.
 
 164         'sub getPid { $_[0]->[3] }' .
 
 165         'sub getTid { $_[0]->[4] }' .
 
 166         'sub getChained { $_[0]->[5] }' .
 
 167         'sub getPropagation { $_[0]->[6]; }' .
 
 169            'q{""} => \&Exception::Lite::_dumpMessage ' .
 
 170            ', q{0+} => \&Exception::Lite::_refaddr, fallback=>1;' .
 
 171         'sub PROPAGATE { push @{$_[0]->[6]},[$_[1],$_[2]]; $_[0]}';
 
 174   $sDeclare .= 'return 1;';
 
 176   local $SIG{__WARN__} = sub {
 
 177     my ($p,$f,$l) = caller(2);
 
 178     my $s=$_[0]; $s =~ s/at \(eval \d+\)\s+line\s+\d+\.//m;
 
 179     print STDERR "$s in declareExceptionClass($sClass,...) "
 
 180       ."in file $f, line $l\n";
 
 183   eval $sDeclare or do {
 
 184     my ($p,$f,$l) = caller(1);
 
 185     print STDERR "Can't create class $sClass at file $f, line $l\n";
 
 186     if ($sClass =~ /\w:\w/) {
 
 187       print STDERR "Bad class name: "
 
 188         ."At least one ':' is not doubled\n";
 
 189     } elsif ($sClass !~ /^\w+(?:::\w+)*$/) {
 
 190       print STDERR "Bad class name: $sClass\n";
 
 192       $sDeclare=~s/(sub |use )/\n$1/g; print STDERR "$sDeclare\n";
 
 196   # this needs to be separate from the eval, otherwise it never
 
 197   # ends up in @INC or @ISA, at least in Perl 5.8.8
 
 198   $INC{$sPath} = __FILE__;
 
 199   eval "\@${sClass}::ISA=qw($sSuperClass);" if $sSuperClass;
 
 204 #------------------------------------------------------------------
 
 206 sub isChainable { return ref($_[0])?1:0; }
 
 208 #------------------------------------------------------------------
 
 211   my ($e, $sClass) = @_;
 
 213   return !defined($sClass)
 
 214     ? ($sRef ? isExceptionClass($sRef) : 0)
 
 216        ? ($sRef eq '' ? 1 : 0)
 
 219             : $sRef->isa($sClass)
 
 223 #------------------------------------------------------------------
 
 225 sub isExceptionClass {
 
 226   return defined($_[0]) && $_[0]->can('_getInterface')
 
 227     && ($_[0]->_getInterface() eq __PACKAGE__) ? 1 : 0;
 
 230 #------------------------------------------------------------------
 
 233   my $iStringify = $_[0];
 
 234   $SIG{__DIE__} = sub {
 
 235     $Exception::Lite::STRINGIFY=$iStringify;
 
 236     warn 'Exception::Lite::Any'->new('Unexpected death:'.$_[0])
 
 237       unless $^S || isException($_[0]);
 
 241 #------------------------------------------------------------------
 
 244   my $iStringify = $_[0];
 
 245   $SIG{__WARN__} = sub {
 
 246     $Exception::Lite::STRINGIFY=$iStringify;
 
 247     print STDERR 'Exception::Lite::Any'->new("Warning: $_[0]");
 
 251 #==================================================================
 
 252 # PRIVATE SUBROUTINES
 
 253 #==================================================================
 
 255 #------------------------------------------------------------------
 
 263   # caller populates @DB::args if called within DB package
 
 265     # this 2 line wierdness is needed to prevent Module::Build from finding
 
 266     # this and adding it to the provides list.
 
 270     #get rid of eval and call to _cacheCall
 
 271     @aCaller = caller($iFrame+2);
 
 273     # mark leading undefined elements as maybe shifted away
 
 281               "'$_'" . (overload::Method($_,'""')
 
 282                         ? ' ('.overload::StrVal($_).')':'')}
 
 283           : 'undef' . (defined($iDefined)
 
 284                        ? '':'  (maybe shifted away?)')
 
 288   return $#aCaller < 0 ? \$aArgs : [ @aCaller[0..3], $aArgs ];
 
 291 #------------------------------------------------------------------
 
 293 sub _cacheStackTrace {
 
 294   my $e=$_[0]; my $st=[];
 
 296   # set up initial frame
 
 297   my $iFrame= $STACK_OFFSET + 1; # call to new
 
 298   my $aCall = _cacheCall($iFrame++);
 
 299   my ($sPackage, $iFile, $iLine, $sSub, $sArgs) = @$aCall;
 
 300   my $iLineFrame=$iFrame;
 
 302   $aCall =  _cacheCall($iFrame++);  #context of call to new
 
 303   while (ref($aCall) ne 'REF') {
 
 304     $sSub  = $aCall->[3];  # subroutine containing file,line
 
 305     $sArgs = $aCall->[4];  # args used to call $sSub
 
 307     #print STDERR "debug-2: package=$sPackage file=$iFile line=$iLine"
 
 308     #  ." sub=$sSub, args=@$sArgs\n";
 
 310     # in evals we want the line number within the eval, but the
 
 311     # name of the sub in which the eval was located. To get this
 
 312     # we wait to push on the stack until we get an actual sub name
 
 313     # and we avoid overwriting the location information, hence 'ne'
 
 315     if (!$FILTER || ($sSub ne EVAL)) {
 
 316       my $aFrame=[ $iFile, $iLine, $sSub, $sArgs ];
 
 317       ($sPackage, $iFile, $iLine) = @$aCall;
 
 320       my $sRef=ref($FILTER);
 
 321       if ($sRef eq 'CODE') {
 
 322         my $x = $FILTER->(@$aFrame, $iFrame, $iLineFrame);
 
 323         if (ref($x) eq 'ARRAY') {
 
 328       } elsif (($sRef eq 'ARRAY') && ! _isIgnored($sSub, $FILTER)) {
 
 330       } elsif (($sRef eq 'Regexp') && !_isIgnored($sSub, [$FILTER])) {
 
 333       push(@$st, $aFrame) if $aFrame;
 
 336     $aCall = _cacheCall($iFrame++);
 
 339   push @$st, [ $iFile, $iLine, "<package: $sPackage>", $$aCall ];
 
 340   if ($e) { my $n=$#{$e->[2]}-$#$st;$e->[2]=[@{$e->[2]}[0..$n]]};
 
 344 #-----------------------------
 
 347   my ($sSub, $aIgnore) = @_;
 
 348   foreach my $re (@$aIgnore) { return 1 if $sSub =~ $re; }
 
 352 #------------------------------------------------------------------
 
 355   my ($e, $iDepth) = @_;
 
 357   my $sMsg = $e->getMessage();
 
 358   return $sMsg unless $STRINGIFY;
 
 359   if (ref($STRINGIFY) eq 'CODE') {
 
 360     return $STRINGIFY->($sMsg);
 
 363   $iDepth = 0 unless defined($iDepth);
 
 364   my $sIndent = ' ' x ($TAB*$iDepth);
 
 365   $sMsg = "\n${sIndent}Exception! $sMsg";
 
 366   return $sMsg if $STRINGIFY == 0;
 
 368   my ($sThrow, $sReach);
 
 369   my $sTab = ' ' x $TAB;
 
 372   if ($STRINGIFY > 2) {
 
 373     my $aPropagation = $e->getPropagation();
 
 374     for (my $i=$#$aPropagation; $i >= 0; $i--) {
 
 375       my ($f,$l) = @{$aPropagation->[$i]};
 
 376       $sMsg .= "\n${sIndent}rethrown at file $f, line $l";
 
 386   my $st=$e->getStackTrace();
 
 387   my $iTop = scalar @$st;
 
 389   for (my $iFrame=0; $iFrame<$iTop; $iFrame++) {
 
 390     my ($f,$l,$s,$aArgs) = @{$st->[$iFrame]};
 
 393       #2nd and following stack frame
 
 394       my $sVia="${sIndent}${sReach}via file $f, line $l";
 
 395       my $sLine="$sVia in $s";
 
 396       $sMsg .= (length($sLine)>$LINE_LENGTH
 
 397                 ? "\n$sVia\n$sIndent${sTab}in $s" : "\n$sLine");
 
 400       my $tid=$e->getTid();
 
 401       my $sAt="${sIndent}${sThrow}at  file $f, line $l";
 
 402       my $sLine="$sAt in $s";
 
 403       $sMsg .= (length($sLine)>$LINE_LENGTH
 
 404                 ? "\n$sAt\n$sIndent${sTab}in $s" : "\n$sLine")
 
 405         . ", pid=" . $e->getPid() . (defined($tid)?", tid=$tid":'');
 
 407       return "$sMsg\n" if $STRINGIFY == 1;
 
 410     if ($STRINGIFY > 3) {
 
 411       my $bTop = ($iFrame+1) == $iTop;
 
 412       my $sVar= ($bTop && !$iDepth) ? '@ARGV' : '@_';
 
 413       my $bMaybeEatenByGetOpt = $bTop && !scalar(@$aArgs)
 
 414         && exists($INC{'Getopt/Long.pm'});
 
 416       my $sVarIndent = "\n${sIndent}" . (' ' x $TAB);
 
 417       my $sArgPrefix = "${sVarIndent}".(' ' x length($sVar)).' ';
 
 418       if ($bMaybeEatenByGetOpt) {
 
 419         $sMsg .= $sArgPrefix . $sVar
 
 420           . '()    # maybe eaten by Getopt::Long?';
 
 422         my $sArgs = join($sArgPrefix.',', @$aArgs);
 
 423         $sMsg .= "${sVarIndent}$sVar=($sArgs";
 
 424         $sMsg .= $sArgs ? "$sArgPrefix)" : ')';
 
 429   return $sMsg if $STRINGIFY == 2;
 
 431   my $eChained = $e->getChained();
 
 432   if (defined($eChained)) {
 
 433     my $sTrigger = isException($eChained)
 
 434       ? _dumpMessage($eChained, $iDepth+1)
 
 435       : "\n${sIndent}$eChained\n";
 
 436     $sMsg .= "\n${sIndent}Triggered by...$sTrigger";
 
 441 #------------------------------------------------------------------
 
 443 # refaddr has a prototype($) so we can't use it directly as an
 
 444 # overload operator: it complains about being passed 3 parameters
 
 446 sub _refaddr { Scalar::Util::refaddr($_[0]) };
 
 448 #------------------------------------------------------------------
 
 451   my $self = shift; my $sAddOrOmit = shift;
 
 452   my ($p,$f,$l)=caller(1);
 
 453   $self->PROPAGATE($f,$l);
 
 456     warn sprintf('bad parameter list to %s->rethrow(...)'
 
 457       .'at file %d, line %d: odd number of elements in property-value '
 
 458       .'list, property value has no property name and will be '
 
 459       ."discarded (common causes: you have %s string)\n"
 
 460       ,$f, $l, $sAddOrOmit);
 
 463   $self->replaceProperties({@_}) if (@_);
 
 467 #------------------------------------------------------------------
 
 468 # Traps warnings and reworks them so that they tell the user how
 
 469 # to fix the problem rather than obscurely complain about an
 
 470 # invisible sprintf with uninitialized values that seem to come from
 
 471 # no where (and make Exception::Lite look like it is broken)
 
 478     local $SIG{__WARN__} = sub { $sWarn=$_[0] if !defined($sWarn) };
 
 480     # sprintf has prototype ($@)
 
 482     $sMsg = sprintf($sFormat, @_);
 
 485   if (defined($sWarn)) {
 
 487     my ($f, $l, $s) = (caller(1))[1,2,3];
 
 488     $s =~ s/::(\w+)\z/->$1/;
 
 489     $sWarn =~ s/sprintf/$s/;
 
 490     $sWarn =~ s/\s+at\s+[\w\/\.]+\s+line\s+\d+\.\s+\z//;
 
 492         =~ m{^Use of uninitialized value in|^Missing argument}) {
 
 493       my $p=$s; $p =~ s/->\w+\z//;
 
 494       $sReason ="\n     Most likely cause: "
 
 495         . "Either you are missing property-value pairs needed to"
 
 496         . "build the message or your exception class's format"
 
 497         . "definition mistakenly has too many placeholders "
 
 498         . "(e.g. %s,%d,etc)\n";
 
 500     warn "$sWarn called at file $f, line $l$sReason\n";
 
 505 #------------------------------------------------------------------
 
 507 sub _shiftProperties {
 
 508   my $cl= shift;  my $st=shift;  my $sAddOrOmit = shift;
 
 511     warn sprintf('bad parameter list to %s->new(...) at '
 
 512       .'file %s, line %d: odd number of elements in property-value '
 
 513       .'list, property value has no property name and will be '
 
 514       .'discarded (common causes: you have %s string -or- you are '
 
 515       ."using a string as a chained exception)\n"
 
 516       ,$cl,$st->[0]->[0],$st->[0]->[1], $sAddOrOmit);
 
 522 #==================================================================
 
 523 # MODULE INITIALIZATION
 
 524 #==================================================================
 
 526 declareExceptionClass(__PACKAGE__ .'::Any');