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 17 17 100.0
pod 7 7 100.0
total 197 206 95.6


line stmt bran cond sub pod time code
1             package Mojo::Exception;
2 70     170   73891 use Mojo::Base -base;
  70         130  
  70         418  
3 70     70   1974 use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1;
  70     244   5779  
  70     1   600  
  68         427  
  278         7388  
4              
5 70     70   5078 use Carp qw(croak);
  70         103  
  70         3200  
6 70     70   368 use Exporter qw(import);
  70         113  
  70         1965  
7 70     70   1836 use Mojo::Util qw(decode);
  70         100  
  70         2836  
8 70     70   287 use Scalar::Util qw(blessed);
  70         123  
  70         120920  
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 11752 my ($err, $spec) = @_;
18 15 100       85 return undef unless $err;
19              
20 13 100 100     324 croak "Array reference of pattern/handler pairs required to dispatch exceptions"
21             if ref $spec ne 'ARRAY' || @$spec % 2;
22              
23 11         18 my ($default, $handler);
24 11         25 my ($is_obj, $str) = (!!blessed($err), "$err");
25 11         40 CHECK: for (my $i = 0; $i < @$spec; $i += 2) {
26 20         21 my ($checks, $cb) = @{$spec}[$i, $i + 1];
  20         32  
27              
28 20 100 50     40 ($default = $cb) and next if $checks eq 'default';
29              
30 16 100       32 for my $c (ref $checks eq 'ARRAY' ? @$checks : $checks) {
31 18         18 my $is_re = !!ref $c;
32 18 100 50     82 ($handler = $cb) and last CHECK if $is_obj && !$is_re && $err->isa($c);
      100        
      100        
33 13 100 50     67 ($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     57 die $err unless $handler ||= $default;
39 10         33 $handler->($_) for $err;
40              
41 10         42 return 1;
42             }
43              
44             sub inspect {
45 133     133 1 648 my ($self, @sources) = @_;
46              
47 133 100       165 return $self if @{$self->line};
  133         396  
48              
49             # Extract file and line from message
50 118         193 my @files;
51 118         265 my $msg = $self->message;
52 118         1967 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       302 if (my $zero = $self->frames->[0]) { push @files, [$zero->[1], $zero->[2]] }
  86         261  
56              
57             # Search for context in files
58 118         243 for my $file (@files) {
59 141 100 66     7122 next unless -r $file->[0] && open my $handle, '<', $file->[0];
60 85         31504 $self->_context($file->[1], [[<$handle>]]);
61 85         6141 return $self;
62             }
63              
64             # Search for context in sources
65 33 100       102 $self->_context($files[-1][1], [map { [split /\n/] } @sources]) if @sources;
  48         294  
66              
67 33         153 return $self;
68             }
69              
70 186 100   186 1 71587 sub new { defined $_[1] ? shift->SUPER::new(message => shift) : shift->SUPER::new }
71              
72             sub raise {
73 5 100   5 1 3893 my ($class, $err) = @_ > 1 ? (@_) : (__PACKAGE__, shift);
74              
75 5 50       52 if (!$class->can('new')) { die $@ unless eval "package $class; use Mojo::Base 'Mojo::Exception'; 1" }
  1 100       98  
  1 100       9  
  1         1  
  1         6  
76 1         9 elsif (!$class->isa(__PACKAGE__)) { die "$class is not a Mojo::Exception subclass" }
77              
78 4         21 CORE::die $class->new($err)->trace;
79             }
80              
81             sub to_string {
82 278     278 1 316 my $self = shift;
83              
84 278         629 my $str = $self->message;
85              
86 278         489 my $frames = $self->frames;
87 278 100       902 if ($str !~ /\n$/) {
88 50 100       122 $str .= @$frames ? " at $frames->[0][1] line $frames->[0][2].\n" : "\n";
89             }
90 278 100       602 return $str unless $self->verbose;
91              
92 67         121 my $line = $self->line;
93 67 100       136 if (@$line) {
94 66         131 $str .= "Context:\n";
95 66         75 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_before};
  66         120  
96 66         144 $str .= " $line->[0]: $line->[1]\n";
97 66         80 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_after};
  66         106  
98             }
99              
100 67 50       161 if (my $max = @$frames) {
101 67         96 $str .= "Traceback (most recent call first):\n";
102 67         2597 $str .= qq{ File "$_->[1]", line $_->[2], in "$_->[3]"\n} for @$frames;
103             }
104              
105 67         545 return $str;
106             }
107              
108 3     3 1 6603 sub throw { CORE::die shift->new(shift)->trace }
109              
110             sub trace {
111 152   100 152 1 585 my ($self, $start) = (shift, shift // 1);
112 152         231 my @frames;
113 152         1554 while (my @trace = caller($start++)) { push @frames, \@trace }
  5826         33810  
114 152         621 return $self->frames(\@frames);
115             }
116              
117             sub _append {
118 975     975   1279 my ($stack, $line) = @_;
119 975   66     1440 $line = decode('UTF-8', $line) // $line;
120 975         1189 chomp $line;
121 975         1809 push @$stack, $line;
122             }
123              
124             sub _context {
125 109     109   336 my ($self, $num, $sources) = @_;
126              
127             # Line
128 109 50       438 return unless defined $sources->[0][$num - 1];
129 109         451 $self->line([$num]);
130 109         358 _append($self->line, $_->[$num - 1]) for @$sources;
131              
132             # Before
133 109         286 for my $i (2 .. 6) {
134 451 100       750 last if ((my $previous = $num - $i) < 0);
135 411         424 unshift @{$self->lines_before}, [$previous + 1];
  411         715  
136 411         769 _append($self->lines_before->[0], $_->[$previous]) for @$sources;
137             }
138              
139             # After
140 109         209 for my $i (0 .. 4) {
141 545 50       838 next if ((my $next = $num + $i) < 0);
142 545 100       835 next unless defined $sources->[0][$next];
143 398         388 push @{$self->lines_after}, [$next + 1];
  398         597  
144 398         665 _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