File Coverage

blib/lib/Perl/Critic/Policy/ControlStructures/ProhibitMutatingListFunctions.pm
Criterion Covered Total %
statement 96 96 100.0
branch 63 68 92.6
condition 18 23 78.2
subroutine 25 25 100.0
pod 5 6 83.3
total 207 218 94.9


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions;
2              
3 40     40   28720 use 5.010001;
  40         206  
4 40     40   288 use strict;
  40         121  
  40         948  
5 40     40   255 use warnings;
  40         101  
  40         1062  
6 40     40   221 use Readonly;
  40         148  
  40         2270  
7              
8 40     40   306 use List::SomeUtils qw( any none );
  40         134  
  40         2545  
9              
10 40         2308 use Perl::Critic::Utils qw{
11             :booleans :characters :severities :data_conversion :classification :ppi
12 40     40   339 };
  40         154  
13              
14 40     40   24921 use parent 'Perl::Critic::Policy';
  40         118  
  40         267  
15              
16             our $VERSION = '1.146';
17              
18             #-----------------------------------------------------------------------------
19              
20             Readonly::Array my @BUILTIN_LIST_FUNCS => qw( map grep );
21             Readonly::Array my @CPAN_LIST_FUNCS => _get_cpan_list_funcs();
22              
23             #-----------------------------------------------------------------------------
24              
25             sub _get_cpan_list_funcs {
26             return qw( List::Util::first ),
27 40     40   170 map { ('List::MoreUtils::'.$_, 'List::SomeUtils::'.$_) } _get_list_moreutils_funcs();
  480         1494  
28             }
29              
30             #-----------------------------------------------------------------------------
31              
32             sub _get_list_moreutils_funcs {
33 40     40   167 return qw(any all none notall true false firstidx first_index
34             lastidx last_index insert_after insert_after_string);
35             }
36              
37             #-----------------------------------------------------------------------------
38              
39             sub _is_topic {
40 220     220   348 my $elem = shift;
41 220   66     1408 return defined $elem
42             && $elem->isa('PPI::Token::Magic')
43             && $elem->content() eq q{$_}; ##no critic (InterpolationOfMetachars)
44             }
45              
46              
47             #-----------------------------------------------------------------------------
48              
49             Readonly::Scalar my $DESC => q{Don't modify $_ in list functions}; ##no critic (InterpolationOfMetachars)
50             Readonly::Scalar my $EXPL => [ 114 ];
51              
52             #-----------------------------------------------------------------------------
53              
54             sub supported_parameters {
55             return (
56             {
57 107     107 0 2188 name => 'list_funcs',
58             description => 'The base set of functions to check.',
59             default_string => join ($SPACE, @BUILTIN_LIST_FUNCS, @CPAN_LIST_FUNCS ),
60             behavior => 'string list',
61             },
62             {
63             name => 'add_list_funcs',
64             description => 'The set of functions to check, in addition to those given in list_funcs.',
65             default_string => $EMPTY,
66             behavior => 'string list',
67             },
68             );
69             }
70              
71 122     122 1 473 sub default_severity { return $SEVERITY_HIGHEST }
72 92     92 1 389 sub default_themes { return qw(core bugs pbp certrule ) }
73 52     52 1 162 sub applies_to { return 'PPI::Token::Word' }
74              
75             #-----------------------------------------------------------------------------
76              
77             sub initialize_if_enabled {
78 87     87 1 330 my ($self, $config) = @_;
79              
80             $self->{_all_list_funcs} = {
81 87         235 hashify keys %{ $self->{_list_funcs} }, keys %{ $self->{_add_list_funcs} }
  87         1208  
  87         488  
82             };
83              
84 87         652 return $TRUE;
85             }
86              
87             #-----------------------------------------------------------------------------
88              
89             sub violates {
90 469     469 1 1042 my ($self, $elem, $doc) = @_;
91              
92             # Is this element a list function?
93 469 100       1169 return if not $self->{_all_list_funcs}->{$elem};
94 84 100       574 return if not is_function_call($elem);
95              
96             # Only the block form of list functions can be analyzed.
97 81 50       260 return if not my $first_arg = first_arg( $elem );
98 81 50       282 return if not $first_arg->isa('PPI::Structure::Block');
99 81 100       242 return if not $self->_has_topic_side_effect( $first_arg, $doc );
100              
101             # Must be a violation
102 48         431 return $self->violation( $DESC, $EXPL, $elem );
103             }
104              
105             #-----------------------------------------------------------------------------
106              
107             sub _has_topic_side_effect {
108 81     81   232 my ( $self, $node, $doc ) = @_;
109              
110             # Search through all significant elements in the block,
111             # testing each element to see if it mutates the topic.
112 81   50     246 my $tokens = $node->find( 'PPI::Token' ) || [];
113 81         25121 for my $elem ( @{ $tokens } ) {
  81         242  
114 319 100       1342 next if not $elem->significant();
115 204 100       424 return 1 if _is_assignment_to_topic( $elem );
116 195 100       461 return 1 if $self->_is_topic_mutating_regex( $elem, $doc );
117 162 100       358 return 1 if _is_topic_mutating_func( $elem );
118 157 100       412 return 1 if _is_topic_mutating_substr( $elem );
119             }
120 33         306 return;
121             }
122              
123             #-----------------------------------------------------------------------------
124              
125             sub _is_assignment_to_topic {
126 204     204   331 my $elem = shift;
127 204 100       398 return if not _is_topic( $elem );
128              
129 32         275 my $sib = $elem->snext_sibling();
130 32 100 100     878 if ($sib && $sib->isa('PPI::Token::Operator')) {
131 27 100       87 return 1 if _is_assignment_operator( $sib );
132             }
133              
134 25         173 my $psib = $elem->sprevious_sibling();
135 25 100 66     489 if ($psib && $psib->isa('PPI::Token::Operator')) {
136 3 100       12 return 1 if _is_increment_operator( $psib );
137             }
138              
139 23         71 return;
140             }
141              
142             #-----------------------------------------------------------------------------
143              
144             sub _is_topic_mutating_regex {
145 195     195   404 my ( $self, $elem, $doc ) = @_;
146 195 100 100     1109 return if ! ( $elem->isa('PPI::Token::Regexp::Substitute')
147             || $elem->isa('PPI::Token::Regexp::Transliterate') );
148              
149             # Exempt PPI::Token::Regexp::Transliterate objects IF the replacement
150             # string is empty AND neither the /d or /s flags are specified, OR the
151             # replacement string equals the match string AND neither the /c or /s
152             # flags are specified. RT 44515.
153             #
154             # NOTE that, at least as of 5.14.2, tr/// does _not_ participate in the
155             # 'use re /modifiers' mechanism. And a good thing, too, since the
156             # modifiers that _are_ common (/s and /d) mean something completely
157             # different in tr///.
158 60 100       196 if ( $elem->isa( 'PPI::Token::Regexp::Transliterate') ) {
159 56         173 my $subs = $elem->get_substitute_string();
160 56         1053 my %mods = $elem->get_modifiers();
161 56 100       758 $mods{r} and return; # Introduced in Perl 5.13.7
162 54 100       171 if ( $EMPTY eq $subs ) {
    100          
163 24 100 100     132 $mods{d} or $mods{s} or return;
164             } elsif ( $elem->get_match_string() eq $subs ) {
165 24 100 100     410 $mods{c} or $mods{s} or return;
166             }
167             }
168              
169             # As of 5.13.2, the substitute built-in supports the /r modifier, which
170             # causes the operation to return the modified string and leave the
171             # original unmodified. This does not parse under earlier Perls, so there
172             # is no version check.
173              
174 34 100       218 if ( $elem->isa( 'PPI::Token::Regexp::Substitute' ) ) {
175 4 50       19 my $re = $doc->ppix_regexp_from_element( $elem )
176             or return;
177 4 100       14290 $re->modifier_asserted( 'r' )
178             and return;
179             }
180              
181             # If the previous sibling does not exist, then
182             # the regex implicitly binds to $_
183 33         203 my $prevsib = $elem->sprevious_sibling;
184 33 100       947 return 1 if not $prevsib;
185              
186             # If the previous sibling does exist, then it
187             # should be a binding operator.
188 22 100       57 return 1 if not _is_binding_operator( $prevsib );
189              
190             # Check if the sibling before the biding operator
191             # is explicitly set to $_
192 11         78 my $bound_to = $prevsib->sprevious_sibling;
193 11         240 return _is_topic( $bound_to );
194             }
195              
196             #-----------------------------------------------------------------------------
197              
198             sub _is_topic_mutating_func {
199 162     162   287 my $elem = shift;
200 162 100       663 return if not $elem->isa('PPI::Token::Word');
201 28         89 my @mutator_funcs = qw(chop chomp undef);
202 28 100   77   147 return if not any { $elem->content() eq $_ } @mutator_funcs;
  77         312  
203 7 50       61 return if not is_function_call( $elem );
204              
205             # If these functions have no argument,
206             # they default to mutating $_
207 7         21 my $first_arg = first_arg( $elem );
208 7 100       23 if (not defined $first_arg) {
209             # undef does not default to $_, unlike the others
210 3 100       12 return if $elem->content() eq 'undef';
211 2         21 return 1;
212             }
213 4         11 return _is_topic( $first_arg );
214             }
215              
216             #-----------------------------------------------------------------------------
217              
218             Readonly::Scalar my $MUTATING_SUBSTR_ARG_COUNT => 4;
219              
220             sub _is_topic_mutating_substr {
221 157     157   264 my $elem = shift;
222 157 100       357 return if $elem->content() ne 'substr';
223 1 50       9 return if not is_function_call( $elem );
224              
225             # check and see if the first arg is $_
226 1         7 my @args = parse_arg_list( $elem );
227 1   33     9 return @args >= $MUTATING_SUBSTR_ARG_COUNT && _is_topic( $args[0]->[0] );
228             }
229              
230             #-----------------------------------------------------------------------------
231              
232             {
233             ##no critic(ArgUnpacking)
234              
235             my %assignment_ops = hashify qw(
236             = *= /= += -= %= **= x= .= &= |= ^= &&= ||= <<= >>= //= ++ --
237             );
238 27     27   94 sub _is_assignment_operator { return exists $assignment_ops{$_[0]} }
239              
240             my %increment_ops = hashify qw( ++ -- );
241 3     3   9 sub _is_increment_operator { return exists $increment_ops{$_[0]} }
242              
243             my %binding_ops = hashify qw( =~ !~ );
244 22     22   61 sub _is_binding_operator { return exists $binding_ops{$_[0]} }
245             }
246              
247             1;
248              
249             #-----------------------------------------------------------------------------
250              
251             __END__
252              
253             =pod
254              
255             =head1 NAME
256              
257             Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions - Don't modify C<$_> in list functions.
258              
259              
260             =head1 AFFILIATION
261              
262             This Policy is part of the core L<Perl::Critic|Perl::Critic>
263             distribution.
264              
265              
266             =head1 DESCRIPTION
267              
268             C<map>, C<grep> and other list operators are intended to transform
269             arrays into other arrays by applying code to the array elements one by
270             one. For speed, the elements are referenced via a C<$_> alias rather
271             than copying them. As a consequence, if the code block of the C<map>
272             or C<grep> modify C<$_> in any way, then it is actually modifying the
273             source array. This IS technically allowed, but those side effects can
274             be quite surprising, especially when the array being passed is C<@_>
275             or perhaps C<values(%ENV)>! Instead authors should restrict in-place
276             array modification to C<for(@array) { ... }> constructs instead, or
277             use C<List::SomeUtiles:apply()> or C<List::MoreUtils::apply()>.
278              
279             =head1 CONFIGURATION
280              
281             By default, this policy applies to the following list functions:
282              
283             map grep
284             List::Util qw(first)
285             List::MoreUtils qw(any all none notall true false firstidx
286             first_index lastidx last_index insert_after
287             insert_after_string)
288             List::SomeUtils qw(any all none notall true false firstidx
289             first_index lastidx last_index insert_after
290             insert_after_string)
291              
292             This list can be overridden the F<.perlcriticrc> file like this:
293              
294             [ControlStructures::ProhibitMutatingListFunctions]
295             list_funcs = map grep List::Util::first
296              
297             Or, one can just append to the list like so:
298              
299             [ControlStructures::ProhibitMutatingListFunctions]
300             add_list_funcs = Foo::Bar::listmunge
301              
302             =head1 LIMITATIONS
303              
304             This policy deliberately does not apply to C<for (@array) { ... }> or
305             C<List::MoreUtils::apply()> C<List::SomeUtils::apply()>.
306              
307             Currently, the policy only detects explicit external module usage like
308             this:
309              
310             my @out = List::MoreUtils::any {s/^foo//} @in;
311             my @out = List::SomeUtils::any {s/^foo//} @in;
312              
313             and not like this:
314              
315             use List::MoreUtils qw(any);
316             my @out = any {s/^foo//} @in;
317              
318             use List::SomeUtils qw(any);
319             my @out = any {s/^foo//} @in;
320              
321             This policy looks only for modifications of C<$_>. Other naughtiness
322             could include modifying C<$a> and C<$b> in C<sort> and the like.
323             That's beyond the scope of this policy.
324              
325              
326             =head1 SEE ALSO
327              
328             There is discussion of this policy at
329             L<http://perlmonks.org/index.pl?node_id=743445>.
330              
331              
332             =head1 AUTHOR
333              
334             Chris Dolan <cdolan@cpan.org>
335              
336             Michael Wolf <MichaelRWolf@att.net>
337              
338              
339             =head1 COPYRIGHT
340              
341             Copyright (c) 2006-2021 Chris Dolan.
342              
343             This program is free software; you can redistribute it and/or modify
344             it under the same terms as Perl itself.
345              
346             =cut
347              
348             # Local Variables:
349             # mode: cperl
350             # cperl-indent-level: 4
351             # fill-column: 78
352             # indent-tabs-mode: nil
353             # End:
354             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :
355