File Coverage

blib/lib/Mojo/Exception.pm
Criterion Covered Total %
statement 103 103 100.0
branch 46 50 92.0
condition 24 29 82.7
subroutine 18 18 100.0
pod 7 7 100.0
total 198 207 95.6


line stmt bran cond sub pod time code
1             package Mojo::Exception;
2 70     70   128401 use Mojo::Base -base;
  70         145  
  70         583  
3 70     70   2463 use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1;
  70     117   7911  
  70     227   1002  
  68         717  
  278         9164  
4              
5 70     70   8175 use Carp qw(croak);
  70         185  
  70         4671  
6 70     70   454 use Exporter qw(import);
  70         203  
  70         3388  
7 70     70   2584 use Mojo::Util qw(decode);
  70         180  
  70         5591  
8 70     70   729 use Scalar::Util qw(blessed);
  70         169  
  70         186550  
9              
10             has [qw(frames line lines_after lines_before)] => sub { [] };
11             has message => 'Exception!';
12             has verbose => sub { $ENV{MOJO_EXCEPTION_VERBOSE} };
13              
14             our @EXPORT_OK = qw(check raise);
15              
16             sub check {
17 15     15 1 12610 my ($err, $spec) = @_;
18 15 100       96 return undef unless $err;
19              
20 13 100 100     419 croak "Array reference of pattern/handler pairs required to dispatch exceptions"
21             if ref $spec ne 'ARRAY' || @$spec % 2;
22              
23 11         15 my ($default, $handler);
24 11         26 my ($is_obj, $str) = (!!blessed($err), "$err");
25 11         55 CHECK: for (my $i = 0; $i < @$spec; $i += 2) {
26 20         30 my ($checks, $cb) = @{$spec}[$i, $i + 1];
  20         38  
27              
28 20 100 50     59 ($default = $cb) and next if $checks eq 'default';
29              
30 16 100       39 for my $c (ref $checks eq 'ARRAY' ? @$checks : $checks) {
31 18         26 my $is_re = !!ref $c;
32 18 100 50     118 ($handler = $cb) and last CHECK if $is_obj && !$is_re && $err->isa($c);
      100        
      100        
33 13 100 50     85 ($handler = $cb) and last CHECK if $is_re && $str =~ $c;
      100        
34             }
35             }
36              
37             # Rethrow if no handler could be found
38 11 100 100     45 die $err unless $handler ||= $default;
39 10         30 $handler->($_) for $err;
40              
41 10         53 return 1;
42             }
43              
44             sub inspect {
45 133     133 1 1362 my ($self, @sources) = @_;
46              
47 133 100       257 return $self if @{$self->line};
  133         547  
48              
49             # Extract file and line from message
50 118         283 my @files;
51 118         497 my $msg = $self->message;
52 118         4170 unshift @files, [$1, $2] while $msg =~ /at\s+(.+?)\s+line\s+(\d+)/g;
53              
54             # Extract file and line from stack trace
55 118 100       439 if (my $zero = $self->frames->[0]) { push @files, [$zero->[1], $zero->[2]] }
  86         388  
56              
57             # Search for context in files
58 118         337 for my $file (@files) {
59 141 100 66     8681 next unless -r $file->[0] && open my $handle, '<', $file->[0];
60 85         47032 $self->_context($file->[1], [[<$handle>]]);
61 85         9041 return $self;
62             }
63              
64             # Search for context in sources
65 33 100       119 $self->_context($files[-1][1], [map { [split /\n/] } @sources]) if @sources;
  48         326  
66              
67 33         223 return $self;
68             }
69              
70 186 100   186 1 89987 sub new { defined $_[1] ? shift->SUPER::new(message => shift) : shift->SUPER::new }
71              
72             sub raise {
73 5 100   5 1 3862 my ($class, $err) = @_ > 1 ? (@_) : (__PACKAGE__, shift);
74              
75 5 50   1   71 if (!$class->can('new')) { die $@ unless eval "package $class; use Mojo::Base 'Mojo::Exception'; 1" }
  1 100       141  
  1 100       9  
  1         2  
  1         7  
76 1         9 elsif (!$class->isa(__PACKAGE__)) { die "$class is not a Mojo::Exception subclass" }
77              
78 4         19 CORE::die $class->new($err)->trace;
79             }
80              
81             sub to_string {
82 278     278 1 575 my $self = shift;
83              
84 278         798 my $str = $self->message;
85              
86 278         744 my $frames = $self->frames;
87 278 100       1466 if ($str !~ /\n$/) {
88 50 100       197 $str .= @$frames ? " at $frames->[0][1] line $frames->[0][2].\n" : "\n";
89             }
90 278 100       783 return $str unless $self->verbose;
91              
92 67         214 my $line = $self->line;
93 67 100       205 if (@$line) {
94 66         200 $str .= "Context:\n";
95 66         104 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_before};
  66         183  
96 66         213 $str .= " $line->[0]: $line->[1]\n";
97 66         123 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_after};
  66         183  
