File Coverage

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


line stmt bran cond sub pod time code
1             package Perl::Lint::Policy::ValuesAndExpressions::ProhibitMagicNumbers;
2 134     134   99856 use strict;
  134         263  
  134         4793  
3 134     134   580 use warnings;
  134         202  
  134         4029  
4 134     134   555 no warnings qw/numeric/;
  134         203  
  134         5197  
5 134     134   1099 use Perl::Lint::Constants::Type;
  134         207  
  134         79642  
6 134     134   834 use parent "Perl::Lint::Policy";
  134         211  
  134         749  
7              
8             use constant {
9 134         229619 DESC => 'Unnamed numeric literals make code less maintainable', # TODO
10             EXPL => 'Unnamed numeric literals make code less maintainable',
11 134     134   8753 };
  134         261  
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 180 my $class = shift;
24 126         215 $file = shift;
25 126         175 $tokens = shift;
26 126         2215 my ($src, $args) = @_;
27              
28 126         472 %allowed_values = (
29             0 => 1,
30             1 => 1,
31             2 => 1,
32             );
33              
34 126         444 %allowed_types = (
35             Int => 1,
36             Float => 1,
37             );
38              
39 126         479 %constant_creator_subroutines = (
40             plan => 1,
41             Readonly => 1,
42             const => 1,
43             );
44              
45 126         161 $allow_to_the_right_of_a_fat_comma = 1;
46              
47             # initializing
48 126 100       369 if (my $this_policies_arg = $args->{prohibit_magic_numbers}) {
49 56   100     300 $allow_to_the_right_of_a_fat_comma =
50             $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       169 if (defined $allowed_values) {
54 29         71 delete $allowed_values{2}; # remove `2` from allowed list when allowed_values is specified
55              
56 29         88 for my $allowed_value (split /\s+/, $allowed_values) {
57 36         90 my ($begin, $end) = split /[.][.]/, $allowed_value; # for range notation (e.g. `1..42`)
58 36 100 66     155 if (defined $begin && defined $end) {
59             # used range notation
60 8         27 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         47 for (my $num = $begin; $num <= $end; $num += $delta) {
64 53         161 $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         116 my $allowed_types = $this_policies_arg->{allowed_types};
75 56 100       172 if (defined $allowed_types) {
76 19         52 delete $allowed_types{Float}; # remove `Float` from allowed types list when allowed_types is specified
77              
78 19         50 for my $allowed_type (split /\s+/, $allowed_types) {
79 15         42 $allowed_types{$allowed_type} = 1;
80             }
81             }
82              
83 56         111 my $constant_creator_subroutines = $this_policies_arg->{constant_creator_subroutines};
84 56 100       145 if (defined $constant_creator_subroutines) {
85 1         4 for my $sub (split /\s+/, $constant_creator_subroutines) {
86 1         4 $constant_creator_subroutines{$sub} = 1;
87             }
88             }
89             }
90              
91 126         158 my @violations;
92 126         456 for (my $i = 0, my $token_type, my $token_data; my $token = $tokens->[$i]; $i++) {
93 834         673 $is_readonly_array1_ctx = 0;
94              
95 834         840 $token_type = $token->{type};
96 834         793 $token_data = $token->{data};
97              
98 834 100 100     3589 if (
      100        
      66        
99             $token_type == USE_DECL ||
100             $token_type == REQUIRE_DECL ||
101             ($token_type == KEY && $constant_creator_subroutines{$token_data})
102             ) {
103 56         137 for ($i++; $token = $tokens->[$i]; $i++) {
104 267         209 $token_type = $token->{type};
105 267 100       559 if ($token_type == SEMI_COLON) {
106 56         74 last;
107             }
108             }
109 56         120 next;
110             }
111              
112 778 100 100     1389 if ($token_type == NAMESPACE && $token_data eq 'Readonly') {
113 8 50       22 $token = $tokens->[++$i] or last;
114              
115 8 50       25 if ($token->{type} == NAMESPACE_RESOLVER) {
116 8 50       20 $token = $tokens->[++$i] or last;
117 8         8 $token_data = $token->{data};
118              
119 8 50       18 if ($token->{type} == NAMESPACE) {
120 8 100 100     42 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         12 $token_type = $token->{type};
125 14 100       51 if ($token_type == SEMI_COLON) {
126 2         4 last;
127             }
128             }
129 2         6 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         3 $token_data = $token->{data};
138              
139 1         1 $is_readonly_array1_ctx = 1;
140              
141             # no break!
142             }
143             }
144             }
145             }
146              
147             # for the $VERSION variable
148 776 50 66     1497 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         238 for ($i++; $token = $tokens->[$i]; $i++) {
154 1518 100       2996 last if $token->{type} == SEMI_COLON;
155             }
156 151         232 next;
157             }
158              
159 625 100 100     1510 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         116 push @violations, @{$class->_scan(\$i)};
  94         281  
164 94         257 next;
165             }
166              
167 531 100       787 if ($token_type == FUNCTION) {
168 4         5 $token = $tokens->[++$i];
169              
170 4         6 my $statement = [];
171 4         4 my @statements = ();
172              
173 4         5 my $lbnum = 1;
174 4         8 for ($i++; $token = $tokens->[$i]; $i++) {
175 19         16 $token_type = $token->{type};
176              
177 19 50       37 if ($token_type == LEFT_BRACE) {
    100          
    100          
178 0         0 $lbnum++;
179             }
180             elsif ($token_type == RIGHT_BRACE) {
181 4 50       10 last if --$lbnum <= 0;
182             }
183             elsif ($token_type == SEMI_COLON) {
184 5         5 push @statements, $statement;
185 5         9 $statement = [];
186             }
187             else {
188 10         17 push @$statement, $token;
189             }
190             }
191              
192 4 100       8 if (scalar @statements > 1) { # when exists multiple statements in function
193 2         3 my $last_statement = pop @statements;
194              
195 2 50       5 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       4 if ($return_value_token->{type} == INT) {
    0          
202 2         6 $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 2         10 push @violations, {
210             filename => $file,
211             line => $token->{line},
212             description => DESC,
213             explanation => EXPL,
214             policy => __PACKAGE__,
215             };
216             }
217             }
218              
219 4         12 next;
220             }
221              
222 527 100 100     1620 if ($token_type == FOR_STATEMENT || $token_type == FOREACH_STATEMENT) {
223 3         10 for ($i++; $token = $tokens->[$i]; $i++) {
224 22         17 $token_type = $token->{type};
225              
226 22 100 100     115 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       5 my $begin = $tokens->[$i-1] or last;
231 2 50       3 my $end = $tokens->[$i+1] or last;
232 2 100 66     11 if ($begin->{type} == INT && $end->{type} == INT) {
233 1 50 33     6 if (!$allowed_values{$begin->{data}} || !$allowed_values{$end->{data}}) {
234 1         6 push @violations, {
235             filename => $file,
236             line => $token->{line},
237             description => DESC,
238             explanation => EXPL,
239             policy => __PACKAGE__,
240             };
241             }
242             }
243             }
244             }
245 3         7 next;
246             }
247              
248             # for index of array
249 524 100       1354 if ($token_type == LEFT_BRACKET) {
250 4 50       22 my $next_token = $tokens->[$i+1] or last;
251 4 50       11 if ($next_token->{type} == INT) {
252 4         8 my $int_token = $next_token;
253 4 50       12 $next_token = $tokens->[$i+2] or last;
254              
255 4         4 my $invalid_token;
256 4 100       13 if ($next_token->{type} == RIGHT_BRACKET) {
    50          
257 3         11 my $num = $int_token->{data} + 0;
258 3 100 100     18 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         2 $invalid_token = $next_token;
264             }
265              
266 4 100       9 if ($invalid_token) {
267 2         11 push @violations, {
268             filename => $file,
269             line => $invalid_token->{line},
270             description => DESC,
271             explanation => EXPL,
272             policy => __PACKAGE__,
273             };
274             }
275             }
276 4         10 next;
277             }
278             }
279              
280 126         558 return \@violations;
281             }
282              
283             my $is_in_assigning_context;
284             sub _scan {
285 273     273   274 my ($class, $i) = @_;
286              
287 273 50       503 my $token = $tokens->[$$i] or return;
288 273         248 my $token_type = $token->{type};
289 273         293 my $token_data = $token->{data};
290              
291 273 100       550 if ($token_type == ASSIGN) {
    100          
292 89         109 $is_in_assigning_context = 1;
293              
294 89 50       238 $token = $tokens->[++$$i] or return;
295 89         121 $token_type = $token->{type};
296 89         121 $token_data = $token->{data};
297             }
298             elsif ($token_type == ARROW) {
299 13         18 $is_in_assigning_context = 0;
300 13 100 100     44 if (!$allow_to_the_right_of_a_fat_comma || $is_readonly_array1_ctx) {
301 10         9 $is_in_assigning_context = 1;
302             }
303              
304 13 50       32 $token = $tokens->[++$$i] or return;
305 13         19 $token_type = $token->{type};
306 13         12 $token_data = $token->{data};
307             }
308              
309 273         254 my $invalid_token;
310              
311             my @violations;
312 273 100       624 if ($token_type == DOUBLE) {
    100          
    100          
    100          
313 39         100 $invalid_token = $class->_validate_doble_token($token, $$i);
314             }
315             elsif ($token_type == INT) {
316 122         240 $invalid_token = $class->_validate_int_token($token);
317             }
318             elsif ($token_type == LEFT_PAREN) {
319 9         10 my $lpnum = 1;
320 9         28 for ($$i++; $token = $tokens->[$$i]; $$i++) {
321 60         53 $token_type = $token->{type};
322 60 100       99 if ($token_type == LEFT_PAREN) {
    100          
323 1         3 $lpnum++;
324             }
325             elsif ($token_type == RIGHT_PAREN) {
326 10 100       25 last if --$lpnum <= 0;
327             }
328             else {
329 49         35 push @violations, @{$class->_scan($i)};
  49         76  
330             }
331             }
332             }
333             elsif ($token_type == LEFT_BRACKET) {
334 12         16 my $lbnum = 1;
335              
336 12         37 for ($$i++; $token = $tokens->[$$i]; $$i++) {
337 146         153 $token_type = $token->{type};
338 146 100       218 if ($token_type == LEFT_BRACKET) {
    100          
339 2         6 $lbnum++;
340             }
341             elsif ($token_type == RIGHT_BRACKET) {
342 14 100       40 last if --$lbnum <= 0;
343             }
344             else {
345 130         97 push @violations, @{$class->_scan($i)};
  130         195  
346             }
347             }
348             }
349              
350 273 100 100     857 if ($is_in_assigning_context && $invalid_token) {
351 42         201 push @violations, {
352             filename => $file,
353             line => $invalid_token->{line},
354             description => DESC,
355             explanation => EXPL,
356             policy => __PACKAGE__,
357             };
358             }
359              
360 273         677 return \@violations;
361             }
362              
363             sub _validate_int_token {
364 124     124   155 my ($class, $token) = @_;
365              
366 124         153 my $token_data = $token->{data};
367              
368 124 100       368 if (my ($base_type) = $token_data =~ /\A[0-9]([b0xe]).+\z/) {
369 20 100       102 if ($1 eq 'b') {
    100          
    100          
    50          
370 5 100       21 return $token if !$allowed_types{Binary};
371             }
372             elsif ($1 eq '0') {
373 6 100       21 return $token if !$allowed_types{Octal};
374             }
375             elsif ($1 eq 'x') {
376 5 100       30 return $token if !$allowed_types{Hex};
377             }
378             elsif ($1 eq 'e') {
379 4 100       16 return $token if !$allowed_types{Exp};
380             }
381             }
382              
383 114 50       270 if (!$allowed_types{Int}) {
384 0         0 return $token;
385             }
386              
387 114 100 66     450 if (!$allowed_values{all_integers} && !$allowed_values{$token_data+0}) { # `+0` to convert to number
388 27         51 return $token;
389             }
390              
391 87         142 return;
392             }
393              
394             sub _validate_doble_token {
395 39     39   49 my ($class, $token, $i) = @_;
396              
397 39         62 my $token_data = $token->{data};
398              
399 39 100 66     458 if ($i && $allowed_types{Float} && $allowed_values{$token_data+0}) { # `+0` to convert to number
    100 100        
      66        
400 30         54 my $next_token = $tokens->[$i+1];
401 30 50 66     121 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         17 return $token;
407             }
408              
409 31         40 return;
410             }
411              
412             1;
413