File Coverage

blib/lib/Perl/Critic/Policy/Variables/ProhibitLoopOnHash.pm
Criterion Covered Total %
statement 66 67 98.5
branch 38 42 90.4
condition 21 24 87.5
subroutine 15 16 93.7
pod 4 5 80.0
total 144 154 93.5


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.009';
5 1     1   367919 use strict;
  1         3  
  1         46  
6 1     1   7 use warnings;
  1         3  
  1         62  
7 1     1   6 use parent 'Perl::Critic::Policy';
  1         3  
  1         10  
8              
9 1     1   20707 use Carp qw< croak >;
  1         4  
  1         74  
10 1     1   7 use Perl::Critic::Utils qw< :severities :classification :ppi >;
  1         2  
  1         74  
11 1     1   491 use List::Util 'first';
  1         3  
  1         92  
12              
13 1     1   7 use constant 'DESC' => 'Looping over hash instead of hash keys or values';
  1         3  
  1         101  
14 1         921 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 13     13 0 2230944 sub supported_parameters { () }
20 26     26 1 190 sub default_severity { $SEVERITY_HIGH }
21 0     0 1 0 sub default_themes { 'bugs' }
22 13     13 1 246604 sub applies_to { 'PPI::Token::Word' }
23              
24             sub violates {
25 139     139 1 4770 my ($self, $elem) = @_;
26              
27 139 100   247   342 first { $elem eq $_ } qw< for foreach >
  247         1102  
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 60 100 66     598 $elem->parent && $elem->parent->isa('PPI::Statement::Expression')
42             and return ();
43              
44             # for \my %foo
45 54 100       487 if ( !$elem->snext_sibling ) {
46 2         48 my $next = $elem->next_token;
47              
48             # exhaust spaces
49 2         91 $next = $next->next_token
50             while $next->isa('PPI::Token::Whitespace');
51              
52             # skip the \
53 2 100       57 if ( $next eq '\\' ) {
54 1         15 $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 54 100 66     1088 if ( !$elem->snext_sibling && $elem->next_token) {
72             # exhaust spaces
73 1         50 $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       86 $elem->next_token
78             and $elem = $elem->next_token;
79             }
80              
81             # for my $foo (%hash)
82             # we simply skip the "my"
83 54 100       885 if ( ( my $scope = $elem->snext_sibling )->isa('PPI::Token::Word') ) {
84 16 100   22   271 if ( first { $scope eq $_ } qw< my our local state > ) {
  22         73  
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 14         122 my $next = $scope;
107 14         39 $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 14         852 $elem = $next->next_token;
113              
114             # And if it's a variable attribute, we skip it
115 14 100       293 $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 54 100       720 my $topical = $elem->snext_sibling
124             or return ();
125              
126             # for $foo (%hash)
127             # we simply skip the "$foo"
128 53 100       817 if ( $topical->isa('PPI::Token::Symbol') ) {
129 21 100 100     35 if ( $topical->snext_sibling
130             && $topical->snext_sibling->isa('PPI::Structure::List') )
131             {
132 7         239 $elem = $topical;
133 7 50       14 $topical = $elem->snext_sibling
134             or return ();
135             } else {
136             # for $foo (%hash);
137             }
138             }
139              
140             # for %hash
141             # (postfix without parens)
142 53 100       1624 _check_symbol_or_cast( $topical )
143             and return $self->violation( DESC(), EXPL(), $elem );
144              
145             # for (%hash)
146 40 100       329 if ( $topical->isa('PPI::Structure::List') ) {
147 30         131 my @children = $topical->schildren;
148 30 50       190 @children > 1
149             and croak "List has multiple significant children ($topical)";
150              
151 30 50       72 if ( ( my $statement = $children[0] )->isa('PPI::Statement') ) {
152 30         49 my @statement_args = $statement->schildren;
153              
154 30 100       210 _check_symbol_or_cast( $statement_args[0] )
155             and return $self->violation( DESC(), EXPL(), $statement );
156             }
157             }
158              
159 27         69 return ();
160             }
161              
162             sub _check_symbol_or_cast {
163 83     83   101 my $arg = shift;
164              
165             # This is either a variable
166             # or casting from a variable (or from a statement)
167 83 100 100     229 $arg->isa('PPI::Token::Symbol') && $arg =~ /^%/xms
      66        
      100        
168             or $arg->isa('PPI::Token::Cast') && $arg eq '%'
169             or return;
170              
171 29         216 my $next_op = $arg->snext_sibling;
172              
173             # If this is a cast, we want to exhaust the block
174             # the block could include anything, really...
175 29 100 100     535 if ( $arg->isa('PPI::Token::Cast') && $next_op->isa('PPI::Structure::Block') ) {
176 9         17 $next_op = $next_op->snext_sibling;
177             }
178              
179             # Safe guard against operators
180             # for ( %hash ? ... : ... );
181 29 100 100     210 $next_op && $next_op->isa('PPI::Token::Operator')
182             and return;
183              
184 26         103 return 1;
185             }
186              
187             1;
188              
189             __END__
190              
191             =pod
192              
193             =encoding UTF-8
194              
195             =head1 NAME
196              
197             Perl::Critic::Policy::Variables::ProhibitLoopOnHash - Don't write loops on hashes, only on keys and values of hashes
198              
199             =head1 VERSION
200              
201             version 0.009
202              
203             =head1 DESCRIPTION
204              
205             When "looping over hashes," we mean looping over hash keys or hash values. If
206             you forgot to call C<keys> or C<values> you will accidentally loop over both.
207              
208             foreach my $foo (%hash) {...} # not ok
209             action() for %hash; # not ok
210             foreach my $foo ( keys %hash ) {...} # ok
211             action() for values %hash; # ok
212              
213             An effort is made to detect expressions:
214              
215             action() for %hash ? keys %hash : (); # ok
216             action() for %{ $hash{'stuff'} } ? keys %{ $hash{'stuff'} } : (); # ok
217              
218             (Granted, the second example there doesn't make much sense, but I have found
219             a variation of it in real code.)
220              
221             =head1 CONFIGURATION
222              
223             This policy is not configurable except for the standard options.
224              
225             =head1 AUTHOR
226              
227             Sawyer X, C<xsawyerx@cpan.org>
228              
229             =head1 THANKS
230              
231             Thank you to Ruud H.G. Van Tol.
232              
233             =head1 SEE ALSO
234              
235             L<Perl::Critic>
236              
237             =head1 AUTHOR
238              
239             Sawyer X
240              
241             =head1 COPYRIGHT AND LICENSE
242              
243             This software is Copyright (c) 2026 by Sawyer X.
244              
245             This is free software, licensed under:
246              
247             The MIT (X11) License
248              
249             =cut