File Coverage

blib/lib/Perl/Critic/Policy/Variables/ProhibitPunctuationVars.pm
Criterion Covered Total %
statement 71 127 55.9
branch 11 58 18.9
condition 0 9 0.0
subroutine 22 25 88.0
pod 4 5 80.0
total 108 224 48.2


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::Variables::ProhibitPunctuationVars;
2              
3 40     40   25647 use 5.010001;
  40         131  
4 40     40   163 use strict;
  40         63  
  40         800  
5 40     40   132 use warnings;
  40         59  
  40         1309  
6 40     40   164 use Readonly;
  40         76  
  40         2133  
7 40     40   208 use English qw< -no_match_vars >;
  40         85  
  40         295  
8              
9 40     40   11978 use PPI::Token::Magic;
  40         85  
  40         1619  
10              
11 40     40   143 use Perl::Critic::Utils qw( :characters :severities :data_conversion );
  40         71  
  40         1769  
12              
13 40     40   30151 use PPIx::Regexp;
  40         6841877  
  40         3642  
14 40         2241 use PPIx::Regexp::Util 0.068 qw<
15             is_ppi_regexp_element
16 40     40   347 >;
  40         4161  
17              
18 40     40   206 use parent 'Perl::Critic::Policy';
  40         78  
  40         2005  
