File Coverage

blib/lib/Perl/Critic/Policy/ValuesAndExpressions/ProhibitDuplicateHashKeys.pm
Criterion Covered Total %
statement 107 119 89.9
branch 40 54 74.0
condition 23 36 63.8
subroutine 18 18 100.0
pod 1 1 100.0
total 189 228 82.8


line stmt bran cond sub pod time code
1             # Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Kevin Ryde
2              
3             # This file is part of Perl-Critic-Pulp.
4              
5             # Perl-Critic-Pulp is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Perl-Critic-Pulp is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Perl-Critic-Pulp. If not, see <http://www.gnu.org/licenses/>.
17              
18              
19             package Perl::Critic::Policy::ValuesAndExpressions::ProhibitDuplicateHashKeys;
20 40     40   31075 use 5.006;
  40         138  
21 40     40   203 use strict;
  40         84  
  40         812  
22 40     40   185 use warnings;
  40         99  
  40         1295  
23 40     40   193 use base 'Perl::Critic::Policy';
  40         89  
  40         3942  
24 40     40   163948 use Perl::Critic::Utils;
  40         92  
  40         693  
25              
26 40     40   32923 use Perl::Critic::Policy::CodeLayout::RequireFinalSemicolon;
  40         100  
  40         999  
27 40     40   18289 use Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders;
  40         103  
  40         1190  
28 40     40   922 use Perl::Critic::Policy::ValuesAndExpressions::ConstantBeforeLt;
  40         76  
  40         983  
29 40     40   255 use Perl::Critic::Pulp::Utils 'elem_is_comma_operator';
  40         159  
  40         2363  
30              
31             # uncomment this to run the ### lines
32             #use Smart::Comments;
33              
34              
35             our $VERSION = 97;
36              
37 40     40   308 use constant supported_parameters => ();
  40         106  
  40         1884  
38 40     40   327 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         106  
  40         1903  
39 40     40   216 use constant default_themes => qw(pulp bugs);
  40         161  
  40         2126  
40 40         32590 use constant applies_to => ('PPI::Structure::Constructor',
41             'PPI::Structure::List',
42             # this policy is not for blocks, but PPI
43             # mis-reports some anonymous hashref
44             # constructors as blocks, so look at them
45 40     40   215 'PPI::Structure::Block');
  40         118  
