File Coverage

blib/lib/Perl/Critic/Policy/ControlStructures/ProhibitMutatingListFunctions.pm
Criterion Covered Total %
statement 97 97 100.0
branch 61 66 92.4
condition 18 23 78.2
subroutine 24 24 100.0
pod 5 6 83.3
total 205 216 94.9


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions;
2              
3 40     40   28845 use 5.010001;
  40         178  
4 40     40   270 use strict;
  40         136  
  40         885  
5 40     40   235 use warnings;
  40         99  
  40         1067  
6 40     40   262 use Readonly;
  40         110  
  40         2109  
7              
8 40     40   328 use List::SomeUtils qw( any none );
  40         176  
  40         2565  
9              
10 40         2221 use Perl::Critic::Utils qw{
11             :booleans :characters :severities :data_conversion :classification :ppi
12 40     40   333 };
  40         138  
13              
14 40     40   24156 use parent 'Perl::Critic::Policy';
  40         114  
  40         280  
15              
16             our $VERSION = '1.148';
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   151 map { ('List::MoreUtils::'.$_, 'List::SomeUtils::'.$_) } _get_list_moreutils_funcs();
  480         1414  
28             }
29              
30             #-----------------------------------------------------------------------------
31              
32             sub _get_list_moreutils_funcs {
33 40     40   174 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   356 my $elem = shift;
41 220   66     1406 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 2266 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 533 sub default_severity { return $SEVERITY_HIGHEST }
72 92     92 1 456 sub default_themes { return qw(core bugs pbp certrule ) }
73 52     52 1 226 sub applies_to { return 'PPI::Token::Word' }
74              
75             #-----------------------------------------------------------------------------
76              
77             sub initialize_if_enabled {
78 87     87 1 323 my ($self, $config) = @_;
79              
80             $self->{_all_list_funcs} = {
81 87         283 hashify keys %{ $self->{_list_funcs} }, keys %{ $self->{_add_list_funcs} }
  87         1232  
  87         473  
82             };
83              
84 87         633 return $TRUE;
85             }
86              
87             #-----------------------------------------------------------------------------
88              
89             sub violates {
90 469     469 1 976 my ($self, $elem, $doc) = @_;
91              
92             # Is this element a list function?
93 469 100       1137 return if not $self->{_all_list_funcs}->{$elem};
94 84 100       559 return if not is_function_call($elem);
95              
96             # Only the block form of list functions can be analyzed.
97 81 50       235 return if not my $first_arg = first_arg( $elem );
98 81 50       265 return if not $first_arg->isa('PPI::Structure::Block');
99 81 100       236 return if not $self->_has_topic_side_effect( $first_arg, $doc );
100              
101             # Must be a violation
102 48         421 return $self->violation( $DESC, $EXPL, $elem );
103             }
104              
105             #-----------------------------------------------------------------------------
106              
107             sub _has_topic_side_effect {
108 81     81   192 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     245 my $tokens = $node->find( 'PPI::Token' ) || [];
113 81         24698 for my $elem ( @{ $tokens } ) {
  81         242  
114 319 100       1483 next if not $elem->significant();
115 204 100       423 return 1 if _is_assignment_to_topic( $elem );
116 195 100       481 return 1 if $self->_is_topic_mutating_regex( $elem, $doc );
117 162 100       370 return 1 if _is_topic_mutating_func( $elem );
118 157 100       491 return 1 if _is_topic_mutating_substr( $elem );
119             }
120 33         279 return;
121             }
122              
123             #-----------------------------------------------------------------------------
124              
125             sub _is_assignment_to_topic {
126 204     204   345 my $elem = shift;
127 204 100       387 return if not _is_topic( $elem );
128              
129 32         277 my $sib = $elem->snext_sibling();
130 32 100 100     873 if ($sib && $sib->isa('PPI::Token::Operator')) {
131 27 100       71 return 1 if _is_assignment_operator( $sib );
132             }
133              
134 25         160 my $psib = $elem->sprevious_sibling();
135 25 100 66     488 if ($psib && $psib->isa('PPI::Token::Operator')) {
136 3 100       15 return 1 if _is_increment_operator( $psib );
137             }
138              
139 23         68 return;
140             }
141              
142             #-----------------------------------------------------------------------------
143              
144             sub _is_topic_mutating_regex {
145 195     195   399 my ( $self, $elem, $doc ) = @_;
146 195 100 100     1134 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       181 if ( $elem->isa( 'PPI::Token::Regexp::Transliterate') ) {
159 56         171 my $subs = $elem->get_substitute_string();
160 56         976 my %mods = $elem->get_modifiers();
161 56 100       756 $mods{r} and return; # Introduced in Perl 5.13.7
162 54 100       179 if ( $EMPTY eq $subs ) {
    100          
163 24 100 100     136 $mods{d} or $mods{s} or return;
164             } elsif ( $elem->get_match_string() eq $subs ) {
165 24 100 100     771 $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       210 if ( $elem->isa( 'PPI::Token::Regexp::Substitute' ) ) {
175 4 50       29 my $re = $doc->ppix_regexp_from_element( $elem )
176             or return;
177 4 100       15342 $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         157 my $prevsib = $elem->sprevious_sibling;
184 33 100       982 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 binding operator
191             # is explicitly set to $_
192 11         72 my $bound_to = $prevsib->sprevious_sibling;
193 11         237 return _is_topic( $bound_to );
194             }
195              
196             #-----------------------------------------------------------------------------
197              
198             sub _is_topic_mutating_func {
199 162     162   259 my $elem = shift;
200 162 100       608 return if not $elem->isa('PPI::Token::Word');
201              
202 28         63 state $mutator_funcs = { hashify qw( chop chomp undef ) };
203 28 100       113 return if !$mutator_funcs->{$elem->content()};
204              
205 7 50       55 return if not is_function_call( $elem );
206              
207             # If these functions have no argument,
208             # they default to mutating $_
209 7         32 my $first_arg = first_arg( $elem );
210 7 100       23 if (not defined $first_arg) {
211             # undef does not default to $_, unlike the others
212 3         11 return $elem->content() ne 'undef';
213             }
214 4         16 return _is_topic( $first_arg );
215             }
216              
217             #-----------------------------------------------------------------------------
218              
219             Readonly::Scalar my $MUTATING_SUBSTR_ARG_COUNT => 4;
220              
221             sub _is_topic_mutating_substr {
222 157     157   279 my $elem = shift;
223 157 100       352 return if $elem->content() ne 'substr';
224 1 50       10 return if not is_function_call( $elem );
225              
226             # check and see if the first arg is $_
227 1         15 my @args = parse_arg_list( $elem );
228 1   33     8 return @args >= $MUTATING_SUBSTR_ARG_COUNT && _is_topic( $args[0]->[0] );
229             }
230              
231             #-----------------------------------------------------------------------------
232              
233             sub _is_assignment_operator { ## no critic (RequireArgUnpacking)
234 27     27   56 state $assignment_ops = { hashify qw( = *= /= += -= %= **= x= .= &= |= ^= &&= ||= <<= >>= //= ++ --) };
235 27         66 return exists $assignment_ops->{$_[0]};
236             }
237              
238             sub _is_increment_operator { ## no critic (RequireArgUnpacking)
239 3     3   9 state $increment_ops = { hashify qw( ++ -- ) };
240 3         11 return exists $increment_ops->{$_[0]};
241             }
242              
243             sub _is_binding_operator { ## no critic (RequireArgUnpacking)
244 22     22   46 state $binding_ops = { hashify qw( =~ !~ ) };
245 22         61 return exists $binding_ops->{$_[0]};
246             }
247              
248             1;
249              
250             #-----------------------------------------------------------------------------
251              
252             __END__
253              
254             =pod
255              
256             =head1 NAME
257              
258             Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions - Don't modify C<$_> in list functions.
259              
260              
261             =head1 AFFILIATION
262              
263             This Policy is part of the core L<Perl::Critic|Perl::Critic>
264             distribution.
265              
266              
267             =head1 DESCRIPTION
268              
269             C<map>, C<grep> and other list operators are intended to transform
270             arrays into other arrays by applying code to the array elements one by
271             one. For speed, the elements are referenced via a C<$_> alias rather
272             than copying them. As a consequence, if the code block of the C<map>
273             or C<grep> modify C<$_> in any way, then it is actually modifying the
274             source array. This IS technically allowed, but those side effects can
275             be quite surprising, especially when the array being passed is C<@_>
276             or perhaps C<values(%ENV)>! Instead authors should restrict in-place
277             array modification to C<for(@array) { ... }> constructs instead, or
278             use C<List::SomeUtils:apply()> or C<List::MoreUtils::apply()>.
279              
280             =head1 CONFIGURATION
281              
282             By default, this policy applies to the following list functions:
283              
284             map grep
285             List::Util qw(first)
286             List::MoreUtils qw(any all none notall true false firstidx
287             first_index lastidx last_index insert_after
288             insert_after_string)
289             List::SomeUtils qw(any all none notall true false firstidx
290             first_index lastidx last_index insert_after
291             insert_after_string)
292              
293             This list can be overridden the F<.perlcriticrc> file like this:
294              
295             [ControlStructures::ProhibitMutatingListFunctions]
296             list_funcs = map grep List::Util::first
297              
298             Or, one can just append to the list like so:
299              
300             [ControlStructures::ProhibitMutatingListFunctions]
301             add_list_funcs = Foo::Bar::listmunge
302              
303             =head1 LIMITATIONS
304              
305             This policy deliberately does not apply to C<for (@array) { ... }> or
306             C<List::MoreUtils::apply()> C<List::SomeUtils::apply()>.
307              
308             Currently, the policy only detects explicit external module usage like
309             this:
310              
311             my @out = List::MoreUtils::any {s/^foo//} @in;
312             my @out = List::SomeUtils::any {s/^foo//} @in;
313              
314             and not like this:
315              
316             use List::MoreUtils qw(any);
317             my @out = any {s/^foo//} @in;
318              
319             use List::SomeUtils qw(any);
320             my @out = any {s/^foo//} @in;
321              
322             This policy looks only for modifications of C<$_>. Other naughtiness
323             could include modifying C<$a> and C<$b> in C<sort> and the like.
324             That's beyond the scope of this policy.
325              
326              
327             =head1 SEE ALSO
328              
329             There is discussion of this policy at
330             L<http://perlmonks.org/index.pl?node_id=743445>.
331              
332              
333             =head1 AUTHOR
334              
335             Chris Dolan <cdolan@cpan.org>
336              
337             Michael Wolf <MichaelRWolf@att.net>
338              
339              
340             =head1 COPYRIGHT
341              
342             Copyright (c) 2006-2021 Chris Dolan.
343              
344             This program is free software; you can redistribute it and/or modify
345             it under the same terms as Perl itself.
346              
347             =cut
348              
349             # Local Variables:
350             # mode: cperl
351             # cperl-indent-level: 4
352             # fill-column: 78
353             # indent-tabs-mode: nil
354             # End:
355             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :
356