File Coverage

blib/lib/Perl/Critic/Policy/ValuesAndExpressions/ProhibitComplexVersion.pm
Criterion Covered Total %
statement 72 73 98.6
branch 30 34 88.2
condition 10 12 83.3
subroutine 18 18 100.0
pod 4 5 80.0
total 134 142 94.3


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