98             }
99              
100 67 50       196 if (my $max = @$frames) {
101 67         130 $str .= "Traceback (most recent call first):\n";
102 67         6246 $str .= qq{ File "$_->[1]", line $_->[2], in "$_->[3]"\n} for @$frames;
103             }
104              
105 67         944 return $str;
106             }
107              
108 3     3 1 9384 sub throw { CORE::die shift->new(shift)->trace }
109              
110             sub trace {
111 152   100 152 1 1004 my ($self, $start) = (shift, shift // 1);
112 152         283 my @frames;
113 152         2300 while (my @trace = caller($start++)) { push @frames, \@trace }
  5826         52116  
114 152         918 return $self->frames(\@frames);
115             }
116              
117             sub _append {
118 975     975   1907 my ($stack, $line) = @_;
119 975   66     2167 $line = decode('UTF-8', $line) // $line;
120 975         2857 chomp $line;
121 975         2789 push @$stack, $line;
122             }
123              
124             sub _context {
125 109     109   506 my ($self, $num, $sources) = @_;
126              
127             # Line
128 109 50       593 return unless defined $sources->[0][$num - 1];
129 109         997 $self->line([$num]);
130 109         524 _append($self->line, $_->[$num - 1]) for @$sources;
131              
132             # Before
133 109         368 for my $i (2 .. 6) {
134 451 100       1158 last if ((my $previous = $num - $i) < 0);
135 411         562 unshift @{$self->lines_before}, [$previous + 1];
  411         981  
136 411         1153 _append($self->lines_before->[0], $_->[$previous]) for @$sources;
137             }
138              
139             # After
140 109         319 for my $i (0 .. 4) {
141 545 50       3490 next if ((my $next = $num + $i) < 0);
142 545 100       1230 next unless defined $sources->[0][$next];
143 398         592 push @{$self->lines_after}, [$next + 1];
  398         977  
144 398         1024 _append($self->lines_after->[-1], $_->[$next]) for @$sources;
145             }
146             }
147              
148             1;
149              
150             =encoding utf8
151              
152             =head1 NAME
153              
154             Mojo::Exception - Exception base class
155              
156             =head1 SYNOPSIS
157              
158             # Create exception classes
159             package MyApp::X::Foo {
160             use Mojo::Base 'Mojo::Exception';
161             }
162             package MyApp::X::Bar {
163             use Mojo::Base 'Mojo::Exception';
164             }
165              
166             # Throw exceptions and handle them gracefully
167             use Mojo::Exception qw(check);
168             eval {
169             MyApp::X::Foo->throw('Something went wrong!');
170             };
171             check $@ => [
172             'MyApp::X::Foo' => sub { say "Foo: $_" },
173             'MyApp::X::Bar' => sub { say "Bar: $_" }
174             ];
175              
176             # Generate exception classes on demand
177             use Mojo::Exception qw(check raise);
178             eval {
179             raise 'MyApp::X::Name', 'The name Minion is already taken';
180             };
181             check $@ => [
182             'MyApp::X::Name' => sub { say "Name error: $_" },
183             default => sub { say "Error: $_" }
184             ];
185              
186             =head1 DESCRIPTION
187              
188             L is a container for exceptions with context information.
189              
190             =head1 FUNCTIONS
191              
192             L implements the following functions, which can be imported individually.
193              
194             =head2 check
195              
196             my $bool = check $err => ['MyApp::X::Foo' => sub {...}];
197              
198             Process exceptions by dispatching them to handlers with one or more matching conditions. Exceptions that could not be
199             handled will be rethrown automatically.
200              
201             # Handle various types of exceptions
202             eval {
203             dangerous_code();
204             };
205             check $@ => [
206             'MyApp::X::Foo' => sub { say "Foo: $_" },
207             qr/^Could not open/ => sub { say "Open error: $_" },
208             default => sub { say "Something went wrong: $_" }
209             ];
210              
211             Matching conditions can be class names for ISA checks on exception objects, or regular expressions to match string
212             exceptions and stringified exception objects. The matching exception will be the first argument passed to the callback,
213             and is also available as C<$_>.
214              
215             # Catch MyApp::X::Foo object or a specific string exception
216             eval {
217             dangerous_code();
218             };
219             check $@ => [
220             'MyApp::X::Foo' => sub { say "Foo: $_" },
221             qr/^Could not open/ => sub { say "Open error: $_" }
222             ];
223              
224             An array reference can be used to share the same handler with multiple conditions, of which only one needs to match.
225             And since exception handlers are just callbacks, they can also throw their own exceptions.
226              
227             # Handle MyApp::X::Foo and MyApp::X::Bar the same
228             eval {
229             dangerous_code();
230             };
231             check $@ => [
232             ['MyApp::X::Foo', 'MyApp::X::Bar'] => sub { die "Foo/Bar: $_" }
233             ];
234              
235             There is currently only one keywords you can use to set special handlers. The C handler is used when no other
236             handler matched.
237              
238             # Use "default" to catch everything
239             eval {
240             dangerous_code();
241             };
242             check $@ => [
243             default => sub { say "Error: $_" }
244             ];
245              
246             =head2 raise
247              
248             raise 'Something went wrong!';
249             raise 'MyApp::X::Foo', 'Something went wrong!';
250              
251             Raise a L, if the class does not exist yet (classes are checked for a C method), one is created
252             as a L subclass on demand.
253              
254             =head1 ATTRIBUTES
255              
256             L implements the following attributes.
257              
258             =head2 frames
259              
260             my $frames = $e->frames;
261             $e = $e->frames([$frame1, $frame2]);
262              
263             Stack trace if available.
264              
265             # Extract information from the last frame
266             my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext,
267             $is_require, $hints, $bitmask, $hinthash) = @{$e->frames->[-1]};
268              
269             =head2 line
270              
271             my $line = $e->line;
272             $e = $e->line([3, 'die;']);
273              
274             The line where the exception occurred if available.
275              
276             =head2 lines_after
277              
278             my $lines = $e->lines_after;
279             $e = $e->lines_after([[4, 'say $foo;'], [5, 'say $bar;']]);
280              
281             Lines after the line where the exception occurred if available.
282              
283             =head2 lines_before
284              
285             my $lines = $e->lines_before;
286             $e = $e->lines_before([[1, 'my $foo = 23;'], [2, 'my $bar = 24;']]);
287              
288             Lines before the line where the exception occurred if available.
289              
290             =head2 message
291              
292             my $msg = $e->message;
293             $e = $e->message('Died at test.pl line 3.');
294              
295             Exception message, defaults to C.
296              
297             =head2 verbose
298              
299             my $bool = $e->verbose;
300             $e = $e->verbose($bool);
301              
302             Show more information with L, such as L, defaults to the value of the
303             C environment variable.
304              
305             =head1 METHODS
306              
307             L inherits all methods from L and implements the following new ones.
308              
309             =head2 inspect
310              
311             $e = $e->inspect;
312             $e = $e->inspect($source1, $source2);
313              
314             Inspect L, L and optional additional sources to fill L, L and
315             L with context information.
316              
317             =head2 new
318              
319             my $e = Mojo::Exception->new;
320             my $e = Mojo::Exception->new('Died at test.pl line 3.');
321              
322             Construct a new L object and assign L if necessary.
323              
324             =head2 to_string
325              
326             my $str = $e->to_string;
327              
328             Render exception. Note that the output format may change as more features are added, only the error message at the
329             beginning is guaranteed not to be modified to allow regex matching.
330              
331             =head2 throw
332              
333             Mojo::Exception->throw('Something went wrong!');
334              
335             Throw exception from the current execution context.
336              
337             # Longer version
338             die Mojo::Exception->new('Something went wrong!')->trace;
339              
340             =head2 trace
341              
342             $e = $e->trace;
343             $e = $e->trace($skip);
344              
345             Generate stack trace and store all L, defaults to skipping C<1> call frame.
346              
347             # Skip 3 call frames
348             $e->trace(3);
349              
350             # Skip no call frames
351             $e->trace(0);
352              
353             =head1 OPERATORS
354              
355             L overloads the following operators.
356              
357             =head2 bool
358              
359             my $bool = !!$e;
360              
361             Always true.
362              
363             =head2 stringify
364              
365             my $str = "$e";
366              
367             Alias for L.
368              
369             =head1 SEE ALSO
370              
371             L, L, L.
372              
373             =cut