File Coverage

blib/lib/Kavorka/Signature.pm
Criterion Covered Total %
statement 180 193 93.2
branch 61 82 74.3
condition 51 65 78.4
subroutine 34 34 100.0
pod 8 9 88.8
total 334 383 87.2


line stmt bran cond sub pod time code
1 38     38   474 use 5.014;
  38         85  
2 38     38   131 use strict;
  38         44  
  38         708  
3 38     38   18204 use utf8;
  38         272  
  38         157  
4 38     38   978 use warnings;
  38         49  
  38         1268  
5              
6 38     38   13781 use Kavorka::Parameter ();
  38         92  
  38         1237  
7 38     38   15783 use Kavorka::ReturnType ();
  38         69  
  38         2976  
8              
9             package Kavorka::Signature;
10              
11             our $AUTHORITY = 'cpan:TOBYINK';
12             our $VERSION = '0.037';
13             our @CARP_NOT = qw( Kavorka::Sub Kavorka );
14              
15 38     38   185 use Carp qw( croak );
  38         46  
  38         1579  
16 38     38   139 use Parse::Keyword {};
  38         42  
  38         136  
17 38     38   2717 use Parse::KeywordX;
  38         48  
  38         251  
18              
19 38     38   9761 use Moo;
  38         56  
  38         129  
20 38     38   14111 use namespace::sweep;
  38         46  
  38         128  
