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   2732 use strict;
  2         5  
  2         62  
3 2     2   11 use warnings;
  2         4  
  2         55  
4 2     2   449 use parent qw[ Perl::Critic::Policy ];
  2         298  
  2         12  
5 2     2   178041 use Perl::Critic::Utils qw[ :severities :booleans ];
  2         5  
  2         102  
6 2     2   1286 use Data::Alias;
  2         1886  
  2         111  
7              
8 2     2   14 use constant PBP_PAGE => 103;
  2         5  
  2         1820  
9              
10 10     10 1 130 sub default_severity { return $SEVERITY_MEDIUM }
11 0     0 1 0 sub default_themes { return qw[ pbp maintenance ] }
12 12     12 1 335707 sub applies_to { return 'PPI::Statement::Compound' }
13              
14             sub violates {
15 21     21 1 548 my ($self, $elem, $doc) = @_;
16 21 100       71 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   1166 my ($s_doc, $s_elem) = @_;
20 135 100       424 return $TRUE if $s_elem->isa('PPI::Structure::Block');
21 120         350 return; # don't descend into other structures
22 15         935 });
23 15 50       221 return if not $block; # postfix loop
24              
25             my $iterator = $elem->find_first(sub {
26 75     75   805 my ($s_doc, $s_elem) = @_;
27 75 100       253 return $TRUE if $s_elem->isa('PPI::Token::Symbol');
28 60 50       227 die if $s_elem->isa('PPI::Structure::List'); # no iterator, halt search
29 15         72 });
30 15 50       192 return if not $iterator; # checking $_ is unreliable
31              
32 15         49 my $subscripts_ref = $block->find('PPI::Structure::Subscript');
33 15 100       32679 return if not $subscripts_ref;
34              
35 14         43 my (%used, @violations);
36 14         45 foreach my $subscript (@$subscripts_ref) {
37 60         237 my $source = $subscript->sprevious_sibling();
38 60 100 66     1822 if ($source->isa('PPI::Token::Operator') and $source eq '->') {
39 48         763 $source = $source->sprevious_sibling(); # reference subscript
40             }
41 60 50 66     1143 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       143 next if _eq_symbol($source, '$_');
46              
47             # skip delete statements since they require keys
48 59 100       797 next if _is_delete_arg($source);
49              
50 57         331 my $source_is_iterator = _eq_symbol($source, $iterator);
51 57         944 my $sub_expr = $subscript->find_first('PPI::Statement::Expression');
52 57         10066 foreach my $sub_value (_extract_values($sub_expr)) {
53 61 100       277 next if $sub_value eq '$_';
54             next # only check subscripts utilising the current iterator
55 59 100 100     804 if not $source_is_iterator
56             and not $sub_value eq $iterator;
57              
58 56         284 alias my $used_cnt = $used{$source}{$sub_value};
59 56 100 100     491 if ($used_cnt and $used_cnt > 2) {
60 10         35 my $braced = $subscript->start . $sub_value . $subscript->finish;
61 10         147 my $desc = "Subscript $braced of $source used multiple times in a block";
62 10         75 push @violations, $self->violation($desc, PBP_PAGE, $subscript);
63             }
64 56         2703 $used_cnt++;
65             }
66             }
67              
68 14         132 return @violations;
69             }
70              
71             sub _eq_symbol {
72 116     116   255 my ($elem, $symbol) = @_;
73 116 50       314 return if not $elem->isa('PPI::Token::Symbol');
74 116         273 return $elem eq $symbol;
75             }
76              
77             sub _extract_values {
78 57     57   131 my ($expr) = @_;
79              
80 57         181 my @children = $expr->children;
81 57 50       388 return if not @children;
82              
83 57 100       155 if (@children == 1) {
84 53         97 my $child = $children[0];
85 53 100       249 return $child->literal if $child->isa('PPI::Token::QuoteLike::Words');
86 52         154 return $child;
87             }
88              
89 4         12 my @values = ([]);
90 4         10 foreach my $child (@children) {
91 17 100       81 next if $child->isa('PPI::Token::Whitespace');
92 12 100 100     49 if ($child->isa('PPI::Token::Operator') and $child eq ',') {
93 3         50 push @values, [];
94 3         7 next;
95             }
96 9 100       31 push @{ $values[-1] },
  9 50       61  
97             $child->isa('PPI::Token::QuoteLike::Words') ? $child->literal
98             : $child->isa('PPI::Token::Quote') ? $child->string
99             : $child;
100             }
101 4 100       33 return map { join '', map { ref() ? $_->content : $_ } @$_ } @values;
  7         16  
  9         47  
102             }
103              
104             sub _is_delete_arg {
105 59     59   124 my ($elem) = @_;
106              
107 59         135 my $maybe_del = $elem->sprevious_sibling();
108 59 100       1178 if (not $maybe_del) { # might still be a delete() with parentheses
109 40         121 my $expr = $elem->parent();
110 40 50 33     321 return if not $expr or not $expr->isa('PPI::Statement');
111 40         100 my $parens = $expr->parent();
112 40 100 66     345 return if not $parens or not $parens->isa('PPI::Structure::List');
113              
114 14         125 $maybe_del = $parens->sprevious_sibling();
115             }
116              
117 33 50       354 return if not $maybe_del;
118 33 100       132 return if not $maybe_del->isa('PPI::Token::Word');
119 20         44 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             =head1 CONFIGURATION
142              
143             This Policy is not configurable except for the standard options.
144              
145             =head1 COPYRIGHT
146              
147             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
148              
149             =cut