File Coverage

blib/lib/Perl/Critic/Policy/ControlStructures/ProhibitMultipleSubscripts.pm
Criterion Covered Total %
statement 86 87 98.8
branch 46 56 82.1
condition 16 21 76.1
subroutine 14 15 93.3
pod 4 4 100.0
total 166 183 90.7


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::ControlStructures::ProhibitMultipleSubscripts;
2 2     2   3554 use strict;
  2         7  
  2         116  
3 2     2   13 use warnings;
  2         4  
  2         145  
4 2     2   15 use parent qw[ Perl::Critic::Policy ];
  2         4  
  2         15  
5 2     2   161 use Perl::Critic::Utils qw[ :severities :booleans ];
  2         23  
  2         220  
6 2     2   1952 use Data::Alias;
  2         2972  
  2         211  
7              
8 2     2   19 use constant PBP_PAGE => 103;
  2         6  
  2         2521  
9              
10 10     10 1 154 sub default_severity { return $SEVERITY_MEDIUM }
11 0     0 1 0 sub default_themes { return qw[ pbp maintenance ] }
12 12     12 1 3213986 sub applies_to { return 'PPI::Statement::Compound' }
13              
14             sub violates {
15 21     21 1 1414 my ($self, $elem, $doc) = @_;
16 21 100       120 return if $elem->type ne 'foreach';
17              
18             my $block = $elem->find_first(sub { # do a flat search for a PPI::Structure::Block
19 135     135   1372 my ($s_doc, $s_elem) = @_;
20 135 100       581 return $TRUE if $s_elem->isa('PPI::Structure::Block');
21 120         411 return; # don't descend into other structures
22 15         1372 });
23 15 50       337 return if not $block; # postfix loop
24              
25             my $iterator = $elem->find_first(sub {
26 75     75   1027 my ($s_doc, $s_elem) = @_;
27 75 100       387 return $TRUE if $s_elem->isa('PPI::Token::Symbol');
28 60 50       303 die if $s_elem->isa('PPI::Structure::List'); # no iterator, halt search
29 15         103 });
30 15 50       287 return if not $iterator; # checking $_ is unreliable
31              
32 15         74 my $subscripts_ref = $block->find('PPI::Structure::Subscript');
33 15 100       47846 return if not $subscripts_ref;
34              
35 14         51 my (%used, @violations);
36 14         53 foreach my $subscript (@$subscripts_ref) {
37 60         329 my $source = $subscript->sprevious_sibling();
38 60 100 66     2464 if ($source->isa('PPI::Token::Operator') and $source eq '->') {
39 48         1015 $source = $source->sprevious_sibling(); # reference subscript
40             }
41 60 50 66     1666 next if not $source->isa('PPI::Token::Symbol') # variable
42             and not $source->isa('PPI::Token::Word'); # constant
43              
44             # skip the topic variable since it can easily reference different things
45 59 50       178 next if _eq_symbol($source, '$_');
46              
47             # skip delete statements since they require keys
48 59 100       1133 next if _is_delete_arg($source);
49              
50 57         417 my $source_is_iterator = _eq_symbol($source, $iterator);
51 57         1270 my $sub_expr = $subscript->find_first('PPI::Statement::Expression');
52 57         13403 foreach my $sub_value (_extract_values($sub_expr)) {
53 61 100       385 next if $sub_value eq '$_';
54             next # only check subscripts utilising the current iterator
55 59 100 100     1100 if not $source_is_iterator
56             and not $sub_value eq $iterator;
57              
58 56         345 alias my $used_cnt = $used{$source}{$sub_value};
59 56 100 100     1769 if ($used_cnt and $used_cnt > 2) {
60 10         45 my $braced = $subscript->start . $sub_value . $subscript->finish;
61 10         220 my $desc = "Subscript $braced of $source used multiple times in a block";
62 10         134 push @violations, $self->violation($desc, PBP_PAGE, $subscript);
63             }
64 56         4784 $used_cnt++;
65             }
66             }
67              
68 14         184 return @violations;
69             }
70              
71             sub _eq_symbol {
72 116     116   290 my ($elem, $symbol) = @_;
73 116 50       422 return if not $elem->isa('PPI::Token::Symbol');
74 116         375 return $elem eq $symbol;
75             }
76              
77             sub _extract_values {
78 57     57   158 my ($expr) = @_;
79              
80 57         291 my @children = $expr->children;
81 57 50       479 return if not @children;
82              
83 57 100       1465 if (@children == 1) {
84 53         108 my $child = $children[0];
85 53 100       1415 return $child->literal if $child->isa('PPI::Token::QuoteLike::Words');
86 52         202 return $child;
87             }
88              
89 4         12 my @values = ([]);
90 4         13 foreach my $child (@children) {
91 17 100       103 next if $child->isa('PPI::Token::Whitespace');
92 12 100 100     62 if ($child->isa('PPI::Token::Operator') and $child eq ',') {
93 3         62 push @values, [];
94 3         10 next;
95             }
96 9 100       37 push @{ $values[-1] },
  9 50       76  
97             $child->isa('PPI::Token::QuoteLike::Words') ? $child->literal
98             : $child->isa('PPI::Token::Quote') ? $child->string
99             : $child;
100             }
101 4 100       39 return map { join '', map { ref() ? $_->content : $_ } @$_ } @values;
  7         17  
  9         59  
102             }
103              
104             sub _is_delete_arg {
105 59     59   158 my ($elem) = @_;
106              
107 59         192 my $maybe_del = $elem->sprevious_sibling();
108 59 100       1722 if (not $maybe_del) { # might still be a delete() with parentheses
109 40         158 my $expr = $elem->parent();
110 40 50 33     486 return if not $expr or not $expr->isa('PPI::Statement');
111 40         122 my $parens = $expr->parent();
112 40 100 66     426 return if not $parens or not $parens->isa('PPI::Structure::List');
113              
114 14         156 $maybe_del = $parens->sprevious_sibling();
115             }
116              
117 33 50       482 return if not $maybe_del;
118 33 100       170 return if not $maybe_del->isa('PPI::Token::Word');
119 20         64 return $maybe_del eq 'delete';
120             }
121              
122             1;
123             __END__
124             =pod
125              
126             =head1 NAME
127              
128             Perl::Critic::Policy::ControlStructures::ProhibitMultipleSubscripts - forbid using the same subscript multiple times in a loop
129              
130             =head1 AFFILIATION
131              
132             This policy as a part of the L<Perl::Critic::PolicyBundle::SNEZ> distribution.
133              
134             =head1 DESCRIPTION
135              
136             Conway suggests only extracting specific values of arrays and hashes in loops
137             exactly once and assigning them to variables for later access.
138             Not only does it make the code less cluttered with repeated lookups,
139             it is also more efficient in many cases.
140              
141             # Not ok
142             for my $n (0..$#clients) {
143             $clients[$n]->tally_hours();
144             $clients[$n]->bill_hours();
145             $clients[$n]->reset_hours();
146             }
147              
148             # Ok
149             for my $client (@clients) {
150             $client->tally_hours();
151             $client->bill_hours();
152             $client->reset_hours();
153             }
154              
155             # Not ok
156             for my $agent_num (0..$#operatives) { # Iterate indices
157             print "Checking agent $agent_num\n"; # Use index
158             if ($on_disavowed_list{$operatives[$agent_num]}) { # Extract value
159             print "\t...$operatives[$agent_num] disavowed!\n"; # Extract value again
160             }
161             }
162              
163             # Ok
164             for my $agent_num (0 .. $#operatives) {
165             print "Checking agent $agent_num\n";
166             my $agent = $operatives[$agent_num];
167             if ($on_disavowed_list{$agent}) {
168             print "\t...$agent disavowed!\n";
169             }
170             }
171              
172             # Not ok
173             foreach my $elem_ref (@stuff) {
174             Some::Util::foo($elem_ref->{data});
175             Some::Util::bar($elem_ref->{data});
176             }
177              
178             # Ok
179             foreach my $elem_ref (@stuff) {
180             my $data = $elem_ref->{data};
181             Some::Util::foo($data);
182             Some::Util::bar($data);
183             }
184              
185             =head1 CONFIGURATION
186              
187             This Policy is not configurable except for the standard options.
188              
189             =head1 COPYRIGHT
190              
191             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
192              
193             =cut