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   74995 use Mojo::Base -base;
  70         100  
  70         404  
3 70     70   2119 use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1;
  70     251   5360  
  70     93   658  
  68         553  
  278         8848  
4              
5 70     70   5454 use Carp qw(croak);
  70         120  
  70         3202  
6 70     70   349 use Exporter qw(import);
  70         185  
  70         2039  
7 70     70   1546 use Mojo::Util qw(decode);
  70         140  
  70         2998  
8 70     70   288 use Scalar::Util qw(blessed);
  70         111  
  70         123363  
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 14323 my ($err, $spec) = @_;
18 15 100       115 return undef unless $err;
19              
20 13 100 100     420 croak "Array reference of pattern/handler pairs required to dispatch exceptions"
21             if ref $spec ne 'ARRAY' || @$spec % 2;
22              
23 11         17 my ($default, $handler);
24 11         31 my ($is_obj, $str) = (!!blessed($err), "$err");
25 11         47 CHECK: for (my $i = 0; $i < @$spec; $i += 2) {
26 20         30 my ($checks, $cb) = @{$spec}[$i, $i + 1];
  20         39  
27              
28 20 100 50     65 ($default = $cb) and next if $checks eq 'default';
29              
30 16 100       38 for my $c (ref $checks eq 'ARRAY' ? @$checks : $checks) {
31 18         26 my $is_re = !!ref $c;
32 18 100 50     86 ($handler = $cb) and last CHECK if $is_obj && !$is_re && $err->isa($c);
      100        
      100        
33 13 100 50     86 ($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     38 die $err unless $handler ||= $default;
39 10         29 $handler->($_) for $err;
40              
41 10         44 return 1;
42             }
43              
44             sub inspect {
45 133     133 1 718 my ($self, @sources) = @_;
46              
47 133 100       233 return $self if @{$self->line};
  133         414  
48              
49             # Extract file and line from message
50 118         194 my @files;
51 118         285 my $msg = $self->message;
52 118         1940 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       288 if (my $zero = $self->frames->[0]) { push @files, [$zero->[1], $zero->[2]] }
  86         272  
56              
57             # Search for context in files
58 118         252 for my $file (@files) {
59 141 100 66     6909 next unless -r $file->[0] && open my $handle, '<', $file->[0];
60 85         30822 $self->_context($file->[1], [[<$handle>]]);
61 85         6308 return $self;
62             }
63              
64             # Search for context in sources
65 33 100       113 $self->_context($files[-1][1], [map { [split /\n/] } @sources]) if @sources;
  48         298  
66              
67 33         181 return $self;
68             }
69              
70 186 100   186 1 81131 sub new { defined $_[1] ? shift->SUPER::new(message => shift) : shift->SUPER::new }
71              
72             sub raise {
73 5 100   5 1 4906 my ($class, $err) = @_ > 1 ? (@_) : (__PACKAGE__, shift);
74              
75 5 50   1   74 if (!$class->can('new')) { die $@ unless eval "package $class; use Mojo::Base 'Mojo::Exception'; 1" }
  1 100       147  
  1 100       10  
  1         1  
  1         7  
76 1         11 elsif (!$class->isa(__PACKAGE__)) { die "$class is not a Mojo::Exception subclass" }
77              
78 4         20 CORE::die $class->new($err)->trace;
79             }
80              
81             sub to_string {
82 278     278 1 391 my $self = shift;
83              
84 278         543 my $str = $self->message;
85              
86 278         488 my $frames = $self->frames;
87 278 100       1000 if ($str !~ /\n$/) {
88 50 100       124 $str .= @$frames ? " at $frames->[0][1] line $frames->[0][2].\n" : "\n";
89             }
90 278 100       531 return $str unless $self->verbose;
91              
92 67         147 my $line = $self->line;
93 67 100       157 if (@$line) {
94 66         148 $str .= "Context:\n";
95 66         75 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_before};
  66         122  
96 66         162 $str .= " $line->[0]: $line->[1]\n";
97 66         71 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_after};
  66         113  
98             }
99              
100 67 50       138 if (my $max = @$frames) {
101 67         113 $str .= "Traceback (most recent call first):\n";
102 67         2804 $str .= qq{ File "$_->[1]", line $_->[2], in "$_->[3]"\n} for @$frames;
103             }
104              
105 67         571 return $str;
106             }
107              
108 3     3 1 7050 sub throw { CORE::die shift->new(shift)->trace }
109              
110             sub trace {
111 152   100 152 1 568 my ($self, $start) = (shift, shift // 1);
112 152         231 my @frames;
113 152         1655 while (my @trace = caller($start++)) { push @frames, \@trace }
  5826         32959  
114 152         610 return $self->frames(\@frames);
115             }
116              
117             sub _append {
118 975     975   1331 my ($stack, $line) = @_;
119 975   66     1433 $line = decode('UTF-8', $line) // $line;
120 975         1198 chomp $line;
121 975         1808 push @$stack, $line;
122             }
123              
124             sub _context {
125 109     109   348 my ($self, $num, $sources) = @_;
126              
127             # Line
128 109 50       445 return unless defined $sources->[0][$num - 1];
129 109         456 $self->line([$num]);
130 109         377 _append($self->line, $_->[$num - 1]) for @$sources;
131              
132             # Before
133 109         284 for my $i (2 .. 6) {
134 451 100       796 last if ((my $previous = $num - $i) < 0);
135 411         398 unshift @{$self->lines_before}, [$previous + 1];
  411         658  
136 411         721 _append($self->lines_before->[0], $_->[$previous]) for @$sources;
137             }
138              
139             # After
140 109         236 for my $i (0 .. 4) {
141 545 50       793 next if ((my $next = $num + $i) < 0);
142 545 100       826 next unless defined $sources->[0][$next];
143 398         411 push @{$self->lines_after}, [$next + 1];
  398         608  
144 398         644 _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