File Coverage

blib/lib/Perl/Critic/Policy/Variables/ProhibitPunctuationVars.pm
Criterion Covered Total %
statement 72 128 56.2
branch 11 58 18.9
condition 0 9 0.0
subroutine 22 25 88.0
pod 4 5 80.0
total 109 225 48.4


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::Variables::ProhibitPunctuationVars;
2              
3 40     40   28657 use 5.010001;
  40         208  
4 40     40   331 use strict;
  40         218  
  40         942  
5 40     40   269 use warnings;
  40         128  
  40         968  
6 40     40   267 use Readonly;
  40         116  
  40         2071  
7 40     40   310 use English qw< -no_match_vars >;
  40         132  
  40         447  
8              
9 40     40   17461 use PPI::Token::Magic;
  40         159  
  40         1964  
10              
11 40     40   308 use Perl::Critic::Utils qw( :characters :severities :data_conversion );
  40         181  
  40         2174  
12              
13 40     40   13798 use PPIx::Regexp;
  40         140  
  40         1940  
14 40         2252 use PPIx::Regexp::Util 0.068 qw<
15             is_ppi_regexp_element
16 40     40   313 >;
  40         1176  
17              
18 40     40   323 use parent 'Perl::Critic::Policy';
  40         162  
  40         341  
