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, 2019, 2020, 2021 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   37428 use 5.006;
  40         170  
21 40     40   238 use strict;
  40         101  
  40         989  
22 40     40   221 use warnings;
  40         98  
  40         1364  
23 40     40   253 use base 'Perl::Critic::Policy';
  40         96  
  40         4912  
24 40     40   184245 use Perl::Critic::Utils;
  40         107  
  40         936  
25              
26 40     40   38360 use Perl::Critic::Policy::CodeLayout::RequireFinalSemicolon;
  40         125  
  40         1322  
27 40     40   22165 use Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders;
  40         196  
  40         1425  
28 40     40   880 use Perl::Critic::Policy::ValuesAndExpressions::ConstantBeforeLt;
  40         189  
  40         1276  
29 40     40   327 use Perl::Critic::Pulp::Utils 'elem_is_comma_operator';
  40         282  
  40         3044  
30              
31             # uncomment this to run the ### lines
32             #use Smart::Comments;
33              
34              
35             our $VERSION = 99;
36              
37 40     40   313 use constant supported_parameters => ();
  40         252  
  40         2380  
38 40     40   307 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         212  
  40         2209  
39 40     40   316 use constant default_themes => qw(pulp bugs);
  40         100  
  40         2506  
40 40         37706 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   321 'PPI::Structure::Block');
  40         122  
46              
47             sub violates {
48 63     63 1 870795 my ($self, $elem, $document) = @_;
49             ### ProhibitDuplicateHashKeys violates() ...
50              
51             ### consider: (ref $elem)." $elem"
52              
53 63 100       259 if ($elem->isa('PPI::Structure::Constructor')) {
    100          
54             ### constructor ...
55 9 50       31 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       51 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         16 return;
67             }
68              
69             } else { # PPI::Structure::List
70 43 100       629 _elem_is_assigned_to_hash($elem) || return;
71             }
72              
73 54   100     372 $elem = $elem->schild(0) || return;
74 53 50       903 if ($elem->isa('PPI::Statement')) {
75 53   50     147 $elem = $elem->schild(0) || return;
76             }
77             ### first elem: (ref $elem)." $elem"
78              
79 53         747 my @elems = Perl::Critic::Policy::ValuesAndExpressions::ConstantBeforeLt::_elem_and_ssiblings($elem);
80             ### elems len: scalar(@elems)
81              
82 53         131 @elems = map {_expand_qw($_)} @elems;
  419         735  
83             ### expanded len: scalar(@elems)
84              
85 53         120 my $state = 'key';
86 53         98 my @violations;
87             my %seen_key;
88              
89 53         136 while (@elems) {
90             ### $state
91 228         600 my ($comma, @arg) = _take_to_comma(\@elems);
92              
93 228 100       639 if (! @arg) {
94             ### consecutive commas ...
95 2         6 next;
96             }
97              
98 226         444 $elem = $arg[0];
99             ### first of arg: (ref $elem)." $elem"
100             ### arg elem count: scalar(@arg)
101              
102 226 100 66     867 if ($elem->isa('PPI::Token::Cast') && $elem eq '%') {
103             ### skip cast % even num elements ...
104 2         28 $state = 'key';
105 2         7 next;
106             }
107             # %$foo is an even number of things
108 224 100 100     1208 if (@arg == 1
      100        
109             && $elem->isa('PPI::Token::Symbol')
110             && $elem->raw_type eq '%') {
111             ### skip hash var even num elements ...
112 3         28 $state = 'key';
113 3         11 next;
114             }
115              
116 221 100 100     657 if ($state eq 'unknown' && $comma eq '=>') {
117 5         65 $state = 'key';
118             }
119              
120 221 100       653 if ($state eq 'key') {
    100          
121 111         221 my $str;
122             my $any_vars;
123 111 100       343 if ($elem->isa('Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword')) {
124             ### qword ...
125 5         15 $str = $elem->{'word'};
126 5         8 $any_vars = 0;
127 5         11 $elem = $elem->{'elem'};
128             } else {
129 106         359 ($str, $any_vars) = Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders::_arg_string(\@arg, $document);
130             }
131              
132             ### $str
133 111 100 66     700 if (defined $str
      100        
134             && ! $any_vars
135             && $seen_key{$str}++) {
136             ### found duplicate ...
137 40         203 push @violations, $self->violation ("Duplicate hash key \"$str\"",
138             '',
139             $elem);
140             }
141              
142 111 100       8949 if ($any_vars >= 2) {
143             ### expression, go to unknown ...
144 5         19 $state = 'unknown';
145             } else {
146 106         355 $state = 'value';
147             }
148              
149             } elsif ($state eq 'value') {
150 106 50       245 if ($comma eq '=>') {
151             ### hmm, something like a=>b=>..., assume next is a value still ...
152 0         0 $state = 'value';
153             } else {
154 106         897 $state = 'key';
155             }
156             }
157             }
158              
159             ### done ...
160 53         212 return @violations;
161             }
162              
163             sub _expand_qw {
164 419     419   794 my ($elem) = @_;
165 419 100       1225 if (! $elem->isa('PPI::Token::QuoteLike::Words')) {
166 416         874 return $elem;
167             }
168 3         15 my @words = $elem->literal;
169             ### @words
170              
171             return map {
172 3         212 Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword->new
  9         24  
173             (word => $_,
174             elem => $elem);
175             } @words;
176             }
177              
178             sub _take_to_comma {
179 228     228   475 my ($aref) = @_;
180 228         415 my @ret;
181 228         526 while (@$aref) {
182 425         766 my $elem = shift @$aref;
183 425 100       1447 if ($elem->isa('Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword')) {
184 9         17 push @ret, $elem;
185 9         28 return ',', @ret;
186             }
187 416 100       1053 if (elem_is_comma_operator($elem)) {
188 171         1110 return $elem, @ret; # found a comma
189             }
190 245         750 push @ret, $elem; # not a comma
191             }
192 48         161 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   102 my ($elem) = @_;
203             ### _elem_is_assigned_to_hash() ...
204              
205 43   50     163 $elem = $elem->sprevious_sibling || return 0;
206              
207 43 100 66     1671 ($elem->isa('PPI::Token::Operator') && $elem eq '=')
208             or return 0;
209              
210 39   50     762 $elem = $elem->sprevious_sibling || return 0;
211             ### assign to: "$elem"
212              
213             # %{expr} = () deref
214 39 50       1126 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       131 if ($elem->isa('PPI::Token::Symbol')) {
221 39 50       134 if ($elem->symbol_type eq '%') {
222             ### yes, %foo ...
223 39         2107 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   31 my ($class, %self) = @_;
253 9         31 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. As runtime behaviour, this is good since you can give defaults
288             which 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, 2019, 2020, 2021 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