File Coverage

blib/lib/Perl/Critic/Policy/ControlStructures/ProhibitMutatingListFunctions.pm
Criterion Covered Total %
statement 32 94 34.0
branch 3 66 4.5
condition 0 23 0.0
subroutine 14 23 60.8
pod 5 6 83.3
total 54 212 25.4


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