File Coverage

blib/lib/Try/Harder.pm
Criterion Covered Total %
statement 66 67 98.5
branch 15 22 68.1
condition 1 3 33.3
subroutine 15 15 100.0
pod 0 2 0.0
total 97 109 88.9


line stmt bran cond sub pod time code
1 9     9   425408 use strict;
  9         71  
  9         214  
2 9     9   36 use warnings;
  9         14  
  9         316  
3             package Try::Harder;
4              
5             # ABSTRACT: Try hard to get the functionality of Syntax::Keyword::Try
6              
7 9     9   3609 use Module::Load::Conditional qw( can_load );
  9         198348  
  9         449  
8 9     9   3359 use Import::Into;
  9         19005  
  9         218  
9              
10 9     9   61 use Carp;
  9         16  
  9         774  
11             $Carp::Internal{+__PACKAGE__}++;
12              
13             # determine if we can use Syntax::Keyword::Try or have to use the pure-perl
14             # source filtering
15             our $USE_PP;
16             BEGIN {
17             # Syntax::Keyword::Try is faster, safer, and better than the source filter
18             # in every way. If it's available, just use it and be done with all this.
19 9 50 33 9   48 if ( can_load( modules => { 'Syntax::Keyword::Try' => undef } )
20             and not $ENV{TRY_HARDER_USE_PP} ) {
21             #warn "Using Syntax::Keyword::Try\n";
22 0         0 $USE_PP = 0;
23             }
24             else {
25             #warn "Using ATHFilter\n";
26 9         4556 $USE_PP = 1;
27             }
28             }
29              
30             sub import {
31             # TODO: add option to force using a particular implementation
32             if ( ! $USE_PP ) {
33             'Syntax::Keyword::Try'->import::into( scalar caller() );
34             }
35             else {
36             # suppress warnings when user uses next/last/continue from within a
37             # try or finally block. This is probably a bad idea, but hell, this
38             # whole module is a bad idea.
39             warnings->unimport('exiting');
40             }
41             }
42              
43              
44             ### code below only needed for the source-filtering implementation
45              
46 9     9   4499 use if $USE_PP, "Filter::Simple";
  9         99  
  9         38  
47 9     9   124457 use if $USE_PP, "Text::Balanced" => qw( extract_codeblock );
  9         15  
  9         38  
48 9     9   599 use if $USE_PP, "Try::Tiny" => ();
  9         15  
  9         26  