46              
47             sub violates {
48 63     63 1 804849 my ($self, $elem, $document) = @_;
49             ### ProhibitDuplicateHashKeys violates() ...
50              
51             ### consider: (ref $elem)." $elem"
52              
53 63 100       236 if ($elem->isa('PPI::Structure::Constructor')) {
    100          
54             ### constructor ...
55 9 50       27 unless ($elem->start eq '{') {
56             ### constructor is not a hash ...
57 0         0 return;
58             }
59              
60             } elsif ($elem->isa('PPI::Structure::Block')) {
61             ### block ...
62 11 100       44 if (Perl::Critic::Policy::CodeLayout::RequireFinalSemicolon::_block_is_hash_constructor($elem) == 1) {
63             ### block is a hash, continue ...
64             } else {
65             ### block is a block, or not certain, stop ...
66 5         13 return;
67             }
68              
69             } else { # PPI::Structure::List
70 43 100       560 _elem_is_assigned_to_hash($elem) || return;
71             }
72              
73 54   100     468 $elem = $elem->schild(0) || return;
74 53 50       756 if ($elem->isa('PPI::Statement')) {
75 53   50     124 $elem = $elem->schild(0) || return;
76             }
77             ### first elem: (ref $elem)." $elem"
78              
79 53         648 my @elems = Perl::Critic::Policy::ValuesAndExpressions::ConstantBeforeLt::_elem_and_ssiblings($elem);
80             ### elems len: scalar(@elems)
81              
82 53         118 @elems = map {_expand_qw($_)} @elems;
  419         641  
83             ### expanded len: scalar(@elems)
84              
85 53         97 my $state = 'key';
86 53         95 my @violations;
87             my %seen_key;
88              
89 53         138 while (@elems) {
90             ### $state
91 228         441 my ($comma, @arg) = _take_to_comma(\@elems);
92              
93 228 100       497 if (! @arg) {
94             ### consecutive commas ...
95 2         6 next;
96             }
97              
98 226         379 $elem = $arg[0];
99             ### first of arg: (ref $elem)." $elem"
100             ### arg elem count: scalar(@arg)
101              
102 226 100 66     703 if ($elem->isa('PPI::Token::Cast') && $elem eq '%') {
103             ### skip cast % even num elements ...
104 2         32 $state = 'key';
105 2         6 next;
106             }
107             # %$foo is an even number of things
108 224 100 100     1107 if (@arg == 1
      100        
109             && $elem->isa('PPI::Token::Symbol')
110             && $elem->raw_type eq '%') {
111             ### skip hash var even num elements ...
112 3         26 $state = 'key';
113 3         10 next;
114             }
115              
116 221 100 100     550 if ($state eq 'unknown' && $comma eq '=>') {
117 5         58 $state = 'key';
118             }
119              
120 221 100       528 if ($state eq 'key') {
    100          
121 111         179 my $str;
122             my $any_vars;
123 111 100       301 if ($elem->isa('Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword')) {
124             ### qword ...
125 5         13 $str = $elem->{'word'};
126 5         10 $any_vars = 0;
127 5         8 $elem = $elem->{'elem'};
128             } else {
129 106         295 ($str, $any_vars) = Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders::_arg_string(\@arg, $document);
130             }
131              
132             ### $str
133 111 100 66     627 if (defined $str
      100        
134             && ! $any_vars
135             && $seen_key{$str}++) {
136             ### found duplicate ...
137 40         164 push @violations, $self->violation ("Duplicate hash key \"$str\"",
138             '',
139             $elem);
140             }
141              
142 111 100       7658 if ($any_vars >= 2) {
143             ### expression, go to unknown ...
144 5         13 $state = 'unknown';
145             } else {
146 106         303 $state = 'value';
147             }
148              
149             } elsif ($state eq 'value') {
150 106 50       206 if ($comma eq '=>') {
151             ### hmm, something like a=>b=>..., assume next is a value still ...
152 0         0 $state = 'value';
153             } else {
154 106         775 $state = 'key';
155             }
156             }
157             }
158              
159             ### done ...
160 53         189 return @violations;
161             }
162              
163             sub _expand_qw {
164 419     419   661 my ($elem) = @_;
165 419 100       1083 if (! $elem->isa('PPI::Token::QuoteLike::Words')) {
166 416         781 return $elem;
167             }
168 3         12 my @words = $elem->literal;
169             ### @words
170              
171             return map {
172 3         172 Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword->new
  9         22  
173             (word => $_,
174             elem => $elem);
175             } @words;
176             }
177              
178             sub _take_to_comma {
179 228     228   384 my ($aref) = @_;
180 228         316 my @ret;
181 228         447 while (@$aref) {
182 425         680 my $elem = shift @$aref;
183 425 100       1227 if ($elem->isa('Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword')) {
184 9         17 push @ret, $elem;
185 9         27 return ',', @ret;
186             }
187 416 100       914 if (elem_is_comma_operator($elem)) {
188 171         945 return $elem, @ret; # found a comma
189             }
190 245         645 push @ret, $elem; # not a comma
191             }
192 48         123 return '', @ret; # no final comma
193             }
194              
195             # $elem is any PPI::Element
196             # return true if it's assigned to a hash,
197             # %foo = ELEM
198             # %$foo = ELEM
199             # %{expr()} = ELEM
200             #
201             sub _elem_is_assigned_to_hash {
202 43     43   93 my ($elem) = @_;
203             ### _elem_is_assigned_to_hash() ...
204              
205 43   50     144 $elem = $elem->sprevious_sibling || return 0;
206              
207 43 100 66     1369 ($elem->isa('PPI::Token::Operator') && $elem eq '=')
208             or return 0;
209              
210 39   50     618 $elem = $elem->sprevious_sibling || return 0;
211             ### assign to: "$elem"
212              
213             # %{expr} = () deref
214 39 50       906 if ($elem->isa('PPI::Structure::Block')) {
215 0   0     0 $elem = $elem->sprevious_sibling || return 0;
216             ### cast hash ...
217 0   0     0 return ($elem->isa('PPI::Token::Cast') && $elem eq '%');
218             }
219              
220 39 50       115 if ($elem->isa('PPI::Token::Symbol')) {
221 39 50       116 if ($elem->symbol_type eq '%') {
222             ### yes, %foo ...
223 39         1783 return 1;
224             }
225 0 0       0 if ($elem->symbol_type eq '$') {
226             ### symbol scalar ...
227             # %$x=() or %$$$x=() deref
228 0         0 for (;;) {
229 0   0     0 $elem = $elem->sprevious_sibling || return 0;
230             ### prev: (ref $elem)." $elem"
231 0 0       0 if ($elem->isa('PPI::Token::Magic')) {
    0          
232             # PPI 1.215 mistakes %$$$r as magic variable $$
233             } elsif ($elem->isa('PPI::Token::Cast')) {
234 0 0       0 if ($elem ne '$') {
235             ### cast hash: ($elem eq '%')
236 0         0 return ($elem eq '%');
237             }
238             } else {
239 0         0 return 0;
240             }
241             }
242             }
243             }
244              
245             ### no ...
246 0         0 return 0;
247             }
248              
249             {
250             package Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword;
251             sub new {
252 9     9   26 my ($class, %self) = @_;
253 9         28 return bless \%self, $class;
254             }
255             }
256              
257             1;
258             __END__
259              
260             =for stopwords Ryde hashref runtime
261              
262             =head1 NAME
263              
264             Perl::Critic::Policy::ValuesAndExpressions::ProhibitDuplicateHashKeys - disallow duplicate literal hash keys
265              
266             =head1 DESCRIPTION
267              
268             This policy is part of the L<C<Perl::Critic::Pulp>|Perl::Critic::Pulp>
269             add-on. It reports duplicate literal hash keys in a hash assignment or
270             anonymous hashref.
271              
272             my %hash = (red => 1,
273             green => 2,
274             red => 3, # bad
275             );
276              
277             my $hashref = { red => 1,
278             red => 3, # bad
279             };
280              
281             Writing duplicate literal keys is probably a mistake or too much cut and
282             paste, and if the values are different will make it unclear to human readers
283             what was meant. On that basis this policy is under the "bugs" theme and
284             medium severity (see L<Perl::Critic/POLICY THEMES>).
285              
286             Perl is happy to run code like the above. The value of the last "red" is
287             stored. Doing this at runtime is good since you can give defaults which
288             further values from a caller or similar can replace. For example,
289              
290             sub new {
291             my $class = shift;
292             return bless { foo => 'default',
293             bar => 'default',
294             @_ }, $class;
295             }
296              
297             MyClass->new (foo => 'caller value'); # overriding 'default'
298              
299             =head2 Expressions
300              
301             Expressions within a hash list cannot be checked in general. Some
302             concatenations of literals are recognised though they're probably unusual.
303              
304             my %hash = (ab => 1,
305             'a'.'b' => 2); # bad
306              
307             my %hash = (__PACKAGE__.'a' => 1,
308             __PACKAGE__.'a' => 2); # bad
309              
310             Function calls etc within a list might return an odd or even number of
311             values. Fat commas C<=E<gt>> are taken as indicating a key when in doubt.
312              
313             my %hash = (blah() => 1, # guided by =>
314             a => 2,
315             a => 3); # bad
316              
317             my %hash = (blah(),
318             a => 2, # guided by =>
319             a => 3); # bad
320              
321             A hash substitution is always an even number of arguments,
322              
323             my %hash = (a => 1,
324             %blah, # even number
325             a => 5); # bad, duplicate
326              
327             C<qw()> words are recognised too
328              
329             my %hash = (qw(foo value1
330             foo value2)); # bad
331              
332             =head2 Disabling
333              
334             If you don't care about this you can always disable
335             C<ProhibitDuplicateHashKeys> from your F<.perlcriticrc> file in the usual
336             way (see L<Perl::Critic/CONFIGURATION>),
337              
338             [-ValuesAndExpressions::ProhibitDuplicateHashKeys]
339              
340             =head1 SEE ALSO
341              
342             L<Perl::Critic::Pulp>,
343             L<Perl::Critic>
344              
345             L<Perl::Critic::Policy::CodeLayout::RequireTrailingCommas>,
346             L<Perl::Critic::Policy::CodeLayout::RequireTrailingCommaAtNewline>
347              
348             =head1 HOME PAGE
349              
350             http://user42.tuxfamily.org/perl-critic-pulp/index.html
351              
352             =head1 COPYRIGHT
353              
354             Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Kevin Ryde
355              
356             Perl-Critic-Pulp is free software; you can redistribute it and/or modify it
357             under the terms of the GNU General Public License as published by the Free
358             Software Foundation; either version 3, or (at your option) any later
359             version.
360              
361             Perl-Critic-Pulp is distributed in the hope that it will be useful, but
362             WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
363             or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
364             more details.
365              
366             You should have received a copy of the GNU General Public License along with
367             Perl-Critic-Pulp. If not, see <http://www.gnu.org/licenses/>.
368              
369             =cut