File Coverage

blib/lib/Perl/Critic/Policy/ValuesAndExpressions/ProhibitComplexVersion.pm
Criterion Covered Total %
statement 54 64 84.3
branch 15 34 44.1
condition 3 12 25.0
subroutine 15 15 100.0
pod 4 5 80.0
total 91 130 70.0


line stmt bran cond sub pod time code
1             package Perl::Critic::Policy::ValuesAndExpressions::ProhibitComplexVersion;
2              
3 40     40   28093 use 5.010001;
  40         187  
4 40     40   293 use strict;
  40         93  
  40         847  
5 40     40   223 use warnings;
  40         90  
  40         1254  
6              
7 40     40   241 use Perl::Critic::Utils qw{ :booleans :characters :severities };
  40         134  
  40         2142  
8 40         2354 use Perl::Critic::Utils::PPI qw{
9             get_next_element_in_same_simple_statement
10             get_previous_module_used_on_same_line
11             is_ppi_simple_statement
12 40     40   13104 };
  40         89  
13 40     40   267 use Readonly;
  40         105  
  40         1795  
14              
15 40     40   315 use parent 'Perl::Critic::Policy';
  40         103  
  40         1768  
16              
17             our $VERSION = '1.150';
18              
19             #-----------------------------------------------------------------------------
20              
21             Readonly::Scalar my $DOLLAR => q<$>;
22             # All uses of the $DOLLAR variable below are to prevent false failures in
23             # xt/93_version.t.
24             Readonly::Scalar my $VERSION_MODULE => q<version>;
25             Readonly::Scalar my $VERSION_VARIABLE => $DOLLAR . q<VERSION>;
26              
27             Readonly::Scalar my $DESC =>
28             $DOLLAR . q<VERSION value should not come from outside module>;
29             Readonly::Scalar my $EXPL =>
30             q<If the version comes from outside the module, you can get everything from unexpected version changes to denial-of-service attacks.>;
31              
32             #-----------------------------------------------------------------------------
33              
34             sub supported_parameters { return (
35             {
36 90     90 0 2094 name => 'forbid_use_version',
37             description =>
38             qq<Make "use version; our ${DOLLAR}VERSION = qv('1.2.3');" a violation of this policy.>,
39             default_string => $FALSE,
40             behavior => 'boolean',
41             },
42             );
43             }
44 74     74 1 319 sub default_severity { return $SEVERITY_MEDIUM }
45 74     74 1 335 sub default_themes { return qw( core maintenance ) }
46 30     30 1 74 sub applies_to { return 'PPI::Token::Symbol' }
47              
48             #-----------------------------------------------------------------------------
49              
50             sub violates {
51 172     172 1 325 my ( $self, $elem, $doc ) = @_;
52              
53             # Any variable other than $VERSION is ignored.
54 172 100       309 return if $VERSION_VARIABLE ne $elem->content();
55              
56             # We are only interested in assignments to $VERSION, but it might be a
57             # list assignment, so if we do not find an assignment, we move up the
58             # parse tree. If we hit a statement (or no parent at all) we do not
59             # understand the code to be an assignment statement, and we simply return.
60 27         161 my $operator;
61             return if
62 27 50 33     116 not $operator = get_next_element_in_same_simple_statement( $elem )
63             or $EQUAL ne $operator;
64              
65             # Find the simple statement we are in. If we can not find it, abandon the
66             # attempt to analyze the code.
67 27 50       1422 my $statement = $self->_get_simple_statement( $elem )
68             or return;
69              
70             # Check all symbols in the statement for violation.
71 27         399 my $exception;
72 27 50       115 return $exception if
73             $exception =
74             $self->_validate_fully_qualified_symbols($elem, $statement, $doc);
75              
76             # At this point we have found no data that is explicitly from outside the
77             # file. If the author wants to use a $VERSION from another module, _and_
78             # wants MM->parse_version to understand it, the other module must be used
79             # on the same line. So we assume no violation unless this has been done.
80 27 50       183 my $module = get_previous_module_used_on_same_line( $elem )
81             or return;
82              
83             # We make an exception for 'use version' unless configured otherwise; so
84             # let it be written, so let it be done.
85 0 0 0     0 return if $module eq $VERSION_MODULE and not $self->{_forbid_use_version};
86              
87             # We assume nefarious intent if we have any other module used on the same
88             # line as the $VERSION assignment.
89 0         0 return $self->violation( $DESC, $EXPL, $elem );
90             }
91              
92             #-----------------------------------------------------------------------------
93              
94             # Return the simple statement that contains our element. The classification
95             # done by is_ppi_simple_statement is not quite good enough in this case -- if
96             # our parent is a PPI::Structure::List, we want to keep looking.
97              
98             sub _get_simple_statement {
99 27     27   117 my ( $self, $elem ) = @_;
100              
101 27         58 my $statement = $elem;
102              
103 27         101 while ( $statement) {
104 54         195 my $parent;
105 54 100       178 if ( is_ppi_simple_statement( $statement ) ) {
106 27 50 33     208 return $statement if
107             not $parent = $statement->parent()
108             or not $parent->isa( 'PPI::Structure::List' );
109 0         0 $statement = $parent;
110             } else {
111 27         228 $statement = $statement->parent();
112             }
113             }
114              
115 0         0 return;
116             }
117              
118             #-----------------------------------------------------------------------------
119              
120             sub _validate_fully_qualified_symbols {
121 27     27   70 my ( $self, $elem, $statement, $doc ) = @_;
122              
123             # Find the package(s) in this file.
124             my %local_package =
125 27         118 map { $_->schild( 1 ) => 1 }
126 27 50       62 @{ $doc->find( 'PPI::Statement::Package' ) || [] };
  27         89  
127 27         615 $local_package{main} = 1; # For completeness.
128              
129             # Check all symbols in the statement for violation.
130 27         52 foreach my $symbol (
131 27 50       83 @{ $statement->find( 'PPI::Token::Symbol' ) || [] }
132             ) {
133 27 50       8702 if ( $symbol->canonical() =~ m< \A [@\$%&] ([\w:]*) :: >smx ) {
134 0 0       0 $local_package{ $1 }
135             or return $self->violation( $DESC, $EXPL, $elem );
136             }
137             }
138              
139             # Check all interpolatable strings in the statement for violation.
140             # TODO this does not correctly handle "@{[some_expression()]}".
141 27         426 foreach my $string (
142             @{
143             $statement->find(
144             sub {
145             return
146 216   33 216   2651 $_[1]->isa('PPI::Token::Quote::Double')
147             || $_[1]->isa('PPI::Token::Quote::Interpolate');
148             }
149             )
150 27 50       210 or []
151             }
152             ) {
153 0         0 my $unquoted = $string->string();
154 0         0 while (
155             $unquoted =~
156             m<
157             (?: \A | [^\\] )
158             (?: \\{2} )*
159             [@\$]
160             [{]?
161             ([\w:]*)
162             ::
163             >gsmx
164             ) {
165 0 0       0 next if $local_package{ $1 };
166              
167 0         0 return $self->violation( $DESC, $EXPL, $elem );
168             }
169             }
170              
171             # Check all words in the statement for violation.
172 27 50       469 foreach my $symbol ( @{ $statement->find( 'PPI::Token::Word' ) || [] } ) {
  27         82  
173 27 50       7483 if ( $symbol->content() =~ m/ \A ([\w:]*) :: /smx ) {
174             return $self->violation( $DESC, $EXPL, $elem )
175 0 0       0 if not $local_package{ $1 };
176             }
177             }
178              
179 27         236 return;
180             }
181              
182             1;
183              
184             __END__
185              
186             #-----------------------------------------------------------------------------
187              
188             =pod
189              
190             =head1 NAME
191              
192             Perl::Critic::Policy::ValuesAndExpressions::ProhibitComplexVersion - Prohibit version values from outside the module.
193              
194             =head1 AFFILIATION
195              
196             This Policy is part of the core L<Perl::Critic|Perl::Critic>
197             distribution.
198              
199              
200             =head1 DESCRIPTION
201              
202             One tempting way to keep a group of related modules at the same version number
203             is to have all of them import the version number from a designated module. For
204             example, module C<Foo::Master> could be the version master for the C<Foo>
205             package, and all other modules could use its C<$VERSION> by
206              
207             use Foo::Master; our $VERSION = $Foo::Master::VERSION;
208              
209             This turns out not to be a good idea, because all sorts of unintended things
210             can happen - anything from unintended version number changes to
211             denial-of-service attacks (since C<Foo::Master> is executed by the 'use').
212              
213             This policy examines statements that assign to C<$VERSION>, and declares a
214             violation under two circumstances: first, if that statement uses a
215             fully-qualified symbol that did not originate in a package declared in the
216             file; second if there is a C<use> statement on the same line that makes the
217             assignment.
218              
219             By default, an exception is made for C<use version;> because of its
220             recommendation by Perl Best Practices. See the C<forbid_use_version>
221             configuration variable if you do not want an exception made for C<use
222             version;>.
223              
224              
225             =head1 CONFIGURATION
226              
227             The construction
228              
229             use version; our $VERSION = qv('1.2.3');
230              
231             is exempt from this policy by default, because it is recommended by Perl Best
232             Practices. Should you wish to identify C<use version;> as a violation, add the
233             following to your perlcriticrc file:
234              
235             [ValuesAndExpressions::ProhibitComplexVersion]
236             forbid_use_version = 1
237              
238              
239             =head1 CAVEATS
240              
241             This code assumes that the hallmark of a violation is a 'use' on the same line
242             as the C<$VERSION> assignment, because that is the way to have it seen by
243             L<ExtUtils::MakeMaker|ExtUtils::MakeMaker>->parse_version(). Other ways to get
244             a version value from outside the module can be imagined, and this policy is
245             currently oblivious to them.
246              
247              
248             =head1 AUTHOR
249              
250             Thomas R. Wyant, III F<wyant at cpan dot org>
251              
252              
253             =head1 COPYRIGHT
254              
255             Copyright (c) 2009-2023 Tom Wyant
256              
257             This program is free software; you can redistribute it and/or modify
258             it under the same terms as Perl itself. The full text of this license
259             can be found in the LICENSE file included with this module.
260              
261             =cut
262              
263             # Local Variables:
264             # mode: cperl
265             # cperl-indent-level: 4
266             # fill-column: 78
267             # indent-tabs-mode: nil
268             # c-indentation-style: bsd
269             # End:
270             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :