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   396077 use 5.006;
  40         181  
21 40     40   253 use strict;
  40         91  
  40         1158  
22 40     40   209 use warnings;
  40         96  
  40         2443  
23 40     40   299 use base 'Perl::Critic::Policy';
  40         103  
  40         6024  
24 40     40   243749 use Perl::Critic::Utils;
  40         129  
  40         976  
25              
26 40     40   70696 use Perl::Critic::Policy::CodeLayout::RequireFinalSemicolon;
  40         195  
  40         1829  
27 40     40   35891 use Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders;
  40         219  
  40         1693  
28 40     40   1011 use Perl::Critic::Policy::ValuesAndExpressions::ConstantBeforeLt;
  40         141  
  40         1515  
29 40     40   293 use Perl::Critic::Pulp::Utils 'elem_is_comma_operator';
  40         210  
  40         3236  
30              
31             # uncomment this to run the ### lines
32             #use Smart::Comments;
33              
34              
35             our $VERSION = 100;
36              
37 40     40   292 use constant supported_parameters => ();
  40         225  
  40         2928  
38 40     40   343 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         87  
  40         2852  
39 40     40   332 use constant default_themes => qw(pulp bugs);
  40         125  
  40         3186  
40 40         47333 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   334 'PPI::Structure::Block');
  40         112  
46              
47             sub violates {
48 63     63 1 1250628 my ($self, $elem, $document) = @_;
49             ### ProhibitDuplicateHashKeys violates() ...
50              
51             ### consider: (ref $elem)." $elem"
52              
53 63 100       427 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       80 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         23 return;
67             }
68              
69             } else { # PPI::Structure::List
70 43 100       935 _elem_is_assigned_to_hash($elem) || return;
71             }
72              
73 54   100     486 $elem = $elem->schild(0) || return;
74 53 50       1312 if ($elem->isa('PPI::Statement')) {
75 53   50     193 $elem = $elem->schild(0) || return;
76             }
77             ### first elem: (ref $elem)." $elem"
78              
79 53         965 my @elems = Perl::Critic::Policy::ValuesAndExpressions::ConstantBeforeLt::_elem_and_ssiblings($elem);
80             ### elems len: scalar(@elems)
81              
82 53         161 @elems = map {_expand_qw($_)} @elems;
  419         919  
83             ### expanded len: scalar(@elems)
84              
85 53         151 my $state = 'key';
86 53         588 my @violations;
87             my %seen_key;
88              
89 53         254 while (@elems) {
90             ### $state
91 228         843 my ($comma, @arg) = _take_to_comma(\@elems);
92              
93 228 100       709 if (! @arg) {
94             ### consecutive commas ...
95 2         5 next;
96             }
97              
98 226         597 $elem = $arg[0];
99             ### first of arg: (ref $elem)." $elem"
100             ### arg elem count: scalar(@arg)
101              
102 226 100 66     1949 if ($elem->isa('PPI::Token::Cast') && $elem eq '%') {
103             ### skip cast % even num elements ...
104 2         35 $state = 'key';
105 2         9 next;
106             }
107             # %$foo is an even number of things
108 224 100 100     1616 if (@arg == 1
      100        
109             && $elem->isa('PPI::Token::Symbol')
110             && $elem->raw_type eq '%') {
111             ### skip hash var even num elements ...
112 3         36 $state = 'key';
113 3         14 next;
114             }
115              
116 221 100 100     795 if ($state eq 'unknown' && $comma eq '=>') {
117 5         87 $state = 'key';
118             }
119              
120 221 100       769 if ($state eq 'key') {
    100          
121 111         225 my $str;
122             my $any_vars;
123 111 100       427 if ($elem->isa('Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword')) {
124             ### qword ...
125 5         17 $str = $elem->{'word'};
126 5         12 $any_vars = 0;
127 5         12 $elem = $elem->{'elem'};
128             } else {
129 106         457 ($str, $any_vars) = Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders::_arg_string(\@arg, $document);
130             }
131              
132             ### $str
133 111 100 66     1037 if (defined $str
      100        
134             && ! $any_vars
135             && $seen_key{$str}++) {
136             ### found duplicate ...
137 40         278 push @violations, $self->violation ("Duplicate hash key \"$str\"",
138             '',
139             $elem);
140             }
141              
142 111 100       12605 if ($any_vars >= 2) {
143             ### expression, go to unknown ...
144 5         21 $state = 'unknown';
145             } else {
146 106         544 $state = 'value';
147             }
148              
149             } elsif ($state eq 'value') {
150 106 50       442 if ($comma eq '=>') {
151             ### hmm, something like a=>b=>..., assume next is a value still ...
152 0         0 $state = 'value';
153             } else {
154 106         1347 $state = 'key';
155             }
156             }
157             }
158              
159             ### done ...
160 53         308 return @violations;
161             }
162              
163             sub _expand_qw {
164 419     419   805 my ($elem) = @_;
165 419 100       1761 if (! $elem->isa('PPI::Token::QuoteLike::Words')) {
166 416         1057 return $elem;
167             }
168 3         18 my @words = $elem->literal;
169             ### @words
170              
171             return map {
172 3         732 Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword->new
  9         32  
173             (word => $_,
174             elem => $elem);
175             } @words;
176             }
177              
178             sub _take_to_comma {
179 228     228   564 my ($aref) = @_;
180 228         469 my @ret;
181 228         732 while (@$aref) {
182 425         882 my $elem = shift @$aref;
183 425 100       2210 if ($elem->isa('Perl::Critic::Pulp::ProhibitDuplicateHashKeys::Qword')) {
184 9         21 push @ret, $elem;
185 9         35 return ',', @ret;
186             }
187 416 100       1410 if (elem_is_comma_operator($elem)) {
188 171         1458 return $elem, @ret; # found a comma
189             }
190 245         906 push @ret, $elem; # not a comma
191             }
192 48         279 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   166 my ($elem) = @_;
203             ### _elem_is_assigned_to_hash() ...
204              
205 43   50     249 $elem = $elem->sprevious_sibling || return 0;
206              
207 43 100 66     2465 ($elem->isa('PPI::Token::Operator') && $elem eq '=')
208             or return 0;
209              
210 39   50     1045 $elem = $elem->sprevious_sibling || return 0;
211             ### assign to: "$elem"
212              
213             # %{expr} = () deref
214 39 50       1452 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       176 if ($elem->isa('PPI::Token::Symbol')) {
221 39 50       178 if ($elem->symbol_type eq '%') {
222             ### yes, %foo ...
223 39         3166 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   39 my ($class, %self) = @_;
253 9         37 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