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   2674 use strict;
  2         5  
  2         58  
3 2     2   12 use warnings;
  2         4  
  2         53  
4 2     2   459 use parent qw[ Perl::Critic::Policy ];
  2         289  
  2         12  
5 2     2   175048 use Perl::Critic::Utils qw[ :severities :booleans ];
  2         5  
  2         100  
6 2     2   1233 use Data::Alias;
  2         1948  
  2         110  
7              
8 2     2   15 use constant PBP_PAGE => 103;
  2         5  
  2         1822  
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 337592 sub applies_to { return 'PPI::Statement::Compound' }
13              
14             sub violates {
15 21     21 1 501 my ($self, $elem, $doc) = @_;
16 21 100       69 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   1142 my ($s_doc, $s_elem) = @_;
20 135 100       402 return $TRUE if $s_elem->isa('PPI::Structure::Block');
21 120         322 return; # don't descend into other structures
22 15         936 });
23 15 50       220 return if not $block; # postfix loop
24              
25             my $iterator = $elem->find_first(sub {
26 75     75   832 my ($s_doc, $s_elem) = @_;
27 75 100       234 return $TRUE if $s_elem->isa('PPI::Token::Symbol');
28 60 50       234 die if $s_elem->isa('PPI::Structure::List'); # no iterator, halt search
29 15         80 });
30 15 50       203 return if not $iterator; # checking $_ is unreliable
31              
32 15         53 my $subscripts_ref = $block->find('PPI::Structure::Subscript');
33 15 100       33203 return if not $subscripts_ref;
34              
35 14         38 my (%used, @violations);
36 14         55 foreach my $subscript (@$subscripts_ref) {
37 60         232 my $source = $subscript->sprevious_sibling();
38 60 100 66     1800 if ($source->isa('PPI::Token::Operator') and $source eq '->') {
39 48         763 $source = $source->sprevious_sibling(); # reference subscript
40             }
41 60 50 66     1254 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       155 next if _eq_symbol($source, '$_');
46              
47             # skip delete statements since they require keys
48 59 100       827 next if _is_delete_arg($source);
49              
50 57         336 my $source_is_iterator = _eq_symbol($source, $iterator);
51 57         979 my $sub_expr = $subscript->find_first('PPI::Statement::Expression');
52 57         9873 foreach my $sub_value (_extract_values($sub_expr)) {
53 61 100       278 next if $sub_value eq '$_';
54             next # only check subscripts utilising the current iterator
55 59 100 100     818 if not $source_is_iterator
56             and not $sub_value eq $iterator;
57              
58 56         293 alias my $used_cnt = $used{$source}{$sub_value};
59 56 100 100     486 if ($used_cnt and $used_cnt > 2) {
60 10         35 my $braced = $subscript->start . $sub_value . $subscript->finish;
61 10         139 my $desc = "Subscript $braced of $source used multiple times in a block";
62 10         68 push @violations, $self->violation($desc, PBP_PAGE, $subscript);
63             }
64 56         2679 $used_cnt++;
65             }
66             }
67              
68 14         130 return @violations;
69             }
70              
71             sub _eq_symbol {
72 116     116   273 my ($elem, $symbol) = @_;
73 116 50       310 return if not $elem->isa('PPI::Token::Symbol');
74 116         292 return $elem eq $symbol;
75             }
76              
77             sub _extract_values {
78 57     57   128 my ($expr) = @_;
79              
80 57         153 my @children = $expr->children;
81 57 50       398 return if not @children;
82              
83 57 100       150 if (@children == 1) {
84 53         95 my $child = $children[0];
85 53 100       273 return $child->literal if $child->isa('PPI::Token::QuoteLike::Words');
86 52         158 return $child;
87             }
88              
89 4         13 my @values = ([]);
90 4         12 foreach my $child (@children) {
91 17 100       76 next if $child->isa('PPI::Token::Whitespace');
92 12 100 100     48 if ($child->isa('PPI::Token::Operator') and $child eq ',') {
93 3         50 push @values, [];
94 3         9 next;
95             }
96 9 100       32 push @{ $values[-1] },
  9 50       64  
97             $child->isa('PPI::Token::QuoteLike::Words') ? $child->literal
98             : $child->isa('PPI::Token::Quote') ? $child->string
99             : $child;
100             }
101 4 100       68 return map { join '', map { ref() ? $_->content : $_ } @$_ } @values;
  7         17  
  9         53  
102             }
103              
104             sub _is_delete_arg {
105 59     59   127 my ($elem) = @_;
106              
107 59         150 my $maybe_del = $elem->sprevious_sibling();
108 59 100       1251 if (not $maybe_del) { # might still be a delete() with parentheses
109 40         125 my $expr = $elem->parent();
110 40 50 33     362 return if not $expr or not $expr->isa('PPI::Statement');
111 40         111 my $parens = $expr->parent();
112 40 100 66     343 return if not $parens or not $parens->isa('PPI::Structure::List');
113              
114 14         127 $maybe_del = $parens->sprevious_sibling();
115             }
116              
117 33 50       366 return if not $maybe_del;
118 33 100       136 return if not $maybe_del->isa('PPI::Token::Word');
119 20         55 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