File Coverage

blib/lib/Filter/signatures.pm
Criterion Covered Total %
statement 68 98 69.3
branch 14 28 50.0
condition 5 20 25.0
subroutine 8 10 80.0
pod 0 3 0.0
total 95 159 59.7


line stmt bran cond sub pod time code
1             package Filter::signatures;
2 8     8   155314 use strict;
  8         28  
  8         256  
3 8     8   4316 use Filter::Simple;
  8         168459  
  8         59  
4              
5             our $VERSION = '0.17';
6              
7             =head1 NAME
8              
9             Filter::signatures - very simplistic signatures for Perl < 5.20
10              
11             =head1 SYNOPSIS
12              
13             use Filter::signatures;
14             no warnings 'experimental::signatures'; # does not raise an error
15             use feature 'signatures'; # this now works on <5.20 as well
16              
17             sub hello( $name ) {
18             print "Hello $name\n";
19             }
20              
21             hello("World");
22              
23             sub hello2( $name="world" ) {
24             print "Hello $name\n";
25             }
26             hello2(); # Hello world
27              
28             =head1 DESCRIPTION
29              
30             This module implements a backwards compatibility shim for formal Perl subroutine
31             signatures that were introduced to the Perl core with Perl 5.20.
32              
33             =head1 CAVEATS
34              
35             The technique used is a very simplistic transform to allow for using very
36             simplistic named formal arguments in subroutine declarations. This module
37             does not implement warning if more or fewer parameters than expected are
38             passed in.
39              
40             The module also implements default values for unnamed parameters by
41             splitting the formal parameters on C<< /,/ >> and assigning the values
42             if C<< @_ >> contains fewer elements than expected. Function calls
43             as default values may work by accident. Commas within default values happen
44             to work due to the design of L, which removes them for
45             the application of this filter.
46              
47             =head2 Syntax peculiarities
48              
49             Note that this module inherits all the bugs of L and potentially
50             adds some of its own.
51              
52             =head3 Slashes
53              
54             Most notable is that Filter::Simple sometimes will
55             misinterpret the division operator C<< / >> as a leading character to starting
56             a regex match:
57              
58             my $wait_time = $needed / $supply;
59              
60             This will manifest itself through syntax errors appearing where everything
61             seems in order. The hotfix is to add a comment to the code that "closes"
62             the misinterpreted regular expression:
63              
64             my $wait_time = $needed / $supply; # / for Filter::Simple
65              
66             A better hotfix is to upgrade to Perl 5.20 or higher and use the native
67             signatures support there. No other code change is needed, as this module will
68             disable its functionality when it is run on a Perl supporting signatures.
69              
70             =head3 Size operator interpreted as replacement
71              
72             Filter::Simple sometimes will
73             misinterpret the file size operator on the default filehandle C<< -s _ >>
74             as the start of a replacement
75              
76             my $filesize = -s _;
77              
78             # Misinterpreted as
79              
80             my $filesize = -(s _;..._g);
81              
82             This will manifest itself through syntax errors appearing where everything
83             seems in order. The hotfix is to indicate that C<<_>> is a filehandle by
84             prefixing it with C<<*>>:
85              
86             my $filesize = -s *_;
87              
88             A better hotfix is to upgrade to Perl 5.20 or higher and use the native
89             signatures support there. No other code change is needed, as this module will
90             disable its functionality when it is run on a Perl supporting signatures.
91              
92             =head2 Parentheses in default expressisons
93              
94             Ancient versions of Perl before version 5.10 do not have recursive regular
95             expressions. These will not be able to properly handle statements such
96             as
97              
98             sub foo ($timestamp = time()) {
99             }
100              
101             The hotfix is to rewrite these function signatures to not use parentheses. The
102             better approach is to upgrade to Perl 5.20 or higher.
103              
104             =head2 Regular expression matches in default expressions
105              
106             To keep the argument parser simple, the parsing of regular expressions has been
107             omitted. For Perl below 5.10, you cannot use regular expressions as default
108             expressions. For higher Perl versions, this means that parentheses, curly braces
109             and commas need to be explicitly escaped with a backslash when used as
110             default expressions:
111              
112             sub foo( $x = /,/ ) { # WRONG!
113             sub foo( $x = /\,/ ) { # GOOD!
114              
115             sub foo( $x = /[(]/ ) { # WRONG!
116             sub foo( $x = /[\(]/ ) { # GOOD!
117              
118             The hotfix is to rewrite these default expressions with explicitly quoted
119             commas, parentheses and curly braces. The better approach is to upgrade to
120             Perl 5.20 or higher.
121              
122             =head2 Subroutine attributes
123              
124             Subroutine attributes are currently not supported at all.
125              
126             =head2 Line Numbers
127              
128             Due to a peculiarity of how Filter::Simple treats here documents in some
129             versions, line numbers may get out of sync if you use here documents.
130              
131             If you spread your formal signatures across multiple lines, the line numbers
132             may also go out of sync with the original document.
133              
134             =head2 C<< eval >>
135              
136             L does not trigger when using
137             code such as
138              
139             eval <<'PERL';
140             use Filter::signatures;
141             use feature 'signatures';
142              
143             sub foo (...) {
144             }
145             PERL
146              
147             So, creating subroutines with signatures from strings won't work with
148             this module. The workaround is to upgrade to Perl 5.20 or higher.
149              
150             =head2 Deparsing
151              
152             The generated code does not deparse identically to the code generated on a
153             Perl with native support for signatures.
154              
155             =head1 ENVIRONMENT
156              
157             If you want to force the use of this module even under versions of
158             Perl that have native support for signatures, set
159             C<< $ENV{FORCE_FILTER_SIGNATURES} >> to a true value before the module is
160             imported.
161              
162             =cut
163              
164             my $have_signatures = eval {
165             require feature;
166             feature->import('signatures');
167             1
168             };
169              
170             sub kill_comment {
171 76     76 0 136 my( $str ) = @_;
172 76         226 my @strings = ($str =~ /$Filter::Simple::placeholder/g);
173 76         143 for my $ph (@strings) {
174 7         36 my $index = unpack('N',$ph);
175 7 100 100     38 if( ref $Filter::Simple::components[$index] and ${ $Filter::Simple::components[$index] } =~ /^#/ ) {
  6         31  
176             #warn ">> $str contains comment ${$Filter::Simple::components[$index]}";
177 4         46 $str =~ s!\Q$;$ph$;\E!!g;
178             };
179             }
180             $str
181 76         221 }
182              
183             sub parse_argument_list {
184 45     45 0 144 my( $name, $arglist, $whitespace ) = @_;
185 45         118 (my $args=$arglist) =~ s!^\(\s*(.*)\s*\)!$1!s;
186              
187 45         80 my @args;
188             # A not so simple argument parser, but still good enough for < 5.10:
189             # We want to split on the outermost commas, so we find the position of these
190             # commas by replacing everything inside parentheses and curly brackets with
191             # whitespace. Then we have the positions of the relevant commas and can extract
192             # the arguments from that. Not elegant but works everywhere:
193 45 100       116 if( length $args ) {
194 41         67 my $splitlist = $args;
195 41         87 my $repl = " " x length $;;
196 41         220 $splitlist =~ s!\Q$;\E.{4}\Q$;\E!$repl $repl!sg; # remove all string placeholders
197 41         145 1 while ($splitlist =~ s!\\.! !g); # unquote all the things
198             #warn $splitlist;
199 41         173 1 while ($splitlist =~ s!(\([^(){}]*\)|\{[^(){}]*\})!" " x length($1)!ge); # Now, remove all nested parentheses stuff
  12         72  
200             #warn $splitlist;
201 41         68 my @argument_positions;
202 41         175 while( $splitlist =~ /,/g ) {
203 35         148 push @argument_positions, pos($splitlist);
204             };
205 41         113 push @argument_positions, length( $splitlist )+1;
206 41         59 my $lastpos = 0;
207 76         162 @args = map { kill_comment($_) } map { s!^\s*!!; s!\s*$!!; $_}
  76         205  
  76         297  
  76         158  
208 41         86 map { my $r = substr $args, $lastpos, $_-$lastpos-1;
  76         152  
209             #warn "$lastpos:$_:$r";
210 76         117 $lastpos=$_;
211 76         166 $r
212             } @argument_positions
213             ;
214             };
215 45         82 my $res;
216             # Adjust how many newlines we gobble
217 45   100     102 $whitespace ||= '';
218             #warn "[[$whitespace$args]]";
219 45         145 my $padding = () = (($whitespace . $args) =~ /\n/smg);
220 45 100       112 if( @args ) {
221 41         59 my @defaults;
222 41         124 for( 0..$#args ) {
223             # Keep everything on one line
224 76         141 $args[$_] =~ s/\n/ /g;
225              
226             # Named argument with default
227 76 100       330 if( $args[$_] =~ /^\s*([\$\%\@]\s*\w+)\s*=/ ) {
    100          
    100          
    50          
228 33         71 my $named = "$1";
229 33         123 push @defaults, "$args[$_] if \@_ <= $_;";
230 33         85 $args[$_] = $named;
231              
232             # Named argument
233             } elsif( $args[$_] =~ /^\s*([\$\%\@]\s*\w+)\s*$/ ) {
234 38         85 my $named = "$1";
235 38         78 $args[$_] = $named;
236              
237             # Slurpy discard
238             } elsif( $args[$_] =~ /^\s*\$\s*$/ ) {
239 3         9 $args[$_] = 'undef';
240              
241             # Slurpy discard (at the end)
242             } elsif( $args[$_] =~ /^\s*[\%\@]\s*$/ ) {
243 2         6 $args[$_] = 'undef';
244             } else {
245             #use Data::Dumper;
246             #warn Dumper \@Filter::Simple::components;
247             #die "Weird, unparsed argument '$args[$_]'";
248             };
249              
250             };
251              
252             # Make sure we return undef as the last statement of our initialization
253             # See t/07*
254 41 50       114 push @defaults, "();" if @args;
255              
256 41         241 $res = sprintf 'sub %s { my (%s)=@_;%s%s', $name, join(",", @args), join( "" , @defaults), "\n" x $padding;
257             # die sprintf("Too many arguments for subroutine at %s line %d.\n", (caller)[1, 2]) unless @_ <= 2
258             # die sprintf("Too few arguments for subroutine at %s line %d.\n", (caller)[1, 2]) unless @_ >= 2
259             } else {
260 4         21 $res = sprintf 'sub %s { @_==0 or warn "Subroutine %s called with parameters.";();', $name, $name;
261             };
262              
263 45         1156 return $res
264             }
265              
266             # This is the version that is most downwards compatible but doesn't handle
267             # parentheses in default assignments
268             sub transform_arguments {
269             # This should also support
270             # sub foo($x,$y,@) { ... }, throwing away additional arguments
271             # Named or anonymous subs
272 8     8   7971 no warnings 'uninitialized';
  8         32  
  8         1406  
273             s{\bsub(\s*)(\w*)(\s*)\((\s*)((?:[^)]*?\@?))(\s*)\)(\s*)\{}{
274             parse_argument_list("$2","$5","$1$3$4$6$7")
275             }mge;
276             $_
277             }
278              
279             if( $] >= 5.010 ) {
280             # Perl 5.10 onwards has recursive regex patterns, and comments, and stuff
281              
282             # We have an interesting dependency on the format the string placeholders that
283             # Filter::Simple supplies. They MUST be four characters wide.
284 8     8   64 no warnings 'redefine';
  8         18  
  8         4959  
285 8     8 0 59 eval <<'PERL_5010_onwards';
  8     42   19  
  8         1637  
  42         103767  
  44         381  
  42         145  
286             sub transform_arguments {
287             # We also want to handle arbitrarily deeply nested balanced parentheses here
288             no warnings 'uninitialized';
289             # If you are staring at this, somewhere in your source code, you have
290             # $/ and you want to make sure there is a second slash on the same line,
291             # like `local $/; # / for Filter::signatures`
292             # Or "-s _" , this also trips up Filter::Simple. Replace by "-s *_"
293             #my $msg = $_;
294             #$msg =~ s!([\x00-\x09\x0b-\x1F])!sprintf "\\%03o", ord $1!ge;
295             #print "$msg\n---\n";
296             #use Regexp::Debugger;
297             s{(?\bsub\b) #1
298             (?>(\s*)) #2
299             (?>(\b\w+\b|)) #3
300             (\s*) #4
301             \(
302             (\s*) #5
303             ( #6
304             ( #7
305             (?:
306             \\. # regex escapes and references
307             |
308             (?>".{5}") # strings (that are placeholders)
309             |
310             (?>"[^"]+") # strings (that are not placeholders, mainly for the test suite)
311             |
312             \(
313             (?7)? # recurse for parentheses
314             \)
315             |
316             \{
317             (?7)? # recurse for curly brackets
318             \}
319             |
320             (?>[^\\\(\)\{\}"]+) # other stuff
321             )+
322             )*
323             \@? # optional slurpy discard argument at the end
324             )
325             (\s*)\)
326             (\s*)\{}{
327             parse_argument_list("$3","$6","$2$4$5$9$10")
328             }mgex;
329             $_
330             }
331             PERL_5010_onwards
332             die $@ if $@;
333             }
334              
335             sub import {
336 0     0     my( $class, $scope ) = @_;
337             # Guard against double-installation of our scanner
338 0 0 0       if( $scope and $scope eq 'global' ) {
339              
340 0           my $scan; $scan = sub {
341 0     0     my( $self, $filename ) = @_;
342              
343             # Find the filters/directories that are still applicable:
344 0           my $idx = 0;
345 0   0       $idx++ while ((!ref $INC[$idx] or $INC[$idx] != $scan) and $idx < @INC);
      0        
346 0           $idx++;
347              
348 0           my @found;
349 0           foreach my $prefix (@INC[ $idx..$#INC ]) {
350 0 0         if (ref($prefix) eq 'CODE') {
351             #... do other stuff - see text below ....
352 0           @found = $prefix->( $self, $filename );
353 0 0         if( @found ) { # we found the module
354 0           last;
355             };
356             } else {
357 0           my $realfilename = "$prefix/$filename";
358 0 0 0       next if ! -e $realfilename || -d _ || -b _;
      0        
359              
360 0 0         open my $fh, '<', $realfilename
361             or die "Couldn't read '$realfilename': $!";
362 0           @found = (undef, $fh);
363             };
364             };
365 0 0         if( !ref $found[0] ) {
366 0           $found[0] = \(my $buf = "");
367             };
368 0           ${$found[0]} .= do { local $/; my $fh = $found[1]; my $content = <$fh>; $content };
  0            
  0            
  0            
  0            
  0            
369              
370             # Prepend usages of "feature" with our filter
371 0           ${$found[0]} =~ s!\b(use\s+feature\s+(['"])signatures\2)!use Filter::signatures;\n$1!gs;
  0            
372              
373             return @found
374 0           };
  0            
375             # We need to run as early as possible to filter other modules
376 0           unshift @INC, $scan;
377             };
378             }
379              
380             if( (! $have_signatures) or $ENV{FORCE_FILTER_SIGNATURES} ) {
381             FILTER_ONLY
382             code_no_comments => \&transform_arguments,
383             executable => sub {
384             s!^\s*(use\s+feature\s*(['"])signatures\2;)!#$1!mg;
385             s!^\s*(no\s+warnings\s*(['"])experimental::signatures\2;)!#$1!mg;
386             },
387             ;
388             # Set up a fake 'experimental::signatures' warnings category
389             { package # hide from CPAN
390             experimental::signatures;
391             eval {
392             require warnings::register;
393             warnings::register->import();
394             }
395             }
396              
397             }
398              
399             1;
400              
401             =head1 USAGE WITHOUT SOURCE CODE MODIFICATION
402              
403             If you have a source file that was written for use with signatures and you
404             cannot modify that source file, you can run it as follows:
405              
406             perl -Mlib=some/directory -MFilter::signatures=global myscript.pl
407              
408             This is intended as a quick-fix solution and is not very robust. If your
409             script modifies C<@INC>, the filtering may not get a chance to modify
410             the source code of the loaded module.
411              
412             This currently does not play well with (other) hooks in C<@INC> as it
413             only handles hooks that return a filehandle. Implementations for the
414             rest are welcome.
415              
416             =head1 SEE ALSO
417              
418             L
419              
420             L, which transforms your source code directly between
421             the different notations without employing a source filter
422              
423             L - a module that doesn't use a source filter but optree
424             modification instead
425              
426             L - uses signatures to dispatch to different subroutines
427             based on which subroutine matches the signature
428              
429             L - this module implements subroutine signatures
430             closer to Perl 6, but requires L and L
431              
432             L - adds two new keywords for declaring subroutines and
433             parses their signatures. It supports more features than core Perl, closer to
434             Perl 6, but requires a C compiler and Perl 5.14+.
435              
436             =head1 REPOSITORY
437              
438             The public repository of this module is
439             L.
440              
441             =head1 SUPPORT
442              
443             The public support forum of this module is
444             L.
445              
446             =head1 BUG TRACKER
447              
448             Please report bugs in this module via the RT CPAN bug queue at
449             L
450             or via mail to L.
451              
452             =head1 AUTHOR
453              
454             Max Maischein C
455              
456             =head1 COPYRIGHT (c)
457              
458             Copyright 2015-2020 by Max Maischein C.
459              
460             =head1 LICENSE
461              
462             This module is released under the same terms as Perl itself.
463              
464             =cut