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');