File Coverage

blib/lib/TryCatch.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package TryCatch;
2              
3 1     1   30505 use strict;
  1         2  
  1         26  
4 1     1   4 use warnings;
  1         1  
  1         20  
5              
6              
7 1     1   110709 use Devel::Declare ();
  0            
  0            
8             use B::Hooks::EndOfScope;
9             use B::Hooks::OP::PPAddr;
10             use Devel::Declare::Context::Simple;
11             use Parse::Method::Signatures;
12             use Moose::Util::TypeConstraints;
13             use Scope::Upper qw/localize unwind want_at :words/;
14             use Carp qw/croak/;
15             use XSLoader;
16              
17             use base qw/Devel::Declare::Context::Simple/;
18              
19              
20             our $VERSION = '1.003002';
21              
22             # Signal to the xs PL_check hooks.
23             our $NEXT_EVAL_IS_TRY = 0;
24              
25             # Constants
26             my ($LOOKAHEAD_TRY, $LOOKAHEAD_CATCH) = (0,1);
27              
28             XSLoader::load(__PACKAGE__, $VERSION);
29              
30             use namespace::clean;
31              
32             use Sub::Exporter -setup => {
33             exports => [qw/try/],
34             groups => { default => [qw/try/] },
35             installer => sub {
36             my ($args, $to_export) = @_;
37             my $pack = $args->{into};
38             my $ctx_class = $args->{parser} || 'TryCatch';
39              
40             foreach my $name (@$to_export) {
41             if (my $parser = __PACKAGE__->can("_parse_${name}")) {
42             Devel::Declare->setup_for(
43             $pack,
44             { $name => { const => sub { $ctx_class->$parser($pack, @_) } } },
45             );
46             }
47             }
48             Sub::Exporter::default_installer(@_);
49              
50             }
51             };
52              
53              
54             # The actual try call itself. Nothing to do with parsing.
55             sub try () {
56             return;
57             }
58              
59             # Where we store all the TCs for catch blocks created at compile time
60             # Not sure we really want to do this, but we will for now.
61             our $TC_LIBRARY = {};
62              
63             sub check_tc {
64             my ($class, $tc) = @_;
65              
66             my $type = $TC_LIBRARY->{$tc} or die "Unable to find parse TC for '$tc'";
67              
68             return $type->check($TryCatch::Error);
69             }
70              
71             # From here on out its parsing methods.
72              
73             # Replace 'try {' with an 'try; { local $@; eval {'
74             sub _parse_try {
75             my ($class,$pack, @args) = @_;
76              
77             # Hide from carp - report errors from line of 'try {' in user source.
78             local $Carp::Internal{'Devel::Declare'} = 1;
79              
80             my $ctx = $class->new->init(@args);
81              
82             # Move parse head past 'try ' (space is optional
83             $ctx->skip_declarator;
84             $ctx->skipspace;
85              
86             # Shadow try to be a constant no-op sub. Hopefully
87             $ctx->shadow(sub () { } );
88              
89             $ctx->inject_if_block(
90             $ctx->inject_into_try . $ctx->scope_injector_call( $LOOKAHEAD_TRY ),
91             ';'
92             ) or croak "block required after try";
93              
94             $ctx->debug_linestr('post try');
95             }
96              
97             sub scope_injector_call {
98             my ($self, $state) = @_;
99             return ' BEGIN { ' . ref( $self ) . "->inject_scope($state) }; ";
100             }
101              
102              
103             sub inject_scope {
104             my ($class, $opts) = @_;
105              
106             my $hooks = TryCatch::XS::install_op_checks();
107              
108             on_scope_end {
109             $class->lookahead_after_block( $opts );
110              
111             # TODO: Rethink how i install the hooks. If i uninstall(/disable) them here
112             # then they get removed before the LEAVETRY check gets called. Probably
113             # switch to a single global set of hooks at look at %^H (?) for lexical
114             # goodness.
115              
116             #TryCatch::XS::uninstall_op_checks( $hooks );
117             #undef $hooks;
118             }
119             }
120              
121             # Called after the block from try {} or catch {}
122             #
123             # Look ahead and determine what action to take based on wether or not we see
124             # a 'catch' token after the block
125             sub lookahead_after_block {
126             my ($class, $state) = @_;
127             my $orig_offset = Devel::Declare::get_linestr_offset();
128             my $ctx = $class->new->init( '', $orig_offset );
129              
130             my $offset = $ctx->skipspace;
131             my $linestr = $ctx->get_linestr;
132              
133             my $toke = '';
134             my $len = 0;
135              
136             # Since we're not being called from a normal D::D callback, we have to
137             # find this info manually.
138             if ($len = Devel::Declare::toke_scan_word($offset, 1 )) {
139             $toke = substr( $linestr, $offset, $len );
140             $ctx->{Declarator} = $toke;
141             }
142              
143             if ($toke eq 'catch') {
144             # We don't want the 'catch' token in the output since it messes up the
145             # if/else we build up. So dont let control go back to perl just yet.
146              
147             $ctx->_parse_catch( $state );
148              
149             } else {
150             # No (more) catch blocks, so write the postlude
151             my $code;
152             if ($state == $LOOKAHEAD_CATCH) {
153             $code = $ctx->inject_post_catch;
154             }
155             else {
156             $code = $ctx->inject_when_no_catch;
157             $NEXT_EVAL_IS_TRY = 1;
158             }
159              
160             # Don't try this at home kids
161             #
162             # Since there was no 'catch' following, move back to the end of the
163             # closing brace (where offset was when we started). If we are after the
164             # skip space then the 'parse pointer' could be at the start of a POD line,
165             # and ";=head1" isn't valid perl ;)
166             #
167             # This seems to cause problems with nested try so taken out for now
168             #
169             #TryCatch::XS::set_linestr_offset($orig_offset);
170             #$ctx->{Offset} = $orig_offset;
171              
172             substr($linestr, $ctx->offset, 0, $code);
173              
174             $ctx->set_linestr($linestr);
175             $ctx->debug_linestr("finalizer");
176             }
177             }
178              
179             sub _parse_catch {
180             my ($ctx, $state) = @_;
181              
182             # Hide these things from carp - this makes C<croak> appear to come from the source line.
183             local $Carp::Internal{'TryCatch'} = 1;
184             local $Carp::Internal{'Devel::Declare'} = 1;
185             local $Carp::Internal{'B::Hooks::EndOfScope::XS'} = 1;
186             local $Carp::Internal{'B::Hooks::EndOfScope::PP'} = 1;
187              
188             # This isn't a normal DD-callback, so we can strip_name to get rid of 'catch'
189             my $offset = $ctx->offset;
190             $ctx->strip_name;
191             $ctx->skipspace;
192              
193             $ctx->debug_linestr('catch');
194             my $linestr = $ctx->get_linestr;
195              
196             my ($code, $var_code, @conditions) = ("","");
197              
198             # optional ()
199             if (substr($linestr, $ctx->offset, 1) eq '(') {
200             ($var_code, @conditions) = $ctx->parse_proto()
201             }
202              
203             @conditions = ('1') unless @conditions;
204              
205             if ( $state != $LOOKAHEAD_CATCH ) {
206             $NEXT_EVAL_IS_TRY = 1;
207             $code = $ctx->inject_after_try . "if (";
208             }
209             else {
210             $code = "elsif (";
211             }
212              
213             $var_code = $ctx->scope_injector_call( $LOOKAHEAD_CATCH ) . $var_code;
214              
215             $ctx->inject_if_block(
216             $var_code,
217             $code . join(' && ', @conditions) . ')'
218             ) or croak "block required after catch";
219              
220             $ctx->debug_linestr('post catch');
221              
222             }
223              
224             sub parse_proto {
225             my ($self) = @_;
226              
227             my $proto = $self->strip_proto;
228             croak "Run-away catch signature"
229             unless (length $proto);
230              
231             return $self->parse_proto_using_pms($proto);
232             }
233              
234             sub _string_to_tc {
235             my ($class, $name) = @_;
236              
237             my $tc = $class->find_registered_constraint($name);
238              
239             return $tc if ref $tc;
240              
241             return Moose::Util::TypeConstraints::find_or_create_isa_type_constraint($name)
242             }
243              
244             sub parse_proto_using_pms {
245             my ($self, $proto) = @_;
246              
247             my @conditions;
248              
249             my $sig = Parse::Method::Signatures->new(
250             input => $proto,
251             from_namespace => $self->get_curstash_name,
252             type_constraint_callback => \&_string_to_tc,
253             );
254             my $errctx = $sig->ppi;
255             my $param = $sig->param;
256              
257             $sig->error( $errctx, "Parameter expected")
258             unless $param;
259              
260             my $left = $sig->remaining_input;
261              
262             my $var_code = '';
263              
264             if (my $var_name = $param->can('variable_name') ) {
265              
266             my $name = $param->$var_name();
267             $var_code = "my $name = \$TryCatch::Error;";
268             }
269              
270             # (TC $var)
271             if ($param->has_type_constraints) {
272             my $tc = $param->meta_type_constraint;
273             $TryCatch::TC_LIBRARY->{"$tc"} = $tc;
274             push @conditions, "TryCatch->check_tc('$tc')";
275             }
276              
277             # ($var where { $_ } )
278             if ($param->has_constraints) {
279             foreach my $con (@{$param->constraints}) {
280             $con =~ s/^{|}$//g;
281             push @conditions, "do {local \$_ = \$TryCatch::Error; $con }";
282             }
283             }
284              
285             return $var_code, @conditions;
286             }
287              
288              
289             #######################################################################
290             # Injected snippets
291              
292             sub inject_into_try {
293             # try { ...
294             # ->
295             # try; { local $@; eval { ...
296              
297             'local $@; eval {'
298             }
299              
300             sub inject_after_try {
301             # This semicolon is for the end of the eval
302             return ';$TryCatch::Error = $@; } if ($TryCatch::Error) { ';
303             }
304              
305             sub inject_when_no_catch {
306             # This undef is to ensure that there is the eval{}; is called in void context
307             # i.e that its not the last op in a subroutine
308             return "};undef;";
309             }
310              
311             sub inject_post_catch {
312             # We do it like this so that PROPGATE gets called, in case anyone is using it
313             return 'else { $@ = $TryCatch::Error; die } };undef;';
314             }
315              
316             #######################################################################
317              
318             require Devel::PartialDump if $ENV{TRYCATCH_DEBUG};
319              
320             *debug_linestr = !( ($ENV{TRYCATCH_DEBUG} || 0) & 1)
321             ? sub {}
322             : sub {
323             my ($ctx, $message) = @_;
324              
325             local $Carp::Internal{'TryCatch'} = 1;
326             local $Carp::Internal{'TryCatch::Basic'} = 1;
327             local $Carp::Internal{'Devel::Declare'} = 1;
328             local $Carp::Internal{'B::Hooks::EndOfScope:XS'} = 1;
329             local $Carp::Internal{'B::Hooks::EndOfScope:PP'} = 1;
330             local $Carp::Internal{'Devel::PartialDump'} = 1;
331             Carp::cluck($message) if $message;
332              
333             warn " Substr: ", Devel::PartialDump::dump(substr($ctx->get_linestr, $ctx->offset)),
334             "\n Whole: ", Devel::PartialDump::dump($ctx->get_linestr), "\n\n";
335             };
336              
337              
338             1;
339              
340             __END__
341              
342             =head1 NAME
343              
344             TryCatch - first class try catch semantics for Perl, without source filters.
345              
346             =head1 DESCRIPTION
347              
348             This module aims to provide a nicer syntax and method to catch errors in Perl,
349             similar to what is found in other languages (such as Java, Python or C++). The
350             standard method of using C<< eval {}; if ($@) {} >> is often prone to subtle
351             bugs, primarily that its far too easy to stomp on the error in error handlers.
352             And also eval/if isn't the nicest idiom.
353              
354             =head1 SYNOPSIS
355              
356             use TryCatch;
357              
358             sub foo {
359             my ($self) = @_;
360              
361             try {
362             die Some::Class->new(code => 404 ) if $self->not_found;
363             return "return value from foo";
364             }
365             catch (Some::Class $e where { $_->code > 100 } ) {
366             }
367             }
368              
369             =head1 SYNTAX
370              
371             This module aims to give first class exception handling to perl via 'try' and
372             'catch' keywords. The basic syntax this module provides is C<try { # block }>
373             followed by zero or more catch blocks. Each catch block has an optional type
374             constraint on it the resembles Perl6's method signatures.
375              
376             Also worth noting is that the error variable (C<$@>) is localised to the
377             try/catch blocks and will not leak outside the scope, or stomp on a previous
378             value of C<$@>.
379              
380             The simplest case of a catch block is just
381              
382             catch { ... }
383              
384             where upon the error is available in the standard C<$@> variable and no type
385             checking is performed. The exception can instead be accessed via a named
386             lexical variable by providing a simple signature to the catch block as follows:
387              
388             catch ($err) { ... }
389              
390             Type checking of the exception can be performed by specifing a type constraint
391             or where clauses in the signature as follows:
392              
393             catch (TypeFoo $e) { ... }
394             catch (Dict[code => Int, message => Str] $err) { ... }
395              
396             As shown in the above example, complex Moose types can be used, including
397             L<MooseX::Types> style of type constraints
398              
399             In addition to type checking via Moose type constraints, you can also use where
400             clauses to only match a certain sub-condition on an error. For example,
401             assuming that C<HTTPError> is a suitably defined TC:
402              
403             catch (HTTPError $e where { $_->code >= 400 && $_->code <= 499 } ) {
404             return "4XX error";
405             }
406             catch (HTTPError $e) {
407             return "other http code";
408             }
409              
410             would return "4XX error" in the case of a 404 error, and "other http code" in
411             the case of a 302.
412              
413             In the case where multiple catch blocks are present, the first one that matches
414             the type constraints (if any) will executed.
415              
416             =head1 BENEFITS
417              
418             B<return>. You can put a return in a try block, and it would do the right thing
419             - namely return a value from the subroutine you are in, instead of just from
420             the eval block.
421              
422             B<Type Checking>. This is nothing you couldn't do manually yourself, it does it
423             for you using Moose type constraints.
424              
425             =head1 TODO
426              
427             =over
428              
429             =item *
430              
431             Decide on C<finally> semantics w.r.t return values.
432              
433             =item *
434              
435             Write some more documentation
436              
437             =item *
438              
439             Split out the dependancy on Moose
440              
441             =back
442              
443             =head1 SEE ALSO
444              
445             L<MooseX::Types>, L<Moose::Util::TypeConstraints>, L<Parse::Method::Signatures>.
446              
447             =head1 AUTHOR
448              
449             Ash Berlin <ash@cpan.org>
450              
451             =head1 THANKS
452              
453             Thanks to Matt S Trout and Florian Ragwitz for work on L<Devel::Declare> and
454             various B::Hooks modules
455              
456             Vincent Pit for L<Scope::Upper> that makes the return from block possible.
457              
458             Zefram for providing support and XS guidance.
459              
460             Xavier Bergade for the impetus to finally fix this module in 5.12.
461              
462             =head1 LICENSE
463              
464             Licensed under the same terms as Perl itself.
465