21              
22             has package => (is => 'ro');
23             has _is_dummy => (is => 'ro');
24             has params => (is => 'ro', default => sub { +[] });
25             has return_types => (is => 'ro', default => sub { +[] });
26             has has_invocants => (is => 'rwp', default => sub { +undef });
27             has has_named => (is => 'rwp', default => sub { +undef });
28             has has_slurpy => (is => 'rwp', default => sub { +undef });
29             has yadayada => (is => 'rwp', default => sub { 0 });
30             has parameter_class => (is => 'ro', default => sub { 'Kavorka::Parameter' });
31             has return_type_class => (is => 'ro', default => sub { 'Kavorka::ReturnType' });
32             has last_position => (is => 'lazy');
33             has args_min => (is => 'lazy');
34             has args_max => (is => 'lazy');
35             has checker => (is => 'lazy');
36             has nobble_checks => (is => 'rwp', default => sub { 0 });
37              
38             sub parse
39             {
40 188     188 1 218 my $class = shift;
41 188         2859 my $self = $class->new(@_);
42            
43 188         715 lex_read_space;
44            
45 188         180 my $found_colon = 0;
46 188         309 my $arr = $self->params;
47 188         190 my $_class = 'parameter_class';
48            
49 188 0       562 if (lex_peek(4) =~ /\A(\xE2\x86\x92|-->)/)
50             {
51 0         0 lex_read(length $1);
52 0         0 $arr = $self->return_types;
53 0         0 $_class = 'return_type_class';
54 0         0 lex_read_space;
55             }
56            
57 188         361 my $skip = 0;
58 188         389 while (lex_peek ne ')')
59             {
60 239 0       684 if (lex_peek(3) eq '...')
61             {
62 42         120 $self->_set_yadayada(1);
63 42         67 lex_read(3);
64 42         57 lex_read_space;
65 42 0 0     100 ++$skip && next if lex_peek(4) =~ /\A(\xE2\x86\x92|-->)/;
66 42 0       104 croak("After yada-yada, expected right parenthesis") unless lex_peek eq ")";
67 42         106 next;
68             }
69            
70             $skip
71 197 50       1484 ? ($skip = 0)
72             : push(@$arr, $self->$_class->parse(package => $self->package));
73 197         14748 lex_read_space;
74            
75 197         381 my $peek = lex_peek;
76 197 50 33     979 if ($found_colon and $peek eq ':')
    100          
    100          
    100          
    0          
77             {
78 0         0 croak("Cannot have two sets of invocants - unexpected colon!");
79             }
80             elsif ($peek eq ':')
81             {
82 6         10 $_->traits->{invocant} = 1 for @{$self->params};
  6         31  
83 6         9 $self->_set_has_invocants( scalar @{$self->params} );
  6         68  
84 6         11 lex_read(1);
85             }
86             elsif ($peek eq ',')
87             {
88 59         96 lex_read(1);
89             }
90             elsif ($peek eq ')')
91             {
92 129         173 last;
93             }
94             elsif (lex_peek(4) =~ /\A(\xE2\x86\x92|-->)/)
95             {
96 3         13 lex_read(length $1);
97 3         11 $arr = $self->return_types;
98 3         7 $_class = 'return_type_class';
99             }
100             else
101             {
102 38     38   25852 use Data::Dumper;
  38         46  
  38         66145  
103 0         0 print Dumper($self);
104 0         0 croak("Unexpected characters in signature (${\ lex_peek(8) })");
  0         0  
105             }
106            
107 68         175 lex_read_space;
108             }
109            
110 188         503 $self->sanity_check;
111            
112 188         418 return $self;
113             }
114              
115             # XXX - check not allowed optional parameters and named parameters in same sig
116             sub sanity_check
117             {
118 188     188 1 214 my $self = shift;
119            
120 188         180 my $has_invocants = 0;
121 188         159 my $has_slurpy = 0;
122 188         272 my $has_named = 0;
123 188 50       170 for my $p (reverse @{ $self->params or croak("Huh?") })
  188         659  
124             {
125 193 100       461 $has_named++ if $p->named;
126 193 100       439 $has_slurpy++ if $p->slurpy;
127            
128 193 100       421 if ($p->invocant) {
    50          
129 7         8 $has_invocants++;
130 7         12 next;
131             }
132             elsif ($has_invocants) {
133 0         0 $has_invocants++;
134 0         0 $p->traits->{invocant} = 1; # anything prior to an invocant is also an invocant!
135             }
136             }
137 188         361 $self->_set_has_invocants($has_invocants);
138 188         292 $self->_set_has_named($has_named);
139 188         287 $self->_set_has_slurpy($has_slurpy);
140            
141 188 50       326 croak("Cannot have more than one slurpy parameter") if $has_slurpy > 1;
142            
143 188         173 my $i = 0;
144 188         179 my $zone = 'invocant';
145 188         173 my %already;
146 188         170 for my $p (@{ $self->params })
  188         355  
147             {
148 193 100       372 my $p_type =
    100          
    100          
    100          
149             $p->invocant ? 'invocant' :
150             $p->named ? 'named' :
151             $p->slurpy ? 'slurpy' :
152             $p->optional ? 'optional' : 'positional';
153            
154 193         1079 $p->sanity_check($self);
155 193 100 100     467 $p->_set_position($i++) unless $p->invocant || $p->slurpy || $p->named;
      100        
156            
157 193         328 my $name = $p->name;
158             croak("Parameter $name occurs twice in signature")
159 193 50 66     897 if length($name) > 1 && $already{$name}++;
160            
161 193 100       344 if ($name eq '@_')
162             {
163 1 50       3 croak("Cannot have slurpy named \@_ after positional parameters") if $self->positional_params;
164 1 50       4 croak("Cannot have slurpy named \@_ after named parameters") if $self->named_params;
165             }
166            
167 193 100       356 next if $p_type eq $zone;
168            
169             # Zone transitions
170 157 100 100     1124 if ($zone eq 'invocant' || $zone eq 'positional'
    50 66        
      66        
      66        
      33        
171             and $p_type eq 'positional' || $p_type eq 'named' || $p_type eq 'slurpy' || $p_type eq 'optional')
172             {
173 149         169 $zone = $p_type;
174 149         274 next;
175             }
176             elsif ($zone eq 'optional' || $zone eq 'named'
177             and $p_type eq 'slurpy')
178             {
179 8         9 $zone = $p_type;
180 8         14 next;
181             }
182            
183 0         0 croak("Found $p_type parameter ($name) after $zone; forbidden");
184             }
185            
186 188         183 $_->sanity_check for @{ $self->return_types };
  188         468  
187            
188 188         337 ();
189             }
190              
191             sub _build_last_position
192             {
193 43     43   244 my $self = shift;
194 43         77 my ($last) = reverse( $self->positional_params );
195 43 100       428 return -1 unless $last;
196 11         88 return $last->position;
197             }
198              
199             sub injection
200             {
201 207     207 1 213 my $self = shift;
202 207         451 join q[] => (
203             $self->_injection_nobble,
204             $self->_injection_invocants,
205             $self->_injection_parameter_count,
206             $self->_injection_positional_params,
207             $self->_injection_hash_underscore,
208             $self->_injection_named_params,
209             $self->_injection_slurpy_param,
210             '();',
211             );
212             }
213              
214             our $NOBBLE = bless(do { my $x = 1; \$x }, 'Kavorka::Signature::NOBBLE');
215             sub _injection_nobble
216             {
217 207     207   186 my $self = shift;
218 207 100       778 return unless $self->nobble_checks;
219            
220 15         35 sprintf('my $____nobble_checks = (ref($_[0]) eq "Kavorka::Signature::NOBBLE") ? ${+shift} : 0;');
221             }
222              
223             sub _injection_parameter_count
224             {
225 207     207   210 my $self = shift;
226            
227 207         3323 my $min = $self->args_min;
228 207         3174 my $max = $self->args_max;
229            
230 207         310 my @lines;
231            
232 207 100 66     1738 return sprintf(
    100 100        
233             'Carp::croak("Expected %d parameter%s") if @_ != %d;',
234             $min,
235             $min==1 ? '' : 's',
236             $min,
237             ) if defined($min) && defined($max) && $min==$max;
238            
239 91 100 66     395 push @lines, sprintf(
    100          
240             'Carp::croak("Expected at least %d parameter%s") if @_ < %d;',
241             $min,
242             $min==1 ? '' : 's',
243             $min,
244             ) if defined $min && $min > 0;
245            
246 91 100       220 push @lines, sprintf(
    100          
247             'Carp::croak("Expected at most %d parameter%s") if @_ > %d;',
248             $max,
249             $max==1 ? '' : 's',
250             $max,
251             ) if defined $max;
252            
253 91         247 return @lines;
254             }
255              
256             sub _build_args_min
257             {
258 188     188   1023 my $self = shift;
259 188         398 0 + scalar grep !$_->optional, $self->positional_params;
260             }
261              
262             sub _build_args_max
263             {
264 188     188   1002 my $self = shift;
265 188 100 100     1410 return if $self->has_named || $self->has_slurpy || $self->yadayada;
      100        
266 107         172 0 + scalar $self->positional_params;
267             }
268              
269             sub _injection_hash_underscore
270             {
271 207     207   206 my $self = shift;
272            
273 207         386 my $slurpy = $self->slurpy_param;
274            
275 207 100 100     1466 if ($self->has_named
      66        
      100        
      100        
      66        
276             or $slurpy && $slurpy->name =~ /\A\%/
277             or $slurpy && $slurpy->name =~ /\A\$/ && $slurpy->type->is_a_type_of(Types::Standard::HashRef()))
278             {
279 21         378 my $ix = 1 + $self->last_position;
280 21         24 my $str;
281 21 50       45 if ($] >= 5.022)
282             {
283 21         24 my $pragma = "use warnings FATAL => qw(all);use experimental 'refaliasing';no warnings 'experimental::refaliasing';";
284 21         112 $str = sprintf(
285             'local %%_;'
286             .'{ %s '
287             .'if ($#_==%d && ref($_[%d]) eq q(HASH)) { '
288             .'\\%%_ = $_[%d]; '
289             .'} else { '
290             .'my $i = %d; '
291             .'my $slice_length = ($#_ + 1 - $i); '
292             .'if ($slice_length %% 2 != 0) { '
293             .'Carp::croak("Odd number of elements in anonymous hash");'
294             .'} '
295             .'while ($i <= $#_) { '
296             .'my $key = $_[$i]; '
297             .'\\$_{$key} = \\$_[$i+1]; '
298             .'$i += 2; '
299             .'} '
300             .'} '
301             .'};',
302             $pragma,
303             ($ix) x 4,
304             );
305             }
306             else
307             {
308 0         0 require Data::Alias;
309 0         0 $str = sprintf(
310             'local %%_; { use warnings FATAL => qw(all); Data::Alias::alias(%%_ = ($#_==%d && ref($_[%d]) eq q(HASH)) ? %%{$_[%d]} : @_[ %d .. $#_ ]) };',
311             ($ix) x 4,
312             );
313             }
314            
315 21 100 100     62 unless ($slurpy or $self->yadayada)
316             {
317 4         7 my @allowed_names = map +($_=>1), map @{$_->named_names}, $self->named_params;
  5         16  
318 4         32 $str .= sprintf(
319             '{ my %%OK = (%s); ',
320             join(q[,], map(sprintf('%s=>1,', B::perlstring $_), @allowed_names)),
321             );
322 4         6 $str .= '$OK{$_}||Carp::croak("Unknown named parameter: $_") for sort keys %_ };';
323             }
324            
325 21         49 return $str;
326             }
327            
328 186         5940 return;
329             }
330              
331             sub _injection_invocants
332             {
333 207     207   198 my $self = shift;
334 207         474 map($_->injection($self), $self->invocants);
335             }
336              
337             sub _injection_positional_params
338             {
339 207     207   202 my $self = shift;
340 207         293 map($_->injection($self), $self->positional_params);
341             }
342              
343             sub _injection_named_params
344             {
345 207     207   193 my $self = shift;
346 207         371 map($_->injection($self), $self->named_params);
347             }
348              
349             sub _injection_slurpy_param
350             {
351 207     207   182 my $self = shift;
352 207         284 map($_->injection($self), grep defined, $self->slurpy_param);
353             }
354              
355             sub named_params
356             {
357 213     213 1 180 my $self = shift;
358 213         181 grep $_->named, @{$self->params};
  213         763  
359             }
360              
361             sub positional_params
362             {
363 547     547 1 408 my $self = shift;
364 547   100     410 grep !$_->named && !$_->invocant && !$_->slurpy, @{$self->params};
  547         2164  
365             }
366              
367             sub slurpy_param
368             {
369 415     415 0 763 my $self = shift;
370 415         324 my ($s) = grep $_->slurpy, @{$self->params};
  415         949  
371 415         1956 $s;
372             }
373              
374             sub invocants
375             {
376 208     208 1 4953 my $self = shift;
377 208         173 grep $_->invocant, @{$self->params};
  208         716  
378             }
379              
380             sub check
381             {
382 7     7 1 120 my $checker = shift->checker;
383 7         140 goto $checker;
384             }
385              
386             sub _build_checker
387             {
388 2     2   15 my $self = shift;
389 2     1   4 eval sprintf(
  1     1   4  
  1         2  
  1         71  
  1         5  
  1         2  
  1         74  
390             'sub { eval { %s; 1 } }',
391             $self->injection,
392             );
393             }
394              
395             sub inline_check
396             {
397 17     17 1 15 my $self = shift;
398 17         20 my ($arr) = @_;
399            
400 17         31 my $tmp = $self->nobble_checks;
401 17         26 $self->_set_nobble_checks(0);
402            
403 17         37 my $inline = sprintf(
404             'do { local @_ = %s; eval { %s; 1 } }',
405             $arr,
406             $self->injection,
407             );
408            
409 17 50       63 $self->_set_nobble_checks($tmp) if $tmp;
410            
411 17         97 return $inline;
412             }
413              
414             1;
415              
416             __END__
417              
418             =pod
419              
420             =encoding utf-8
421              
422             =for stopwords invocant invocants lexicals unintuitive yadayada
423              
424             =head1 NAME
425              
426             Kavorka::Signature - a function signature
427              
428             =head1 DESCRIPTION
429              
430             Kavorka::Signature is a class where each instance represents a function
431             signature. This class is used to parse the function signature, and also
432             to inject Perl code into the final function.
433              
434             Instances of this class are also returned by Kavorka's function
435             introspection API.
436              
437             =head2 Introspection API
438              
439             A signature instance has the following methods. Each method
440             which returns parameters, returns an instance of
441             L<Kavorka::Parameter>.
442              
443             =over
444              
445             =item C<package>
446              
447             Returns the package name the parameter was declared in.
448              
449             =item C<params>
450              
451             Returns an arrayref of parameters.
452              
453             =item C<return_types>
454              
455             Returns an arrayref of declared return types.
456              
457             =item C<has_invocants>, C<invocants>
458              
459             Returns a boolean/list of invocant parameters.
460              
461             =item C<positional_params>
462              
463             Returns a list of positional parameters.
464              
465             =item C<has_named>, C<named_params>
466              
467             Returns a boolean/list of named parameters.
468              
469             =item C<has_slurpy>, C<slurpy>
470              
471             Returns a boolean indicating whether there is a slurpy parameter
472             in this signature / returns the slurpy parameter.
473              
474             =item C<yadayada>
475              
476             Indicates whether the yadayada operator was encountered in the
477             signature.
478              
479             =item C<last_position>
480              
481             The numeric index of the last positional parameter.
482              
483             =item C<args_min>, C<args_max>
484              
485             The minimum/maximum number of arguments expected by the function.
486             Invocants are not counted. If there are any named or slurpy arguments,
487             of the yada yada operator was used in the signature, then C<args_max>
488             will be undef.
489              
490             =item C<< check(@args) >>
491              
492             Check whether C<< @args >> (which should include any invocants) would
493             satisfy the signature.
494              
495             =item C<< checker >>
496              
497             Returns a coderef which acts like C<< check(@args) >>.
498              
499             =item C<< inline_check($varname) >>
500              
501             Returns a string of Perl code that acts like an inline check, given the
502             name of an array variable, such as C<< '@foo' >>.
503              
504             =back
505              
506             =head2 Other Methods
507              
508             =over
509              
510             =item C<parse>
511              
512             An internal method used to parse a signature. Only makes sense to use
513             within a L<Parse::Keyword> parser.
514              
515             =item C<parameter_class>
516              
517             A class to use for parameters when parsing the signature.
518              
519             =item C<return_type_class>
520              
521             A class to use for return types when parsing the signature.
522              
523             =item C<injection>
524              
525             The string of Perl code to inject for this signature.
526              
527             =item C<sanity_check>
528              
529             Tests that the signature is sane. (For example it would not be sane to
530             have a slurpy parameter prior to a positional one.)
531              
532             =back
533              
534             =head1 BUGS
535              
536             Please report any bugs to
537             L<http://rt.cpan.org/Dist/Display.html?Queue=Kavorka>.
538              
539             =head1 SEE ALSO
540              
541             L<Kavorka::Manual::API>,
542             L<Kavorka::Sub>,
543             L<Kavorka::Parameter>.
544              
545             =head1 AUTHOR
546              
547             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
548              
549             =head1 COPYRIGHT AND LICENCE
550              
551             This software is copyright (c) 2013-2014, 2017 by Toby Inkster.
552              
553             This is free software; you can redistribute it and/or modify it under
554             the same terms as the Perl 5 programming language system itself.
555              
556             =head1 DISCLAIMER OF WARRANTIES
557              
558             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
559             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
560             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
561