49              
50             setup_filter() if $USE_PP;
51              
52             sub setup_filter {
53             # Let Filter::Simple strip out all comments and strings to make it easier
54             # to extract try/catch/finally code-blocks correctly.
55             FILTER_ONLY(
56 8     8   229819 code_no_comments => sub { $_ = munge_code( $_ ) }
57 9     9 0 49 );
58             }
59              
60             # use an object to indicate a code-block never called return. This assumes
61             # nobody will ever intentionally return this object themselves...
62             my $S = __PACKAGE__ . "::SENTINEL";
63             our $SENTINEL = bless {}, $S;
64              
65             # return val of a Try::Tiny try/catch construct gets stored here
66             # so we can return it to the caller if needed.
67             my $R = __PACKAGE__ . "::RETVAL";
68             our @RETVAL;
69              
70             # if an error is caught, stash it here to inject in the finally block
71             my $E = __PACKAGE__ . "::ERROR";
72             our $ERROR;
73              
74             # flag to set if an exception is thrown
75             my $D = __PACKAGE__ . "::DIED";
76             our $DIED;
77              
78             # wantarray context of the surrounding code
79             my $W = __PACKAGE__ . "::WANTARRAY";
80             our $WANTARRAY;
81              
82             # stash the try/catch/finally closures in these
83             my $T = __PACKAGE__ . "::TRY";
84             our $TRY;
85             my $C = __PACKAGE__ . "::CATCH";
86             our $CATCH;
87             my $F = __PACKAGE__ . "::FINALLY";
88             our $FINALLY;
89              
90             # name of the ScopeGuard object for finally functionality
91             my $G = __PACKAGE__ . "::ScopeGuard";
92              
93              
94             # stealing ideas from Try::Tiny, re-write the user's code to present the
95             # same functionality and behavior as Syntax::Keyword::Try, more or less.
96             # Note that the re-written code should take up the same number of lines
97             # as the original code, so line-numbers from warnings and such don't
98             # drive people bonkers.
99             sub munge_code {
100 62     62 0 111 my ($code_to_filter) = @_;
101 62         79 my $filtered_code = "";
102              
103             # ensure user does not use multiple catch/finally blocks.
104             # Note that try { ... } try { ... } is perfectly valid.
105 62         69 my $found_catch = 0;
106 62         67 my $found_finally = 0;
107              
108             # find try/catch/finally keywords followed by a code-block, and extract the block
109 62         193 while ( $code_to_filter =~ / ( .*? ) \b( try | catch | finally ) \s* ( [{] .* ) /msx ) {
110              
111 54         176 my ($before_kw, $kw, $after_kw) = ($1, $2, $3);
112              
113 54         218 my ($code_block, $remainder) = extract_codeblock($after_kw, "{}");
114              
115             # make sure to munge any nested try/catch blocks
116 54 50       25982 $code_block = munge_code( $code_block ) if $code_block;
117              
118             # maybe unnecessary?
119 54         72 chomp $code_block;
120              
121             # rebuild the code with our modifications...
122 54         149 $filtered_code .= $before_kw;
123              
124 54 100       131 if ( $kw eq 'try' ) {
    100          
    50          
125             # found a try block, put everything in a new scope...
126 26         31 $filtered_code .= ";{ ";
127             # wrap the try block in a do block... if we reach the end of the do block,
128             # we know return was never used in the do, so return a SENTINEL.
129 26         70 $filtered_code .= "local \$$T = sub { do $code_block; return \$$S; };";
130             }
131             elsif ( $kw eq 'catch' ) {
132 20 50       39 die "Syntax Error: Only one catch-block allowed." if $found_catch++;
133 20         49 $filtered_code .= "local \$$C = sub { do $code_block; return \$$S; };";
134             }
135             elsif ( $kw eq 'finally' ) {
136 8 50       18 die "Syntax Error: Only one finally-block allowed." if $found_finally++;
137 8         23 $filtered_code .= "local \$$F = '$G'->_new(sub $code_block, \@_); ";
138             }
139              
140             # if the remainder doesn't start with a catch or finally clause, assume
141             # that's the end and add the code that makes this monstrosity work.
142 54 100       187 if ( $remainder !~ /\A \s* ( catch | finally ) \s* [{] /msx ) {
143             # add the code all on one line to preserve the original numbering.
144 26         363 $filtered_code .=
145             # init ERROR, DIED, RETVAL, and WANTARRAY
146             "local ( \$$E, \$$D, \@$R ); local \$$W = wantarray; "
147             . "{ "
148             . "local \$@; "
149             # if an exception is thrown, value of eval will be undef, stash in DIED
150             . "\$$D = not eval { "
151             # call TRY sub in appropriate context according to value of WANTARRAY
152             # capturing the return value in RETVAL
153             . "if ( \$$W ) { \@$R = &\$$T; } elsif ( defined \$$W ) { \$$R\[0] = &\$$T; } else { &\$$T; } "
154             # return 1 if no exception is thrown
155             . "return 1; "
156             . "}; "
157             # stash any exception in ERROR
158             . "\$$E = \$@; "
159             . "}; "
160             # if DIED is true, and there's a CATCH sub, stash the ERROR in $@ and then
161             # call the CATCH sub in the apropriate context. Else, re-throw ERROR
162             . "if ( \$$D ) { "
163             . "if ( \$$C ) { "
164             . "local \$@ = \$$E; "
165             . "if ( \$$W ) { \@$R = &\$$C; } elsif ( defined \$$W ) { \$$R\[0] = &\$$C; } else { &\$$C; } "
166             . "} "
167             . "else { die \$$E } "
168             . "}; "
169             # if in current scope caller is true, and RETVAL isn't a ref or a SENTINEL, we know
170             # that return was called in the code block, so return RETVAL in the apropriate context
171             . "if ( caller() and (!ref(\$$R\[0]) or !\$$R\[0]->isa('$S')) ) { return \$$W ? \@$R : \$$R\[0]; } ";
172              
173             # close the scope opened when we first found the "try"
174 26         39 $filtered_code .= "}";
175              
176             # this try/catch/finally construct is done. reset counters.
177 26         33 $found_catch = $found_finally = 0;
178             }
179              
180             # repeat this loop on the remaining code
181 54         281 $code_to_filter = $remainder;
182             }
183              
184             # overwrite the original code with the filtered code, plus whatever was left-over
185 62         240 return $filtered_code . $code_to_filter;
186             }
187              
188              
189             {
190             package # hide from PAUSE
191             Try::Harder::ScopeGuard;
192              
193             # older versions of perl have an issue with $@ during global destruction
194 9 50   9   23673 use constant UNSTABLE_DOLLARAT => ("$]" < '5.013002') ? 1 : 0;
  9         18  
  9         1871  
195              
196             sub _new {
197 8     8   2543 shift;
198 8         21 bless [ @_ ];
199             }
200              
201             sub DESTROY {
202 8     8   199 my ($code, @args) = @{ $_[0] };
  8         24  
203             # save the current exception to make it available in the finally sub,
204             # and to restore it after the eval
205 8         15 my $err = $@;
206 8         10 local $@ if UNSTABLE_DOLLARAT;
207             eval {
208 8         11 $@ = $err;
209 8         20 $code->(@args);
210 7         25 1;
211 8 100       9 } or do {
212 1 50       19 warn
213             "Execution of finally() block $code resulted in an exception, which "
214             . '*CAN NOT BE PROPAGATED* due to fundamental limitations of Perl. '
215             . 'Your program will continue as if this event never took place. '
216             . "Original exception text follows:\n\n"
217             . (defined $@ ? $@ : '$@ left undefined...')
218             . "\n"
219             ;
220             };
221             # maybe unnecessary?
222 8         48 $@ = $err;
223             }
224             }
225              
226             1 && "This was an awful idea."; # truth
227             __END__