File Coverage

blib/lib/Perl/Lint/Policy/ValuesAndExpressions/ProhibitMagicNumbers.pm
Criterion Covered Total %
statement 189 194 97.4
branch 124 152 81.5
condition 58 73 79.4
subroutine 10 10 100.0
pod 0 1 0.0
total 381 430 88.6


line stmt bran cond sub pod time code
1             package Perl::Lint::Policy::ValuesAndExpressions::ProhibitMagicNumbers;
2 134     134   73606 use strict;
  134         177  
  134         3166  
3 134     134   409 use warnings;
  134         203  
  134         2851  
4 134     134   397 no warnings qw/numeric/;
  134         161  
  134         3999  
5 134     134   816 use Perl::Lint::Constants::Type;
  134         145  
  134         56798  
6 134     134   583 use parent "Perl::Lint::Policy";
  134         156  
  134         535  
7              
8             use constant {
9 134         169514 DESC => 'Unnamed numeric literals make code less maintainable', # TODO
10             EXPL => 'Unnamed numeric literals make code less maintainable',
11 134     134   6078 };
  134         165  
12              
13             my $file;
14             my $tokens;
15              
16             my %allowed_values;
17             my %allowed_types;
18             my %constant_creator_subroutines;
19             my $allow_to_the_right_of_a_fat_comma;
20             my $is_readonly_array1_ctx;
21              
22             sub evaluate {
23 126     126 0 191 my $class = shift;
24 126         162 $file = shift;
25 126         166 $tokens = shift;
26 126         1725 my ($src, $args) = @_;
27              
28 126         486 %allowed_values = (
29             0 => 1,
30             1 => 1,
31             2 => 1,
32             );
33              
34 126         356 %allowed_types = (
35             Int => 1,
36             Float => 1,
37             );
38              
39 126         399 %constant_creator_subroutines = (
40             plan => 1,
41             Readonly => 1,
42             const => 1,
43             );
44              
45 126         174 $allow_to_the_right_of_a_fat_comma = 1;
46              
47             # initializing
48 126 100       359 if (my $this_policies_arg = $args->{prohibit_magic_numbers}) {
49             $allow_to_the_right_of_a_fat_comma =
50 56   100     237 $this_policies_arg->{allow_to_the_right_of_a_fat_comma} // 1;
51              
52 56         126 my $allowed_values = $this_policies_arg->{allowed_values};
53 56 100       171 if (defined $allowed_values) {
54 29         52 delete $allowed_values{2}; # remove `2` from allowed list when allowed_values is specified
55              
56 29         111 for my $allowed_value (split /\s+/, $allowed_values) {
57 36         82 my ($begin, $end) = split /[.][.]/, $allowed_value; # for range notation (e.g. `1..42`)
58 36 100 66     142 if (defined $begin && defined $end) {
59             # used range notation
60 8         22 my ($delta) = $end =~ /:by [(] (.+) [)] \z/x; # for range notation with by (e.g. `-2.0..2.0:by(0.5)`)
61 8   100     24 $delta //= 1; # default delta
62              
63 8         42 for (my $num = $begin; $num <= $end; $num += $delta) {
64 53         157 $allowed_values{$num} = 1;
65             }
66             }
67             else {
68             # not used range notation
69 28         65 $allowed_values{$allowed_value} = 1;
70             }
71             }
72             }
73              
74 56         113 my $allowed_types = $this_policies_arg->{allowed_types};
75 56 100       132 if (defined $allowed_types) {
76 19         34 delete $allowed_types{Float}; # remove `Float` from allowed types list when allowed_types is specified
77              
78 19         56 for my $allowed_type (split /\s+/, $allowed_types) {
79 15         29 $allowed_types{$allowed_type} = 1;
80             }
81             }
82              
83 56         98 my $constant_creator_subroutines = $this_policies_arg->{constant_creator_subroutines};
84 56 100       159 if (defined $constant_creator_subroutines) {
85 1         5 for my $sub (split /\s+/, $constant_creator_subroutines) {
86 1         11 $constant_creator_subroutines{$sub} = 1;
87             }
88             }
89             }
90              
91 126         125 my @violations;
92 126         362 for (my $i = 0, my $token_type, my $token_data; my $token = $tokens->[$i]; $i++) {
93 834         593 $is_readonly_array1_ctx = 0;
94              
95 834         621 $token_type = $token->{type};
96 834         699 $token_data = $token->{data};
97              
98 834 100 100     2984 if (
      66        
      66        
99             $token_type == USE_DECL ||
100             $token_type == REQUIRE_DECL ||
101             ($token_type == KEY && $constant_creator_subroutines{$token_data})
102             ) {
103 56         104 for ($i++; $token = $tokens->[$i]; $i++) {
104 267         174 $token_type = $token->{type};
105 267 100       442 if ($token_type == SEMI_COLON) {
106 56         54 last;
107             }
108             }
109 56         93 next;
110             }
111              
112 778 100 100     1175 if ($token_type == NAMESPACE && $token_data eq 'Readonly') {
113 8 50       19 $token = $tokens->[++$i] or last;
114              
115 8 50       19 if ($token->{type} == NAMESPACE_RESOLVER) {
116 8 50       14 $token = $tokens->[++$i] or last;
117 8         12 $token_data = $token->{data};
118              
119 8 50       15 if ($token->{type} == NAMESPACE) {
120 8 100 100     36 if ($token_data eq 'Scalar' || $token_data eq 'Array') {
    100          
121             # when `Readonly::Scalar` or `Readonly::Array`,
122             # skip tokens to semi colon (means don't evaluate).
123 2         6 for ($i++; $token = $tokens->[$i]; $i++) {
124 14         9 $token_type = $token->{type};
125 14 100       37 if ($token_type == SEMI_COLON) {
126 2         2 last;
127             }
128             }
129 2         7 next;
130             }
131             elsif ($token_data eq 'Array1') {
132             # when `Readonly::Array1`
133 1         2 $i += 2; # skip to assigning token
134              
135 1         2 $token = $tokens->[$i];
136 1         2 $token_type = $token->{type};
137 1         2 $token_data = $token->{data};
138              
139 1         2 $is_readonly_array1_ctx = 1;
140              
141             # no break!
142             }
143             }
144             }
145             }
146              
147             # for the $VERSION variable
148 776 50 66     1123 if (
      66        
149             $token_data eq '$VERSION' &&
150             ($token_type == VAR || $token_type == GLOBAL_VAR)
151             ) {
152             # skip to end of line. Don't evaluate it.
153 151         189 for ($i++; $token = $tokens->[$i]; $i++) {
154 1518 100       2433 last if $token->{type} == SEMI_COLON;
155             }
156 151         187 next;
157             }
158              
159 625 100 100     1287 if (
      66        
      66        
160             $token_type == ASSIGN ||
161             ($token_type == ARROW && (!$allow_to_the_right_of_a_fat_comma || $is_readonly_array1_ctx))
162             ) {
163 94         105 push @violations, @{$class->_scan(\$i)};
  94         281  
164 94         231 next;
165             }
166              
167 531 100       641 if ($token_type == FUNCTION) {
168 4         8 $token = $tokens->[++$i];
169              
170 4         5 my $statement = [];
171 4         5 my @statements = ();
172              
173 4         4 my $lbnum = 1;
174 4         8 for ($i++; $token = $tokens->[$i]; $i++) {
175 19         17 $token_type = $token->{type};
176              
177 19 50       35 if ($token_type == LEFT_BRACE) {
    100          
    100          
178 0         0 $lbnum++;
179             }
180             elsif ($token_type == RIGHT_BRACE) {
181 4 50       11 last if --$lbnum <= 0;
182             }
183             elsif ($token_type == SEMI_COLON) {
184 5         6 push @statements, $statement;
185 5         11 $statement = [];
186             }
187             else {
188 10         22 push @$statement, $token;
189             }
190             }
191              
192 4 100       9 if (scalar @statements > 1) { # when exists multiple statements in function
193 2         3 my $last_statement = pop @statements;
194              
195 2 50       7 my $return_value_token = pop @$last_statement or next;
196 2 50       6 if ($return_value_token->{type} == RETURN) {
197 0 0       0 $return_value_token = pop @$last_statement or next;
198             }
199              
200 2         3 my $invalid_token;
201 2 50       3 if ($return_value_token->{type} == INT) {
    0          
202 2         10 $invalid_token = $class->_validate_int_token($return_value_token);
203             }
204             elsif ($return_value_token->{type} == DOUBLE) {
205 0         0 $invalid_token = $class->_validate_doble_token($return_value_token);
206             }
207              
208 2 50       5 if ($invalid_token) {
209             push @violations, {
210             filename => $file,
211             line => $token->{line},
212 2         11 description => DESC,
213             explanation => EXPL,
214             policy => __PACKAGE__,
215             };
216             }
217             }
218              
219 4         12 next;
220             }
221              
222 527 100 100     1482 if ($token_type == FOR_STATEMENT || $token_type == FOREACH_STATEMENT) {
223 3         11 for ($i++; $token = $tokens->[$i]; $i++) {
224 22         16 $token_type = $token->{type};
225              
226 22 100 100     79 if ($token_type == SEMI_COLON || $token_type == LEFT_BRACE) {
    100          
227 3         4 last;
228             }
229             elsif ($token_type == SLICE) { # e.g. for my $foo (1..42)
230 2 50       6 my $begin = $tokens->[$i-1] or last;
231 2 50       7 my $end = $tokens->[$i+1] or last;
232 2 100 66     10 if ($begin->{type} == INT && $end->{type} == INT) {
233 1 50 33     7 if (!$allowed_values{$begin->{data}} || !$allowed_values{$end->{data}}) {
234             push @violations, {
235             filename => $file,
236             line => $token->{line},
237 1         6 description => DESC,
238             explanation => EXPL,
239             policy => __PACKAGE__,
240             };
241             }
242             }
243             }
244             }
245 3         8 next;
246             }
247              
248             # for index of array
249 524 100       1088 if ($token_type == LEFT_BRACKET) {
250 4 50       13 my $next_token = $tokens->[$i+1] or last;
251 4 50       10 if ($next_token->{type} == INT) {
252 4         5 my $int_token = $next_token;
253 4 50       8 $next_token = $tokens->[$i+2] or last;
254              
255 4         6 my $invalid_token;
256 4 100       12 if ($next_token->{type} == RIGHT_BRACKET) {
    50          
257 3         8 my $num = $int_token->{data} + 0;
258 3 100 100     15 if (!$allowed_values{$num} && $num ne -1) { # -1 is allowed specially when it is used as index of array
259 1         2 $invalid_token = $int_token;
260             }
261             }
262             elsif ($next_token->{type} != COMMA) { # if it is not enumeration (probably it is any handling for index of array)
263 1         3 $invalid_token = $next_token;
264             }
265              
266 4 100       9 if ($invalid_token) {
267             push @violations, {
268             filename => $file,
269             line => $invalid_token->{line},
270 2         9 description => DESC,
271             explanation => EXPL,
272             policy => __PACKAGE__,
273             };
274             }
275             }
276 4         10 next;
277             }
278             }
279              
280 126         459 return \@violations;
281             }
282              
283             my $is_in_assigning_context;
284             sub _scan {
285 273     273   255 my ($class, $i) = @_;
286              
287 273 50       436 my $token = $tokens->[$$i] or return;
288 273         216 my $token_type = $token->{type};
289 273         265 my $token_data = $token->{data};
290              
291 273 100       438 if ($token_type == ASSIGN) {
    100          
292 89         110 $is_in_assigning_context = 1;
293              
294 89 50       234 $token = $tokens->[++$$i] or return;
295 89         135 $token_type = $token->{type};
296 89         128 $token_data = $token->{data};
297             }
298             elsif ($token_type == ARROW) {
299 13         19 $is_in_assigning_context = 0;
300 13 100 100     42 if (!$allow_to_the_right_of_a_fat_comma || $is_readonly_array1_ctx) {
301 10         13 $is_in_assigning_context = 1;
302             }
303              
304 13 50       29 $token = $tokens->[++$$i] or return;
305 13         14 $token_type = $token->{type};
306 13         17 $token_data = $token->{data};
307             }
308              
309 273         226 my $invalid_token;
310              
311             my @violations;
312 273 100       579 if ($token_type == DOUBLE) {
    100          
    100          
    100          
313 39         110 $invalid_token = $class->_validate_doble_token($token, $$i);
314             }
315             elsif ($token_type == INT) {
316 122         310 $invalid_token = $class->_validate_int_token($token);
317             }
318             elsif ($token_type == LEFT_PAREN) {
319 9         11 my $lpnum = 1;
320 9         32 for ($$i++; $token = $tokens->[$$i]; $$i++) {
321 60         51 $token_type = $token->{type};
322 60 100       90 if ($token_type == LEFT_PAREN) {
    100          
323 1         4 $lpnum++;
324             }
325             elsif ($token_type == RIGHT_PAREN) {
326 10 100       23 last if --$lpnum <= 0;
327             }
328             else {
329 49         41 push @violations, @{$class->_scan($i)};
  49         69  
330             }
331             }
332             }
333             elsif ($token_type == LEFT_BRACKET) {
334 12         14 my $lbnum = 1;
335              
336 12         35 for ($$i++; $token = $tokens->[$$i]; $$i++) {
337 146         118 $token_type = $token->{type};
338 146 100       189 if ($token_type == LEFT_BRACKET) {
    100          
339 2         5 $lbnum++;
340             }
341             elsif ($token_type == RIGHT_BRACKET) {
342 14 100       33 last if --$lbnum <= 0;
343             }
344             else {
345 130         88 push @violations, @{$class->_scan($i)};
  130         176  
346             }
347             }
348             }
349              
350 273 100 100     787 if ($is_in_assigning_context && $invalid_token) {
351             push @violations, {
352             filename => $file,
353             line => $invalid_token->{line},
354 42         200 description => DESC,
355             explanation => EXPL,
356             policy => __PACKAGE__,
357             };
358             }
359              
360 273         606 return \@violations;
361             }
362              
363             sub _validate_int_token {
364 124     124   108 my ($class, $token) = @_;
365              
366 124         124 my $token_data = $token->{data};
367              
368 124 100       295 if (my ($base_type) = $token_data =~ /\A[0-9]([b0xe]).+\z/) {
369 20 100       96 if ($1 eq 'b') {
    100          
    100          
    50          
370 5 100       15 return $token if !$allowed_types{Binary};
371             }
372             elsif ($1 eq '0') {
373 6 100       18 return $token if !$allowed_types{Octal};
374             }
375             elsif ($1 eq 'x') {
376 5 100       18 return $token if !$allowed_types{Hex};
377             }
378             elsif ($1 eq 'e') {
379 4 100       14 return $token if !$allowed_types{Exp};
380             }
381             }
382              
383 114 50       177 if (!$allowed_types{Int}) {
384 0         0 return $token;
385             }
386              
387 114 100 66     395 if (!$allowed_values{all_integers} && !$allowed_values{$token_data+0}) { # `+0` to convert to number
388 27         46 return $token;
389             }
390              
391 87         122 return;
392             }
393              
394             sub _validate_doble_token {
395 39     39   43 my ($class, $token, $i) = @_;
396              
397 39         45 my $token_data = $token->{data};
398              
399 39 100 33     406 if ($i && $allowed_types{Float} && $allowed_values{$token_data+0}) { # `+0` to convert to number
    100 100        
      66        
400 30         46 my $next_token = $tokens->[$i+1];
401 30 50 66     115 if ($next_token && $next_token->{type} == DOUBLE) {
402 0         0 return $next_token;
403             }
404             }
405             elsif (!$allowed_values{all_integers} || $token_data !~ /[.]0+\z/) {
406 8         23 return $token;
407             }
408              
409 31         46 return;
410             }
411              
412             1;
413