19              
20             our $VERSION = '1.150';
21              
22             #-----------------------------------------------------------------------------
23              
24             Readonly::Scalar my $DESC => q<Magic punctuation variable %s used>;
25             Readonly::Scalar my $EXPL => [79];
26              
27             #-----------------------------------------------------------------------------
28              
29             # There is no English.pm equivalent for $].
30             sub supported_parameters {
31             return (
32             {
33 92     92 0 2687 name => 'allow',
34             description => 'The additional variables to allow.',
35             default_string => $EMPTY,
36             behavior => 'string list',
37             list_always_present_values =>
38             [ qw< $_ @_ $1 $2 $3 $4 $5 $6 $7 $8 $9 _ $] > ],
39             },
40             {
41             name => 'string_mode',
42             description =>
43             'Controls checking interpolated strings for punctuation variables.',
44             default_string => 'thorough',
45             behavior => 'enumeration',
46             enumeration_values => [ qw< simple disable thorough > ],
47             enumeration_allow_multiple_values => 0,
48             },
49             );
50             }
51              
52 89     89 1 381 sub default_severity { return $SEVERITY_LOW }
53 84     84 1 329 sub default_themes { return qw< core pbp cosmetic > }
54              
55             sub applies_to {
56 30     30 1 122 return qw<
57             PPI::Token::Magic
58             PPI::Token::Quote::Double
59             PPI::Token::Quote::Interpolate
60             PPI::Token::QuoteLike::Command
61             PPI::Token::QuoteLike::Backtick
62             PPI::Token::QuoteLike::Regexp
63             PPI::Token::QuoteLike::Readline
64             PPI::Token::HereDoc
65             >;
66             }
67              
68             #-----------------------------------------------------------------------------
69              
70              
71             # This list matches the initialization of %PPI::Token::Magic::magic.
72             ## no critic (ValuesAndExpressions::RequireInterpolationOfMetachars)
73             Readonly::Array my @MAGIC_VARIABLES =>
74             qw{
75             $1 $2 $3 $4 $5 $6 $7 $8 $9
76             $_ $& $` $' $+ @+ %+ $* $. $/ $|
77             $\\ $" $; $% $= $- @- %- $)
78             $~ $^ $: $? $! %! $@ $$ $< $>
79             $( $0 $[ $] @_ @*
80              
81             $^L $^A $^E $^C $^D $^F $^H
82             $^I $^M $^N $^O $^P $^R $^S
83             $^T $^V $^W $^X %^H
84              
85             $::|
86             },
87             q<$}>,
88             q<$,>,
89             q<$#>,
90             q<$#+>,
91             q<$#->;
92             ## use critic
93              
94             # The main regular expression for detecting magic variables.
95             Readonly::Scalar my $MAGIC_REGEX => _create_magic_detector();
96              
97             # The magic vars in this array will be ignored in interpolated strings
98             # in simple mode. See CONFIGURATION in the pod.
99             Readonly::Array my @IGNORE_FOR_INTERPOLATION =>
100             ( q{$'}, q{$$}, q{$#}, q{$:}, ); ## no critic ( RequireInterpolationOfMetachars, ProhibitQuotedWordLists )
101              
102             #-----------------------------------------------------------------------------
103              
104             sub violates {
105 16     16 1 32 my ( $self, $elem, $doc ) = @_;
106              
107 16 100       66 if ( $elem->isa('PPI::Token::Magic') ) {
    50          
    50          
108 15         30 return _violates_magic( $self, $elem );
109             }
110             elsif ( $elem->isa('PPI::Token::HereDoc') ) {
111 0         0 return _violates_heredoc( $self, $elem );
112             }
113             elsif ( is_ppi_regexp_element( $elem ) ) { # GitHub #843
114 0         0 return _violates_regexp( $self, $elem, $doc );
115             }
116              
117             #the remaining applies_to() classes are all interpolated strings
118 1         32 return _violates_string( $self, $elem );
119             }
120              
121             #-----------------------------------------------------------------------------
122              
123             # Helper functions for the four types of violations: code, quotes, heredoc,
124             # regexp
125              
126             sub _violates_magic {
127 15     15   24 my ( $self, $elem, undef ) = @_;
128              
129 15 50       36 if ( !exists $self->{_allow}->{$elem} ) {
130 15         74 return $self->_make_violation( $DESC, $EXPL, $elem );
131             }
132              
133 0         0 return; # no violation
134             }
135              
136             sub _violates_string {
137 1     1   2 my ( $self, $elem, undef ) = @_;
138              
139             # RT #55604: Variables::ProhibitPunctuationVars gives false-positive on
140             # qr// regexp's ending in '$'
141             # We want to analyze the content of the string in the dictionary sense of
142             # the word 'content'. We can not simply use the PPI content() method to
143             # get this, because content() includes the delimiters.
144 1         2 my $string;
145 1 50       20 if ( $elem->can( 'string' ) ) {
146             # If we have a string() method (currently only the PPI::Token::Quote
147             # classes) use it to extract the content of the string.
148 1         5 $string = $elem->string();
149             } else {
150             # Lacking string(), we fake it under the assumption that the content
151             # of our element represents one of the 'normal' Perl strings, with a
152             # single-character delimiter, possibly preceded by an operator like
153             # 'qx' or 'qr'. If there is a leading operator, spaces may appear
154             # after it.
155 0         0 $string = $elem->content();
156 0         0 $string =~ s/ \A \w* \s* . //smx;
157 0         0 chop $string;
158             }
159              
160 1         9 my %matches = _strings_helper( $self, $string );
161 1 50       4 if (%matches) {
162 0         0 return $self->_make_violation( $DESC . ' in interpolated string', $EXPL, $elem, \%matches );
163             }
164              
165 1         4 return; # no violation
166             }
167              
168             sub _violates_heredoc {
169 0     0   0 my ( $self, $elem, undef ) = @_;
170              
171 0 0 0     0 if ( $elem->{_mode} eq 'interpolate' or $elem->{_mode} eq 'command' ) {
172 0         0 my $heredoc_string = join "\n", $elem->heredoc();
173 0         0 my %matches = _strings_helper( $self, $heredoc_string );
174 0 0       0 if (%matches) {
175 0         0 return $self->_make_violation( $DESC . ' in interpolated here-document', $EXPL, $elem, \%matches );
176             }
177             }
178              
179 0         0 return; # no violation
180             }
181              
182             sub _violates_regexp { # GitHub #843 (https://github.com/Perl-Critic/Perl-Critic/issues/843)
183 0     0   0 my ( $self, $elem, $doc ) = @_;
184              
185 0 0       0 return if ( $self->{_string_mode} eq 'disable' );
186              
187 0 0       0 my $pre = $doc->ppix_regexp_from_element( $elem )
188             or return;
189 0 0       0 $pre->failures()
190             and return;
191              
192 0         0 my @raw_matches;
193 0 0       0 foreach my $code ( @{ $pre->find( 'PPIx::Regexp::Token::Code' ) || [] } ) {
  0         0  
194 0 0       0 my $code_doc = $code->ppi()
195             or next;
196 0         0 push @raw_matches, map { $_->symbol() } @{
197 0 0       0 $code_doc->find( 'PPI::Token::Magic' ) || [] };
  0         0  
198             }
199              
200 0         0 my %matches = hashify( @raw_matches );
201 0         0 delete @matches{ keys %{ $self->{_allow} } };
  0         0  
202 0 0       0 if ( $self->{_string_mode} eq 'simple' ) {
203 0         0 delete @matches{@IGNORE_FOR_INTERPOLATION};
204             }
205              
206 0 0       0 if ( keys %matches ) {
207 0         0 return $self->_make_violation( $DESC . ' in interpolated Regexp', $EXPL, $elem, \%matches );
208             }
209              
210 0         0 return;
211             }
212              
213             #-----------------------------------------------------------------------------
214              
215             # Helper functions specific to interpolated strings
216              
217             sub _strings_helper {
218 1     1   3 my ( $self, $target_string, undef ) = @_;
219              
220 1 50       4 return if ( $self->{_string_mode} eq 'disable' );
221             return _strings_thorough( $self, $target_string )
222 1 50       6 if $self->{_string_mode} eq 'thorough';
223              
224             # we are in string_mode = simple
225              
226 0         0 my @raw_matches = map { _unbracket_variable_name( $_ ) }
  0         0  
227             $target_string =~ m/$MAGIC_REGEX/goxms;
228 0 0       0 return if not @raw_matches;
229              
230 0         0 my %matches = hashify(@raw_matches);
231              
232 0         0 delete @matches{ keys %{ $self->{_allow} } };
  0         0  
233 0         0 delete @matches{@IGNORE_FOR_INTERPOLATION};
234              
235 0         0 return %matches;
236             }
237              
238             sub _strings_thorough {
239 1     1   4 my ( $self, $target_string, undef ) = @_;
240 1         2 my %matches;
241              
242             MATCH:
243 1         13 while ( my ($match) = $target_string =~ m/$MAGIC_REGEX/gcxms ) {
244 0         0 my $nextchar = substr $target_string, $LAST_MATCH_END[0], 1;
245 0         0 my $vname = _unbracket_variable_name( $match );
246 0         0 my $c = $vname . $nextchar;
247              
248             # These tests closely parallel those in PPI::Token::Magic,
249             # from which the regular expressions were taken.
250             # A degree of simplicity is sacrificed to maintain the parallel.
251             # $c is so named by analogy to that module.
252              
253             # possibly *not* a magic variable
254 0 0       0 if ($c =~ m/ ^ \$ .* [ \w : \$ { ] $ /xms) {
255             ## no critic (RequireInterpolationOfMetachars)
256              
257 0 0 0     0 if (
258             $c =~ m/ ^(\$(?:\_[\w:]|::)) /xms
259             or $c =~ m/ ^\$\'[\w] /xms )
260             {
261             next MATCH
262 0 0       0 if $c !~ m/ ^\$\'\d$ /xms;
263             # It not $' followed by a digit.
264             # So it's magic var with something immediately after.
265             }
266              
267             next MATCH
268 0 0       0 if $c =~ m/ ^\$\$\w /xms; # It's a scalar dereference
269             next MATCH
270 0 0 0     0 if $c eq '$#$'
271             or $c eq '$#{'; # It's an index dereferencing cast
272             next MATCH
273 0 0       0 if $c =~ m/ ^(\$\#)\w /xms
274             ; # It's an array index thingy, e.g. $#array_name
275              
276             # PPI's checks for long escaped vars like $^WIDE_SYSTEM_CALLS
277             # appear to be erroneous, and are omitted here.
278             # if ( $c =~ m/^\$\^\w{2}$/xms ) {
279             # }
280              
281 0 0       0 next MATCH if $c =~ m/ ^ \$ \# [{] /xms; # It's a $#{...} cast
282             }
283              
284             # The additional checking that PPI::Token::Magic does at this point
285             # is not necessary here, in an interpolated string context.
286              
287 0         0 $matches{$vname} = 1;
288             }
289              
290 1         2 delete @matches{ keys %{ $self->{_allow} } };
  1         9  
291              
292 1         5 return %matches;
293             }
294              
295             # RT #72910: A magic variable may appear in bracketed form; e.g. "$$" as
296             # "${$}". Generate the bracketed form from the unbracketed form, and
297             # return both.
298             sub _bracketed_form_of_variable_name {
299 2640     2640   4177 my ( $name ) = @_;
300 2640 50       4927 length $name > 1
301             or return ( $name );
302 2640         3913 my $brktd = $name;
303 2640         4026 substr $brktd, 1, 0, '{';
304 2640         3743 $brktd .= '}';
305 2640         6165 return( $name, $brktd );
306             }
307              
308             # RT #72910: Since we loaded both bracketed and unbracketed forms of the
309             # punctuation variables into our detecting regex, we need to detect and
310             # strip the brackets if they are present to recover the canonical name.
311             sub _unbracket_variable_name {
312 0     0   0 my ( $name ) = @_;
313 0 0       0 $name =~ m/ \A ( . ) [{] ( .+ ) [}] \z /smx
314             and return "$1$2";
315 0         0 return $name;
316             }
317              
318             #-----------------------------------------------------------------------------
319              
320             sub _create_magic_detector {
321 40     40   133 my ($config) = @_;
322              
323             # Set up the regexp alternation for matching magic variables.
324             # We can't process $config->{_allow} here because of a quirk in the
325             # way Perl::Critic handles testing.
326             #
327             # The sort is needed so that, e.g., $^ doesn't mask out $^M
328             my $magic_alternation =
329             '(?:'
330             . (
331             join
332             q<|>,
333 5280         9866 map { quotemeta }
334 25000         35134 reverse sort { length $a <=> length $b }
335 2640         4698 map { _bracketed_form_of_variable_name( $_ ) }
336 40         200 grep { q<%> ne substr $_, 0, 1 }
  2800         18169  
337             @MAGIC_VARIABLES
338             )
339             . ')';
340              
341 40         19449 return qr<
342             (?: \A | [^\\] ) # beginning-of-string or any non-backslash
343             (?: \\{2} )* # zero or more double-backslashes
344             ( $magic_alternation ) # any magic punctuation variable
345             >xsm;
346             }
347              
348             sub _make_violation {
349 15     15   30 my ( $self, $desc, $expl, $elem, $vars ) = @_;
350              
351             my $vname = 'HASH' eq ref $vars ?
352 15 50       34 join ', ', sort keys %{ $vars } :
  0         0  
353             $elem->content();
354 15         115 return $self->violation( sprintf( $desc, $vname ), $expl, $elem );
355             }
356              
357             1;
358              
359             __END__
360              
361             #-----------------------------------------------------------------------------
362              
363             =pod
364              
365             =head1 NAME
366              
367             Perl::Critic::Policy::Variables::ProhibitPunctuationVars - Write C<$EVAL_ERROR> instead of C<$@>.
368              
369              
370             =head1 AFFILIATION
371              
372             This Policy is part of the core L<Perl::Critic|Perl::Critic>
373             distribution.
374              
375              
376             =head1 DESCRIPTION
377              
378             Perl's vocabulary of punctuation variables such as C<$!>, C<$.>, and
379             C<$^> are perhaps the leading cause of its reputation as inscrutable
380             line noise. The simple alternative is to use the L<English|English>
381             module to give them clear names.
382              
383             $| = undef; #not ok
384              
385             use English qw(-no_match_vars);
386             local $OUTPUT_AUTOFLUSH = undef; #ok
387              
388             =head1 CONFIGURATION
389              
390             The scratch variables C<$_> and C<@_> are very common and are pretty
391             well understood, so they are exempt from this policy. The same goes
392             for the less-frequently-used default filehandle C<_> used by stat().
393             All the regexp capture variables (C<$1>, C<$2>, ...) are exempt too.
394             C<$]> is exempt because there is no L<English|English> equivalent and
395             L<Module::CoreList|Module::CoreList> is based upon it.
396              
397             You can add more exceptions to your configuration. In your
398             perlcriticrc file, add a block like this:
399              
400             [Variables::ProhibitPunctuationVars]
401             allow = $@ $!
402              
403             The C<allow> property should be a whitespace-delimited list of
404             punctuation variables.
405              
406             Other configuration options control the parsing of interpolated
407             strings in the search for forbidden variables. They have no effect
408             on detecting punctuation variables outside of interpolated strings.
409              
410             [Variables::ProhibitPunctuationVars]
411             string_mode = thorough
412              
413             The option C<string_mode> controls whether and how interpolated
414             strings are searched for punctuation variables. Setting
415             C<string_mode = thorough>, the default, checks for special cases
416             that may look like punctuation variables but aren't, for example
417             C<$#foo>, an array index count; C<$$bar>, a scalar dereference; or
418             C<$::baz>, a global symbol.
419              
420             Setting C<string_mode = disable> causes all interpolated strings to
421             be ignored entirely.
422              
423             Setting C<string_mode = simple> uses a simple regular expression to
424             find matches. In this mode, the magic variables C<$$>, C<$'>, C<$#>
425             and C<$:> are ignored within interpolated strings due to the high
426             risk of false positives. Simple mode is retained from an earlier
427             draft of the interpolated- strings code. Its use is only recommended
428             as a workaround if bugs appear in thorough mode.
429              
430             The C<string_mode> option will go away when the parsing of
431             interpolated strings is implemented in PPI. See L</CAVEATS> below.
432              
433              
434             =head1 BUGS
435              
436             Punctuation variables that confuse PPI's document parsing may not be
437             detected correctly or at all, and may prevent detection of
438             subsequent ones. In particular, C<$"> is known to cause difficulties
439             in interpolated strings.
440              
441              
442             =head1 CAVEATS
443              
444             ProhibitPunctuationVars relies exclusively on PPI to find
445             punctuation variables in code, but does all the parsing itself for
446             interpolated strings. When, at some point, this functionality is
447             transferred to PPI, ProhibitPunctuationVars will cease doing the
448             interpolating and the C<string_mode> option will go away.
449              
450              
451             =head1 AUTHOR
452              
453             Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
454              
455              
456             =head1 COPYRIGHT
457              
458             Copyright (c) 2005-2023 Imaginative Software Systems
459              
460             This program is free software; you can redistribute it and/or modify
461             it under the same terms as Perl itself. The full text of this license
462             can be found in the LICENSE file included with this module.
463              
464             =cut
465              
466             # Local Variables:
467             # mode: cperl
468             # cperl-indent-level: 4
469             # fill-column: 78
470             # indent-tabs-mode: nil
471             # c-indentation-style: bsd
472             # End:
473             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :