File Coverage

blib/lib/Closure/Explicit.pm
Criterion Covered Total %
statement 60 63 95.2
branch 9 14 64.2
condition n/a
subroutine 10 11 90.9
pod 2 2 100.0
total 81 90 90.0


line stmt bran cond sub pod time code
1             package Closure::Explicit;
2             # ABSTRACT: check coderefs for unintended lexical capture
3 6     6   155786 use strict;
  6         13  
  6         200  
4 6     6   33 use warnings;
  6         12  
  6         192  
5 6     6   29 use B;
  6         9  
  6         253  
6 6     6   2797 use PadWalker qw(closed_over peek_sub peek_my);
  6         3535  
  6         442  
7 6     6   36 use Scalar::Util ();
  6         9  
  6         265  
8              
9             our $VERSION = '0.002';
10              
11             =head1 NAME
12              
13             Closure::Explicit - check coderefs for variable capture
14              
15             =head1 VERSION
16              
17             version 0.002
18              
19             =head1 SYNOPSIS
20              
21             use Closure::Explicit qw(callback);
22              
23             {
24             package Example;
25             sub new { my $class = shift; bless {}, $class }
26             sub method { my $self = shift; print "In method\n" }
27             }
28             my $self = Example->new;
29             # This will raise an exception due to the reference to $self
30             eval {
31             my $code = callback {
32             $self->method;
33             };
34             };
35             # This will not raise the exception because $self is whitelisted
36             my $code = callback {
37             $self->method;
38             } [qw($self)];
39             # This will wrap the coderef so we can pass a weakened copy of $self
40             my $code = callback {
41             my $self = shift;
42             $self->method;
43             } weaken => [qw($self)];
44              
45             =head1 DESCRIPTION
46              
47             Attempts to provide some very basic protection against unintentional
48             capturing of lexicals in a closure.
49              
50             For example, code such as the following risks creating cycles which
51             mean the top-level object is never freed:
52              
53             sub some_method {
54             my $self = shift;
55             $self->{callback} = sub { $self->other_method }
56             }
57              
58             and this can in turn lead to memory leaks.
59              
60             =head1 API STABILITY
61              
62             The main L function is not expected to change in future versions,
63             so as long as you use this:
64              
65             use Closure::Explicit qw(callback);
66              
67             to import the function into your local namespace, or fully-qualify it using
68              
69             Closure::Explicit::callback { ... }
70              
71             then you should have no problems with future versions of this module.
72              
73             However, it is highly likely that a future version will also start exporting
74             a differently-named function with a better interface.
75              
76             =cut
77              
78 6     6   2929 use parent qw(Exporter);
  6         1506  
  6         30  
79             our @EXPORT_OK = qw(callback);
80              
81             # This is not documented, because turning it off will break
82             # the weaken behaviour.
83 6 50   6   534 use constant CLOSURE_CHECKS => exists($ENV{PERL_CLOSURE_EXPLICIT_CHECKS}) ? $ENV{PERL_CLOSURE_EXPLICIT_CHECKS} : 1;
  6         10  
  6         4102  
