File Coverage

blib/lib/Perl/Critic/Policy/Variables/ProhibitLoopOnHash.pm
Criterion Covered Total %
statement 64 65 98.4
branch 35 38 92.1
condition 21 24 87.5
subroutine 15 16 93.7
pod 4 5 80.0
total 139 148 93.9


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::Variables::ProhibitLoopOnHash;
2             our $AUTHORITY = 'cpan:XSAWYERX';
3             # ABSTRACT: Don't write loops on hashes, only on keys and values of hashes
4             $Perl::Critic::Policy::Variables::ProhibitLoopOnHash::VERSION = '0.008';
5 1     1   273757 use strict;
  1         10  
  1         30  
6 1     1   6 use warnings;
  1         2  
  1         30  
7 1     1   6 use parent 'Perl::Critic::Policy';
  1         2  
  1         10  
8              
9 1     1   16926 use Carp qw< croak >;
  1         3  
  1         62  
10 1     1   7 use Perl::Critic::Utils qw< :severities :classification :ppi >;
  1         3  
  1         52  
11 1     1   375 use List::Util 'first';
  1         4  
  1         136  
12              
13 1     1   10 use constant 'DESC' => 'Looping over hash instead of hash keys or values';
  1         2  
  1         101  
14 1         664 use constant 'EXPL' => 'You are accidentally looping over the hash itself '
15             . '(both keys and values) '
16 1     1   7 . 'instead of only keys or only values';
  1         2  
