1 | package Mojo::Exception;
|
---|
2 | use Mojo::Base -base;
|
---|
3 | use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1;
|
---|
4 |
|
---|
5 | use Mojo::Util 'decode';
|
---|
6 |
|
---|
7 | has [qw(frames line lines_after lines_before)] => sub { [] };
|
---|
8 | has message => 'Exception!';
|
---|
9 | has 'verbose';
|
---|
10 |
|
---|
11 | sub inspect {
|
---|
12 | my ($self, @sources) = @_;
|
---|
13 |
|
---|
14 | # Extract file and line from message
|
---|
15 | my @files;
|
---|
16 | my $msg = $self->lines_before([])->line([])->lines_after([])->message;
|
---|
17 | while ($msg =~ /at\s+(.+?)\s+line\s+(\d+)/g) { unshift @files, [$1, $2] }
|
---|
18 |
|
---|
19 | # Extract file and line from stack trace
|
---|
20 | if (my $zero = $self->frames->[0]) { push @files, [$zero->[1], $zero->[2]] }
|
---|
21 |
|
---|
22 | # Search for context in files
|
---|
23 | for my $file (@files) {
|
---|
24 | next unless -r $file->[0] && open my $handle, '<', $file->[0];
|
---|
25 | $self->_context($file->[1], [[<$handle>]]);
|
---|
26 | return $self;
|
---|
27 | }
|
---|
28 |
|
---|
29 | # Search for context in sources
|
---|
30 | $self->_context($files[-1][1], [map { [split "\n"] } @sources]) if @sources;
|
---|
31 |
|
---|
32 | return $self;
|
---|
33 | }
|
---|
34 |
|
---|
35 | sub new { @_ > 1 ? shift->SUPER::new(message => shift) : shift->SUPER::new }
|
---|
36 |
|
---|
37 | sub to_string {
|
---|
38 | my $self = shift;
|
---|
39 |
|
---|
40 | my $str = $self->message;
|
---|
41 | return $str unless $self->verbose;
|
---|
42 |
|
---|
43 | $str .= "\n" unless $str =~ /\n$/;
|
---|
44 | $str .= $_->[0] . ': ' . $_->[1] . "\n" for @{$self->lines_before};
|
---|
45 | $str .= $self->line->[0] . ': ' . $self->line->[1] . "\n" if $self->line->[0];
|
---|
46 | $str .= $_->[0] . ': ' . $_->[1] . "\n" for @{$self->lines_after};
|
---|
47 |
|
---|
48 | return $str;
|
---|
49 | }
|
---|
50 |
|
---|
51 | sub throw { CORE::die shift->new(shift)->trace(2)->inspect }
|
---|
52 |
|
---|
53 | sub trace {
|
---|
54 | my ($self, $start) = (shift, shift // 1);
|
---|
55 | my @frames;
|
---|
56 | while (my @trace = caller($start++)) { push @frames, \@trace }
|
---|
57 | return $self->frames(\@frames);
|
---|
58 | }
|
---|
59 |
|
---|
60 | sub _append {
|
---|
61 | my ($stack, $line) = @_;
|
---|
62 | $line = decode('UTF-8', $line) // $line;
|
---|
63 | chomp $line;
|
---|
64 | push @$stack, $line;
|
---|
65 | }
|
---|
66 |
|
---|
67 | sub _context {
|
---|
68 | my ($self, $num, $sources) = @_;
|
---|
69 |
|
---|
70 | # Line
|
---|
71 | return unless defined $sources->[0][$num - 1];
|
---|
72 | $self->line([$num]);
|
---|
73 | _append($self->line, $_->[$num - 1]) for @$sources;
|
---|
74 |
|
---|
75 | # Before
|
---|
76 | for my $i (2 .. 6) {
|
---|
77 | last if ((my $previous = $num - $i) < 0);
|
---|
78 | unshift @{$self->lines_before}, [$previous + 1];
|
---|
79 | _append($self->lines_before->[0], $_->[$previous]) for @$sources;
|
---|
80 | }
|
---|
81 |
|
---|
82 | # After
|
---|
83 | for my $i (0 .. 4) {
|
---|
84 | next if ((my $next = $num + $i) < 0);
|
---|
85 | next unless defined $sources->[0][$next];
|
---|
86 | push @{$self->lines_after}, [$next + 1];
|
---|
87 | _append($self->lines_after->[-1], $_->[$next]) for @$sources;
|
---|
88 | }
|
---|
89 | }
|
---|
90 |
|
---|
91 | 1;
|
---|
92 |
|
---|
93 | =encoding utf8
|
---|
94 |
|
---|
95 | =head1 NAME
|
---|
96 |
|
---|
97 | Mojo::Exception - Exceptions with context
|
---|
98 |
|
---|
99 | =head1 SYNOPSIS
|
---|
100 |
|
---|
101 | use Mojo::Exception;
|
---|
102 |
|
---|
103 | # Throw exception and show stack trace
|
---|
104 | eval { Mojo::Exception->throw('Something went wrong!') };
|
---|
105 | say "$_->[1]:$_->[2]" for @{$@->frames};
|
---|
106 |
|
---|
107 | # Customize exception
|
---|
108 | eval {
|
---|
109 | my $e = Mojo::Exception->new('Died at test.pl line 3.');
|
---|
110 | die $e->trace(2)->inspect->verbose(1);
|
---|
111 | };
|
---|
112 | say $@;
|
---|
113 |
|
---|
114 | =head1 DESCRIPTION
|
---|
115 |
|
---|
116 | L<Mojo::Exception> is a container for exceptions with context information.
|
---|
117 |
|
---|
118 | =head1 ATTRIBUTES
|
---|
119 |
|
---|
120 | L<Mojo::Exception> implements the following attributes.
|
---|
121 |
|
---|
122 | =head2 frames
|
---|
123 |
|
---|
124 | my $frames = $e->frames;
|
---|
125 | $e = $e->frames([$frame1, $frame2]);
|
---|
126 |
|
---|
127 | Stack trace if available.
|
---|
128 |
|
---|
129 | # Extract information from the last frame
|
---|
130 | my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext,
|
---|
131 | $is_require, $hints, $bitmask, $hinthash) = @{$e->frames->[-1]};
|
---|
132 |
|
---|
133 | =head2 line
|
---|
134 |
|
---|
135 | my $line = $e->line;
|
---|
136 | $e = $e->line([3, 'die;']);
|
---|
137 |
|
---|
138 | The line where the exception occurred if available.
|
---|
139 |
|
---|
140 | =head2 lines_after
|
---|
141 |
|
---|
142 | my $lines = $e->lines_after;
|
---|
143 | $e = $e->lines_after([[4, 'say $foo;'], [5, 'say $bar;']]);
|
---|
144 |
|
---|
145 | Lines after the line where the exception occurred if available.
|
---|
146 |
|
---|
147 | =head2 lines_before
|
---|
148 |
|
---|
149 | my $lines = $e->lines_before;
|
---|
150 | $e = $e->lines_before([[1, 'my $foo = 23;'], [2, 'my $bar = 24;']]);
|
---|
151 |
|
---|
152 | Lines before the line where the exception occurred if available.
|
---|
153 |
|
---|
154 | =head2 message
|
---|
155 |
|
---|
156 | my $msg = $e->message;
|
---|
157 | $e = $e->message('Died at test.pl line 3.');
|
---|
158 |
|
---|
159 | Exception message, defaults to C<Exception!>.
|
---|
160 |
|
---|
161 | =head2 verbose
|
---|
162 |
|
---|
163 | my $bool = $e->verbose;
|
---|
164 | $e = $e->verbose($bool);
|
---|
165 |
|
---|
166 | Enable context information for L</"to_string">.
|
---|
167 |
|
---|
168 | =head1 METHODS
|
---|
169 |
|
---|
170 | L<Mojo::Exception> inherits all methods from L<Mojo::Base> and implements the
|
---|
171 | following new ones.
|
---|
172 |
|
---|
173 | =head2 inspect
|
---|
174 |
|
---|
175 | $e = $e->inspect;
|
---|
176 | $e = $e->inspect($source1, $source2);
|
---|
177 |
|
---|
178 | Inspect L</"message">, L</"frames"> and optional additional sources to fill
|
---|
179 | L</"lines_before">, L</"line"> and L</"lines_after"> with context information.
|
---|
180 |
|
---|
181 | =head2 new
|
---|
182 |
|
---|
183 | my $e = Mojo::Exception->new;
|
---|
184 | my $e = Mojo::Exception->new('Died at test.pl line 3.');
|
---|
185 |
|
---|
186 | Construct a new L<Mojo::Exception> object and assign L</"message"> if necessary.
|
---|
187 |
|
---|
188 | =head2 to_string
|
---|
189 |
|
---|
190 | my $str = $e->to_string;
|
---|
191 |
|
---|
192 | Render exception.
|
---|
193 |
|
---|
194 | # Render exception with context
|
---|
195 | say $e->verbose(1)->to_string;
|
---|
196 |
|
---|
197 | =head2 throw
|
---|
198 |
|
---|
199 | Mojo::Exception->throw('Something went wrong!');
|
---|
200 |
|
---|
201 | Throw exception from the current execution context.
|
---|
202 |
|
---|
203 | # Longer version
|
---|
204 | die Mojo::Exception->new('Something went wrong!')->trace->inspect;
|
---|
205 |
|
---|
206 | =head2 trace
|
---|
207 |
|
---|
208 | $e = $e->trace;
|
---|
209 | $e = $e->trace($skip);
|
---|
210 |
|
---|
211 | Generate stack trace and store all L</"frames">, defaults to skipping C<1> call
|
---|
212 | frame.
|
---|
213 |
|
---|
214 | # Skip 3 call frames
|
---|
215 | $e->trace(3);
|
---|
216 |
|
---|
217 | # Skip no call frames
|
---|
218 | $e->trace(0);
|
---|
219 |
|
---|
220 | =head1 OPERATORS
|
---|
221 |
|
---|
222 | L<Mojo::Exception> overloads the following operators.
|
---|
223 |
|
---|
224 | =head2 bool
|
---|
225 |
|
---|
226 | my $bool = !!$e;
|
---|
227 |
|
---|
228 | Always true.
|
---|
229 |
|
---|
230 | =head2 stringify
|
---|
231 |
|
---|
232 | my $str = "$e";
|
---|
233 |
|
---|
234 | Alias for L</"to_string">.
|
---|
235 |
|
---|
236 | =head1 SEE ALSO
|
---|
237 |
|
---|
238 | L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
|
---|
239 |
|
---|
240 | =cut
|
---|