84              
85             =head1 EXPORTS
86              
87             =cut
88              
89             =head2 callback
90              
91             Checks the given coderef for potential closure issues, raising an exception if any
92             are found and returning the coderef (or a wrapped version of it) if everything is
93             okay.
94              
95             The first parameter is the block of code to run. This is protoyped as C< & > so
96             you can replace the usual 'sub { ... }' with 'callback { ... }'. If you already
97             have a coderef, you can pass that using C< &callback($code, ...) >, but please
98             don't.
99              
100             Remaining parameters are optional - you can either pass a single array, containing
101             a list of the B of the variables that are safe to capture:
102              
103             callback { print "$x\n" } [qw($x)];
104              
105             or a list of named parameters:
106              
107             =over 4
108              
109             =item * weaken => [...] - list of B which will be copied, weakened
110             via L, then prepended to the parameter list available in @_
111             in your code block
112              
113             =item * allowed => [...] - list of B to ignore if used in the code,
114             same behaviour as passing a single arrayref
115              
116             =back
117              
118             For example, a method call might look like this:
119              
120             my $code = callback {
121             my $self = shift;
122             $self->method(@_);
123             } weaken => [qw($self)];
124              
125             although L would be a much cleaner alternative there:
126              
127             my $code = $self->curry::weak::method;
128              
129             You can mix C and C:
130              
131             my $x = 1;
132             my $code = callback {
133             shift->method(++$x);
134             } weaken => [qw($self)], allowed => [qw($x)];
135              
136             =cut
137              
138             sub callback(&;@) {
139 19     19 1 9184 if(CLOSURE_CHECKS) {
140 19         25 my $code = shift;
141 19 100       73 my %spec = (@_ > 1) ? (@_) : (allowed => shift);
142             # warn "Have " . join ',', keys %spec;
143 19 100       57 if(my @err = lint( $code => %spec )) {
144 6         83 warn "$_\n" for @err;
145 6         888 die "Had " . @err . " error(s) in closure";
146             }
147 13         63 return $code
148             } else {
149             return $_[0] unless grep $_ eq 'weaken', @_;
150             my $code = shift;
151             my %spec = @_;
152             if($spec{weaken}) {
153             my $scope = peek_my(1);
154             my @extra = map ${ $scope->{$_} }, @{$spec{weaken}};
155             Scalar::Util::weaken($_) for @extra;
156 0     0   0 return sub { $code->(@extra, @_) };
157             }
158             }
159             }
160              
161             =head2 lint
162              
163             Runs checks on the given coderef. This is used internally and not exported,
164             but if you just want to get a list of potential problems for a coderef,
165             call this:
166              
167             my @errors = lint($code, allowed => [qw($x)]);
168              
169             It's unlikely that the C parameter will work when calling this
170             function directly - this may be fixed in a future version.
171              
172             =cut
173              
174             sub lint {
175 19     19 1 30 my ($code, %spec) = @_;
176 19         79 my $cv = B::svref_2object($code);
177 19         258 my $details = sprintf '%s(%s:%d)', $cv->STASH->NAME, $cv->FILE, $cv->GV->LINE;
178              
179 19         35 my %closed = %{closed_over($code)};
  19         103  
180             my %closed_by_value = map {
181 19         45 ref($closed{$_}) eq 'REF'
182 12 100       61 ? (${$closed{$_}} => $_)
  11         63  
183             : ()
184             } keys %closed;
185              
186             # This is everything we declare in the sub
187 19         22 my @lexicals = grep !exists $closed{$_}, keys %{ peek_sub $code };
  19         110  
188              
189 19 100       59 if($spec{weaken}) {
190             # warn "weaken request: " . join ',', @{$spec{weaken}};
191 6         37 my $scope = peek_my(2);
192 6         13 my $real_code = $code;
193 6         8 my @extra = map ${ $scope->{$_} }, @{$spec{weaken}};
  6         73  
  6         12  
194 6         49 Scalar::Util::weaken($_) for @extra;
195 6     2   24 $code = $_[0] = sub { $real_code->(@extra, @_) };
  2         6  
196 6         13 shift;
197             }
198              
199             # That's it for the data collection, now run the tests
200 19         18 my @errors;
201 19         19 foreach my $var (@{$spec{declares}}) {
  19         46  
202 0 0       0 push @errors, "no $var declared in padlist" unless grep $_ eq $var, @lexicals;
203             }
204             # say " * We are capturing $_" for sort keys %closed;
205 19         33 my %allowed = map { $_ => 1 } @{$spec{allowed}};
  6         26  
  19         28  
206 19         68 push @errors, "$_ captured in closure, recommend checking for cycles" for sort grep !exists $allowed{$_}, keys %closed;
207              
208 19         22 foreach my $var (@{$spec{captures}}) {
  19         29  
209 0 0       0 push @errors, "$var captured in closure, recommend checking for cycles" if grep $_ eq $var, keys %closed;
210             }
211 19         20 push @errors, "blacklisted value found in closure: $_ ($closed_by_value{$_})" for grep exists $closed_by_value{$_}, @{$spec{values}};
  19         36  
212 19         103 return map "$details - $_", @errors;
213             }
214              
215             1;
216              
217             __END__