17              
18             # \bfor(each)?(\s+my)?\s*\$\w+\s*\(\s*%
19 12     12 0 67556 sub supported_parameters { () }
20 26     26 1 281 sub default_severity { $SEVERITY_HIGH }
21 0     0 1 0 sub default_themes { 'bugs' }
22 12     12 1 210505 sub applies_to { 'PPI::Token::Word' }
23              
24             sub violates {
25 137     137 1 6815 my ($self, $elem) = @_;
26              
27 137 100   244   555 first { $elem eq $_ } qw< for foreach >
  244         1865  
28             or return ();
29              
30             # This is how we do it:
31             # * First, we clear out scoping (like "my" for "foreach my ...")
32             # * Second, we clear out topical variables ("foreach $foo (...)")
33             # * Then we check if it's a postfix without parenthesis
34             # * Lastly, we handle the remaining cases
35              
36             # Skip if we do not have the right type of PPI::Statement
37             # For example, "$var->{for}" has a PPI::Statement::Expression
38             # when leading for() is a PPI::Statement::Compound and
39             # a postfix for() is a PPI::Statement
40             # This was originally written as: $elem->snext_sibling or return
41 59 100 66     928 $elem->parent && $elem->parent->isa('PPI::Statement::Expression')
42             and return ();
43              
44             # for \my %foo
45 53 100       759 if ( !$elem->snext_sibling ) {
46 2         48 my $next = $elem->next_token;
47              
48             # exhaust spaces
49 2         113 $next = $next->next_token
50             while $next->isa('PPI::Token::Whitespace');
51              
52             # skip the \
53 2 100       79 if ( $next eq '\\' ) {
54 1         23 $elem = $next->next_token;
55             }
56             }
57              
58             # for Class->method($foo)
59             # PPI::Document
60             # PPI::Statement::Compound
61             # PPI::Token::Word 'for'
62             # PPI::Token::Whitespace ' '
63             # PPI::Statement
64             # PPI::Token::Word 'Class'
65             # PPI::Token::Operator '->'
66             # PPI::Token::Word 'method'
67             # PPI::Structure::List ( ... )
68             # PPI::Statement::Expression
69             # PPI::Token::Symbol '$foo'
70             # PPI::Token::Structure ';'
71 53 100 66     1399 if ( !$elem->snext_sibling && $elem->next_token) {
72             # exhaust spaces
73 1         65 $elem = $elem->next_token
74             while $elem->next_token->isa('PPI::Token::Whitespace');
75              
76             # just move to next token and continue from there
77 1 50       115 $elem->next_token
78             and $elem = $elem->next_token;
79             }
80              
81             # for my $foo (%hash)
82             # we simply skip the "my"
83 53 100       1219 if ( ( my $scope = $elem->snext_sibling )->isa('PPI::Token::Word') ) {
84 15 100   21   342 if ( first { $scope eq $_ } qw< my our local state > ) {
  21         115  
85             # for my Foo::Bar $baz (%hash)
86             # PPI doesn't handle this well
87             # as you can see from the following dump:
88             # PPI::Statement::Compound
89             # PPI::Token::Word 'for'
90             # PPI::Token::Whitespace ' '
91             # PPI::Token::Word 'my'
92             # PPI::Token::Whitespace ' '
93             # PPI::Statement
94             # PPI::Token::Word 'Foo::BAR'
95             # PPI::Token::Whitespace ' '
96             # PPI::Token::Symbol '$payment'
97             # PPI::Token::Whitespace ' '
98             # PPI::Structure::List ( ... )
99             # PPI::Statement::Expression
100             # PPI::Token::Symbol '@bar'
101             # PPI::Token::Whitespace ' '
102             # PPI::Structure::Block { ... }
103             # PPI::Token::Whitespace ' '
104              
105             # First, we need to exhaust spaces
106 13         194 my $next = $scope;
107 13         50 $next = $next->next_token
108             while $next->next_token->isa('PPI::Token::Whitespace');
109              
110             # Then we can use 'next_token' to jump to the next one,
111             # even if it's not a sibling
112 13         1135 $elem = $next->next_token;
113              
114             # And if it's a variable attribute, we skip it
115 13 100       376 $elem->isa('PPI::Token::Word')
116             and $elem = $elem->snext_sibling;
117             } else {
118             # for keys %hash
119             # for Class->method($foo)
120             }
121             }
122              
123             # for $foo (%hash)
124             # we simply skip the "$foo"
125 53 100       1064 if ( ( my $topical = $elem->snext_sibling )->isa('PPI::Token::Symbol') ) {
126 21 100 100     484 if ( $topical->snext_sibling
127             && $topical->snext_sibling->isa('PPI::Structure::List') )
128             {
129 7         324 $elem = $topical;
130             } else {
131             # for $foo (%hash);
132             }
133             }
134              
135             # for %hash
136             # (postfix without parens)
137 53 100       1499 _check_symbol_or_cast( $elem->snext_sibling )
138             and return $self->violation( DESC(), EXPL(), $elem );
139              
140             # for (%hash)
141 40 100       577 if ( ( my $list = $elem->snext_sibling )->isa('PPI::Structure::List') ) {
142 30         792 my @children = $list->schildren;
143 30 50       279 @children > 1
144             and croak "List has multiple significant children ($list)";
145              
146 30 50       103 if ( ( my $statement = $children[0] )->isa('PPI::Statement') ) {
147 30         86 my @statement_args = $statement->schildren;
148              
149 30 100       312 _check_symbol_or_cast( $statement_args[0] )
150             and return $self->violation( DESC(), EXPL(), $statement );
151             }
152             }
153              
154 27         330 return ();
155             }
156              
157             sub _check_symbol_or_cast {
158 83     83   1191 my $arg = shift;
159              
160             # This is either a variable
161             # or casting from a variable (or from a statement)
162 83 100 100     367 $arg->isa('PPI::Token::Symbol') && $arg =~ /^%/xms
      66        
      100        
163             or $arg->isa('PPI::Token::Cast') && $arg eq '%'
164             or return;
165              
166 29         382 my $next_op = $arg->snext_sibling;
167              
168             # If this is a cast, we want to exhaust the block
169             # the block could include anything, really...
170 29 100 100     714 if ( $arg->isa('PPI::Token::Cast') && $next_op->isa('PPI::Structure::Block') ) {
171 9         31 $next_op = $next_op->snext_sibling;
172             }
173              
174             # Safe guard against operators
175             # for ( %hash ? ... : ... );
176 29 100 100     296 $next_op && $next_op->isa('PPI::Token::Operator')
177             and return;
178              
179 26         125 return 1;
180             }
181              
182             1;
183              
184             __END__
185              
186             =pod
187              
188             =encoding UTF-8
189              
190             =head1 NAME
191              
192             Perl::Critic::Policy::Variables::ProhibitLoopOnHash - Don't write loops on hashes, only on keys and values of hashes
193              
194             =head1 VERSION
195              
196             version 0.008
197              
198             =head1 DESCRIPTION
199              
200             When "looping over hashes," we mean looping over hash keys or hash values. If
201             you forgot to call C<keys> or C<values> you will accidentally loop over both.
202              
203             foreach my $foo (%hash) {...} # not ok
204             action() for %hash; # not ok
205             foreach my $foo ( keys %hash ) {...} # ok
206             action() for values %hash; # ok
207              
208             An effort is made to detect expressions:
209              
210             action() for %hash ? keys %hash : (); # ok
211             action() for %{ $hash{'stuff'} } ? keys %{ $hash{'stuff'} } : (); # ok
212              
213             (Granted, the second example there doesn't make much sense, but I have found
214             a variation of it in real code.)
215              
216             =head1 CONFIGURATION
217              
218             This policy is not configurable except for the standard options.
219              
220             =head1 AUTHOR
221              
222             Sawyer X, C<xsawyerx@cpan.org>
223              
224             =head1 THANKS
225              
226             Thank you to Ruud H.G. Van Tol.
227              
228             =head1 SEE ALSO
229              
230             L<Perl::Critic>
231              
232             =head1 AUTHOR
233              
234             Sawyer X
235              
236             =head1 COPYRIGHT AND LICENSE
237              
238             This software is copyright (c) 2018 by Sawyer X.
239              
240             This is free software; you can redistribute it and/or modify it under
241             the same terms as the Perl 5 programming language system itself.
242              
243             =cut