19              
20             our $VERSION = '1.156';
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 93     93 0 1626 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 90     90 1 295 sub default_severity { return $SEVERITY_LOW }
53 84     84 1 231 sub default_themes { return qw< core pbp cosmetic > }
54              
55             sub applies_to {
56 31     31 1 133 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 30 my ( $self, $elem, $doc ) = @_;
106              
107 16 100       50 if ( $elem->isa('PPI::Token::Magic') ) {
    50          
    50          
108 15         23 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         27 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   26 my ( $self, $elem, undef ) = @_;
128              
129 15 50       26 if ( !exists $self->{_allow}->{$elem} ) {
130 15         71 return $self->_make_violation( $DESC, $EXPL, $elem );
131             }
132              
133 0         0 return; # no violation
134             }
135              
136             sub _violates_string {
137 1     1   3 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         14 my $string;
145 1 50       5 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         4 $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         10 my %matches = _strings_helper( $self, $string );
161 1 50       3 if (%matches) {
162 0         0 return $self->_make_violation( $DESC . ' in interpolated string', $EXPL, $elem, \%matches );
163             }
164              
165 1         3 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   2 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       4 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   3 my ( $self, $target_string, undef ) = @_;
240 1         3 my %matches;
241              
242             MATCH:
243 1         17 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         3 delete @matches{ keys %{ $self->{_allow} } };
  1         7  
291              
292 1         4 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   3176 my ( $name ) = @_;
300 2640 50       3612 length $name > 1
301             or return ( $name );
302 2640         2844 my $brktd = $name;
303 2640         2976 substr $brktd, 1, 0, '{';
304 2640         2867 $brktd .= '}';
305 2640         4231 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             # Set up the regexp alternation for matching magic variables.
322             # We can't process $config->{_allow} here because of a quirk in the
323             # way Perl::Critic handles testing.
324             #
325             # The sort is needed so that, e.g., $^ doesn't mask out $^M
326             my $magic_alternation =
327             '(?:'
328             . (
329             join
330             q<|>,
331 5280         7416 map { quotemeta }
332 25000         25856 reverse sort { length $a <=> length $b }
333 2640         3300 map { _bracketed_form_of_variable_name( $_ ) }
334 40     40   207 grep { q<%> ne substr $_, 0, 1 }
  2800         11740  
335             @MAGIC_VARIABLES
336             )
337             . ')';
338              
339 40         19841 return qr<
340             (?: \A | [^\\] ) # beginning-of-string or any non-backslash
341             (?: \\{2} )* # zero or more double-backslashes
342             ( $magic_alternation ) # any magic punctuation variable
343             >xsm;
344             }
345              
346             sub _make_violation {
347 15     15   22 my ( $self, $desc, $expl, $elem, $vars ) = @_;
348              
349             my $vname = 'HASH' eq ref $vars ?
350 15 50       43 join ', ', sort keys %{ $vars } :
  0         0  
351             $elem->content();
352 15         73 return $self->violation( sprintf( $desc, $vname ), $expl, $elem );
353             }
354              
355             1;
356              
357             __END__
358              
359             #-----------------------------------------------------------------------------
360              
361             =pod
362              
363             =head1 NAME
364              
365             Perl::Critic::Policy::Variables::ProhibitPunctuationVars - Write C<$EVAL_ERROR> instead of C<$@>.
366              
367              
368             =head1 AFFILIATION
369              
370             This Policy is part of the core L<Perl::Critic|Perl::Critic>
371             distribution.
372              
373              
374             =head1 DESCRIPTION
375              
376             Perl's vocabulary of punctuation variables such as C<$!>, C<$.>, and
377             C<$^> are perhaps the leading cause of its reputation as inscrutable
378             line noise. The simple alternative is to use the L<English|English>
379             module to give them clear names.
380              
381             $| = undef; #not ok
382              
383             use English qw(-no_match_vars);
384             local $OUTPUT_AUTOFLUSH = undef; #ok
385              
386             =head1 CONFIGURATION
387              
388             The scratch variables C<$_> and C<@_> are very common and are pretty
389             well understood, so they are exempt from this policy. The same goes
390             for the less-frequently-used default filehandle C<_> used by stat().
391             All the regexp capture variables (C<$1>, C<$2>, ...) are exempt too.
392             C<$]> is exempt because there is no L<English|English> equivalent and
393             L<Module::CoreList|Module::CoreList> is based upon it.
394              
395             You can add more exceptions to your configuration. In your
396             perlcriticrc file, add a block like this:
397              
398             [Variables::ProhibitPunctuationVars]
399             allow = $@ $!
400              
401             The C<allow> property should be a whitespace-delimited list of
402             punctuation variables.
403              
404             Other configuration options control the parsing of interpolated
405             strings in the search for forbidden variables. They have no effect
406             on detecting punctuation variables outside of interpolated strings.
407              
408             [Variables::ProhibitPunctuationVars]
409             string_mode = thorough
410              
411             The option C<string_mode> controls whether and how interpolated
412             strings are searched for punctuation variables. Setting
413             C<string_mode = thorough>, the default, checks for special cases
414             that may look like punctuation variables but aren't, for example
415             C<$#foo>, an array index count; C<$$bar>, a scalar dereference; or
416             C<$::baz>, a global symbol.
417              
418             Setting C<string_mode = disable> causes all interpolated strings to
419             be ignored entirely.
420              
421             Setting C<string_mode = simple> uses a simple regular expression to
422             find matches. In this mode, the magic variables C<$$>, C<$'>, C<$#>
423             and C<$:> are ignored within interpolated strings due to the high
424             risk of false positives. Simple mode is retained from an earlier
425             draft of the interpolated- strings code. Its use is only recommended
426             as a workaround if bugs appear in thorough mode.
427              
428             The C<string_mode> option will go away when the parsing of
429             interpolated strings is implemented in PPI. See L</CAVEATS> below.
430              
431              
432             =head1 BUGS
433              
434             Punctuation variables that confuse PPI's document parsing may not be
435             detected correctly or at all, and may prevent detection of
436             subsequent ones. In particular, C<$"> is known to cause difficulties
437             in interpolated strings.
438              
439              
440             =head1 CAVEATS
441              
442             ProhibitPunctuationVars relies exclusively on PPI to find
443             punctuation variables in code, but does all the parsing itself for
444             interpolated strings. When, at some point, this functionality is
445             transferred to PPI, ProhibitPunctuationVars will cease doing the
446             interpolating and the C<string_mode> option will go away.
447              
448              
449             =head1 AUTHOR
450              
451             Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
452              
453              
454             =head1 COPYRIGHT
455              
456             Copyright (c) 2005-2023 Imaginative Software Systems
457              
458             This program is free software; you can redistribute it and/or modify
459             it under the same terms as Perl itself. The full text of this license
460             can be found in the LICENSE file included with this module.
461              
462             =cut
463              
464             # Local Variables:
465             # mode: cperl
466             # cperl-indent-level: 4
467             # fill-column: 78
468             # indent-tabs-mode: nil
469             # c-indentation-style: bsd
470             # End:
471             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :