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   97458 use Mojo::Base -base;
  70         171  
  70         572  
3 70     70   2658 use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1;
  70     73   8637  
  70     247   895  
  68         719  
  278         9039  
4              
5 70     70   7376 use Carp qw(croak);
  70         276  
  70         4434  
6 70     70   414 use Exporter qw(import);
  70         147  
  70         3001  
7 70     70   2368 use Mojo::Util qw(decode);
  70         143  
  70         4748  
8 70     70   439 use Scalar::Util qw(blessed);
  70         233  
  70         167244  
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       117 return undef unless $err;
19              
20 13 100 100     343 croak "Array reference of pattern/handler pairs required to dispatch exceptions"
21             if ref $spec ne 'ARRAY' || @$spec % 2;
22              
23 11         19 my ($default, $handler);
24 11         42 my ($is_obj, $str) = (!!blessed($err), "$err");
25 11         51 CHECK: for (my $i = 0; $i < @$spec; $i += 2) {
26 20         33 my ($checks, $cb) = @{$spec}[$i, $i + 1];
  20         39  
27              
28 20 100 50     56 ($default = $cb) and next if $checks eq 'default';
29              
30 16 100       44 for my $c (ref $checks eq 'ARRAY' ? @$checks : $checks) {
31 18         27 my $is_re = !!ref $c;
32 18 100 50     100 ($handler = $cb) and last CHECK if $is_obj && !$is_re && $err->isa($c);
      100        
      100        
33 13 100 50     97 ($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     41 die $err unless $handler ||= $default;
39 10         38 $handler->($_) for $err;
40              
41 10         44 return 1;
42             }
43              
44             sub inspect {
45 133     133 1 916 my ($self, @sources) = @_;
46              
47 133 100       250 return $self if @{$self->line};
  133         552  
48              
49             # Extract file and line from message
50 118         273 my @files;
51 118         446 my $msg = $self->message;
52 118         2783 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       471 if (my $zero = $self->frames->[0]) { push @files, [$zero->[1], $zero->[2]] }
  86         410  
56              
57             # Search for context in files
58 118         396 for my $file (@files) {
59 141 100 66     9086 next unless -r $file->[0] && open my $handle, '<', $file->[0];
60 85         50644 $self->_context($file->[1], [[<$handle>]]);
61 85         9613 return $self;
62             }
63              
64             # Search for context in sources
65 33 100       249 $self->_context($files[-1][1], [map { [split /\n/] } @sources]) if @sources;
  48         454  
66              
67 33         272 return $self;
68             }
69              
70 186 100   186 1 80358 sub new { defined $_[1] ? shift->SUPER::new(message => shift) : shift->SUPER::new }
71              
72             sub raise {
73 5 100   5 1 3984 my ($class, $err) = @_ > 1 ? (@_) : (__PACKAGE__, shift);
74              
75 5 50   1   52 if (!$class->can('new')) { die $@ unless eval "package $class; use Mojo::Base 'Mojo::Exception'; 1" }
  1 100       109  
  1 100       9  
  1         1  
  1         5  
76 1         8 elsif (!$class->isa(__PACKAGE__)) { die "$class is not a Mojo::Exception subclass" }
77              
78 4         18 CORE::die $class->new($err)->trace;
79             }
80              
81             sub to_string {
82 278     278 1 504 my $self = shift;
83              
84 278         976 my $str = $self->message;
85              
86 278         905 my $frames = $self->frames;
87 278 100       1541 if ($str !~ /\n$/) {
88 50 100       209 $str .= @$frames ? " at $frames->[0][1] line $frames->[0][2].\n" : "\n";
89             }
90 278 100       925 return $str unless $self->verbose;
91              
92 67         260 my $line = $self->line;
93 67 100       228 if (@$line) {
94 66         230 $str .= "Context:\n";
95 66         110 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_before};
  66         184  
96 66         238 $str .= " $line->[0]: $line->[1]\n";
97 66         113 $str .= " $_->[0]: $_->[1]\n" for @{$self->lines_after};
  66         220  
98             }
99              
100 67 50       215 if (my $max = @$frames) {
101 67         176 $str .= "Traceback (most recent call first):\n";
102 67         4805 $str .= qq{ File "$_->[1]", line $_->[2], in "$_->[3]"\n} for @$frames;
103             }
104              
105 67         1019 return $str;
106             }
107              
108 3     3 1 7302 sub throw { CORE::die shift->new(shift)->trace }
109              
110             sub trace {
111 152   100 152 1 874 my ($self, $start) = (shift, shift // 1);
112 152         312 my @frames;
113 152         2579 while (my @trace = caller($start++)) { push @frames, \@trace }
  5826         53264  
114 152         963 return $self->frames(\@frames);
115             }
116              
117             sub _append {
118 975     975   1995 my ($stack, $line) = @_;
119 975   66     2395 $line = decode('UTF-8', $line) // $line;
120 975         1819 chomp $line;
121 975         3088 push @$stack, $line;
122             }
123              
124             sub _context {
125 109     109   471 my ($self, $num, $sources) = @_;
126              
127             # Line
128 109 50       618 return unless defined $sources->[0][$num - 1];
129 109         689 $self->line([$num]);
130 109         536 _append($self->line, $_->[$num - 1]) for @$sources;
131              
132             # Before
133 109         372 for my $i (2 .. 6) {
134 451 100       1204 last if ((my $previous = $num - $i) < 0);
135 411         607 unshift @{$self->lines_before}, [$previous + 1];
  411         1103  
136 411         1187 _append($self->lines_before->[0], $_->[$previous]) for @$sources;
137             }
138              
139             # After
140 109         375 for my $i (0 .. 4) {
141 545 50       1254 next if ((my $next = $num + $i) < 0);
142 545 100       1287 next unless defined $sources->[0][$next];
143 398         609 push @{$self->lines_after}, [$next + 1];
  398         1000  
144 398         1121 _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