File Coverage

blib/lib/Test/Whitespaces.pm
Criterion Covered Total %
statement 149 257 57.9
branch 41 112 36.6
condition 8 18 44.4
subroutine 18 30 60.0
pod n/a
total 216 417 51.8


line stmt bran cond sub pod time code
1             package Test::Whitespaces;
2             {
3             $Test::Whitespaces::VERSION = '1.2.1';
4             }
5              
6             # ABSTRACT: test source code for errors in whitespaces
7              
8 4     4   163594 use warnings;
  4         12  
  4         538  
9 4     4   25 use strict;
  4         8  
  4         151  
10 4     4   22 use utf8;
  4         12  
  4         31  
11 4     4   112 use open qw(:std :utf8);
  4         8  
  4         29  
12              
13 4     4   565 use Carp;
  4         10  
  4         421  
14 4     4   25 use Cwd qw(realpath);
  4         8  
  4         334  
15 4     4   26 use File::Find;
  4         6  
  4         362  
16 4     4   6564 use FindBin qw($Bin);
  4         5864  
  4         594  
17 4     4   6698 use Getopt::Long;
  4         130232  
  4         29  
18 4     4   836 use List::Util qw(max);
  4         9  
  4         545  
19 4     4   4730 use Pod::Usage;
  4         431125  
  4         747  
20 4     4   7149 use Term::ANSIColor qw(colored);
  4         64263  
  4         23979  
21              
22              
23              
24             my $true = 1;
25             my $false = '';
26              
27             $| = 1;
28              
29             my $current_test = 0;
30             my $verbose = $false;
31             my $print_ok_files = $true;
32             my @ignore;
33              
34             sub import {
35 4     4   57 my ($class, $args) = @_;
36              
37 4 50 33     51 if (defined $args and ref $args ne "HASH") {
38 0         0 croak "Test::Whitespaces expected to recieve hashref with params. Stopped";
39             }
40              
41 4 50       23 if (defined $args->{ignore}) {
42 0 0       0 croak "Parameter 'ignore' shoud contain ARRAY. Stopped" if ref $args->{ignore} ne "ARRAY";
43              
44 0         0 foreach (@{$args->{ignore}}) {
  0         0  
45 0 0       0 croak "Parameter 'ignore' shoud contain ARRAY with Regexp-es. Stopped" if ref $_ ne "Regexp";
46 0         0 push @ignore, $_;
47             }
48             }
49              
50 4 50       16 if (not defined $args->{dirs}) {
51 4         13 $args->{dirs} = [],
52             }
53              
54 4 50       17 if (not defined $args->{files}) {
55 4         22 $args->{files} = [];
56             }
57              
58 4 50       12534 if (not $args->{_only_load}) {
59 0         0 _check_dir($_) foreach @{$args->{dirs}};
  0         0  
60 0         0 _check_file($_) foreach @{$args->{files}};
  0         0  
61 0         0 _done_testing();
62             }
63             }
64              
65             sub _run_script {
66 0     0   0 my (%args) = @_;
67              
68 0 0       0 if (not defined $args{dir}) {
69 0         0 croak "_run_script expected to recieve param 'dir'. Stopped";
70             }
71              
72 0 0       0 if (not defined $args{file}) {
73 0         0 croak "_run_script expected to recieve param 'file'. Stopped";
74             }
75              
76 0 0       0 if (not defined $args{script}) {
77 0         0 croak "_run_script expected to recieve param 'script'. Stopped";
78             }
79              
80 0         0 my $opt;
81              
82 0         0 GetOptions (
83             "only_errors" => \$opt->{only_errors},
84             "help" => \$opt->{help},
85             "version" => \$opt->{version},
86             "verbose" => \$opt->{verbose},
87             );
88              
89 0 0       0 if ($opt->{help}) {
90 0         0 pod2usage({
91             -exitval => 0,
92             });
93             }
94              
95 0 0       0 if ($opt->{version}) {
96 0         0 print "$args{script} $Test::Whitespaces::VERSION\n";
97 0         0 exit 0;
98             }
99              
100 0 0 0     0 if ($args{script} eq 'test_whitespaces' and $opt->{only_errors}) {
101 0         0 $print_ok_files = $false;
102             }
103              
104 0 0 0     0 if ($args{script} eq 'whiter' and $opt->{verbose}) {
105 0         0 $verbose = $true;
106             }
107              
108 0 0       0 unless (@ARGV) {
109 0         0 print "No. Run me with some parameters, please.\n";
110 0         0 exit 1;
111             }
112              
113 0         0 foreach my $argv (@ARGV) {
114 0 0       0 if (-d $argv) {
    0          
115 0         0 $args{dir}->($argv);
116             } elsif (-T $argv) {
117 0         0 $args{file}->($argv);
118             } else {
119 0         0 print _colored(
120             "Fatal error. '$argv' is not a directory and it is not a text file.\n",
121             "red"
122             );
123 0         0 exit 1;
124             }
125             }
126             }
127              
128             # writing custom _is() because Test::More::is() output ugly additional info
129             sub _is {
130 0     0   0 my ($got, $expected, $text) = @_;
131              
132 0         0 $current_test++;
133              
134 0 0       0 if ($got eq $expected) {
135 0 0       0 if ($print_ok_files) {
136 0         0 print "ok $current_test - $text\n";
137             }
138             } else {
139 0         0 print _colored("not ok", "red");
140 0         0 print " $current_test - $text\n";
141 0         0 my $diff =
142             "# \n"
143             . "# ## Failed check on file '$text':\n"
144             . "# \n"
145             . _get_diff($got, $expected)
146             . "# \n"
147             . "# \n"
148             ;
149              
150 0 0       0 if ($ENV{HARNESS_ACTIVE}) {
151 0         0 print STDERR "\n";
152 0         0 print STDERR $diff;
153             } else {
154 0         0 print $diff;
155             }
156             }
157             }
158              
159             sub _done_testing {
160 0     0   0 print "1..$current_test\n";
161             };
162              
163             sub _colored {
164 7     7   15 my ($text, $color) = @_;
165              
166 7 50       51 if (-t STDOUT) {
167 0         0 return colored($text, $color);
168             } else {
169 7         26 return $text;
170             }
171             }
172              
173             sub _read_file {
174 0     0   0 my ($filename) = @_;
175              
176 0 0       0 open FILE, "< :encoding(UTF-8)", $filename or croak "Can't open file '$filename': $!. Stopped";
177 0         0 my @lines = ;
178 0         0 close FILE;
179              
180 0         0 my $content = join '', @lines;
181              
182 0         0 return $content;
183             }
184              
185             sub _write_file {
186 0     0   0 my ($filename, $content) = @_;
187              
188 0 0       0 open FILE, "> :encoding(UTF-8)", $filename or croak "Can't open file '$filename': $!. Stopped";
189 0         0 print FILE $content;
190 0         0 close FILE;
191              
192 0         0 return $false;
193             }
194              
195             sub _get_fixed_text {
196 20     20   35275 my ($original_text) = @_;
197              
198 20         44 my $fixed_text;
199              
200 20         98 my @lines = split(/\n/, $original_text);
201              
202 20         49 foreach my $line (@lines) {
203 32         58 $line =~ s{\t}{ }g;
204 32         159 $line =~ s{\s*$}{\n};
205 32         75 $fixed_text .= $line;
206             }
207              
208 20         87 $fixed_text .= "\n";
209 20         693 $fixed_text =~ s{\s*$}{\n};
210              
211 20         292 return $fixed_text;
212             }
213              
214             sub _get_diff {
215 7     7   13 my ($got, $expected) = @_;
216              
217 7 50       23 croak "Expected 'got'. Stopped" if not defined $got;
218 7 50       15 croak "Expected 'expected'. Stopped" if not defined $expected;
219              
220 7         13 my $return;
221              
222 7 100       16 if ($got eq "") {
223 1         81 $return .= "# line 1 ";
224 1         5 $return .= _colored("No \\n on line", "red");
225 1         3 $return .= "\n";
226              
227 1         3 return $return;
228             }
229              
230 6         10 my $diff = '';
231              
232 6         26 my @got_lines = split /\n/, $got, -1;
233 6         22 my @expected_lines = split /\n/, $expected, -1;
234              
235 6         8 my %error_lines; # key - line number, value - line content
236              
237 6         30 my $lines_in_file = max(scalar @got_lines, scalar @expected_lines);
238 6         16 foreach my $line_number (1 .. $lines_in_file) {
239 16         26 my $i = $line_number - 1;
240              
241 16 100       34 if (not defined $got_lines[$i]) {
242             # no \n on the last line
243 2         7 $error_lines{$line_number-1} = $got_lines[$i-1];
244 2         7 next;
245             }
246              
247 14 100 66     76 if (not defined $expected_lines[$i] or ($got_lines[$i] ne $expected_lines[$i])) {
248             # empty lines in the end of file or some problems with whitespaces
249 6         28 $error_lines{$line_number} = $got_lines[$i] . "\n";
250             }
251             }
252              
253 6         9 my $previous_line_number = 0;
254 6         29 foreach my $line_number (sort {$a <=> $b} keys %error_lines) {
  1         5  
255              
256 7 100       23 if ($previous_line_number + 1 != $line_number) {
257 3         6 $return .= "# ...\n";
258             }
259              
260 7         19 $return .= _get_diff_line($line_number, $error_lines{$line_number});
261              
262 7         20 $previous_line_number = $line_number;
263             }
264              
265 6         31 return $return;
266             }
267              
268             sub _get_diff_line {
269 7     7   14 my ($line_number, $error_line) = @_;
270              
271 7         10 my $diff_line;
272              
273 7 50       17 if ($error_line eq "\n") {
274 0         0 $diff_line .= "# line $line_number \\n ";
275 0         0 $diff_line .= _colored("Empty line in the end of file", "red");
276 0         0 $diff_line .= "\n";
277              
278 0         0 return $diff_line;
279             }
280              
281             # array of hashes:
282             # { status => 'correct', text => 'a' },
283             # { status => 'error', text => '__' },
284             # { status => 'correct', text => '\n' },
285 7         16 my @parsed_line = _split_error_line($error_line);
286              
287 7         16 my $prefix = "# line $line_number: ";
288 7         12 my $spacer = "...";
289 7         8 my $max_length = 78;
290 7         12 my $system_length = length($prefix . $spacer);
291 7         11 my $max_text_length = $max_length - $system_length;
292              
293 7         7 my $line;
294 7         11 map { $line .= $_->{text}} @parsed_line;
  18         49  
295              
296 7         11 my $symbols_to_skip = length($line) - $max_text_length;
297              
298 7         9 my $skipped_length = 0;
299              
300 7         11 $diff_line .= $prefix;
301              
302 7 100       16 if ($symbols_to_skip > 0) {
303 2         3 $diff_line .= $spacer;
304             }
305              
306 7         12 foreach (@parsed_line) {
307              
308 18 100       42 if ($skipped_length < $symbols_to_skip) {
309 2         9 my $removed = substr $_->{text}, 0, ($symbols_to_skip - $skipped_length), '';
310 2         6 $skipped_length += length($removed);
311             }
312              
313 18 100       40 if ($_->{status} eq 'correct') {
314 12         191 $diff_line .= $_->{text};
315             } else {
316 6         28 $diff_line .= _colored($_->{text}, 'red');
317             }
318             }
319 7         14 $diff_line .= "\n";
320              
321 7         35 return $diff_line;
322             }
323              
324             sub _split_error_line {
325 12     12   4810 my ($error_line) = @_;
326              
327 12         22 my @parsed_line;
328              
329 12         20 my $correct_part = '';
330 12         18 my $error_part = '';
331              
332             # Value of $prev_status can be 'correct' or 'error'
333 12         57 my $prev_status = '';
334              
335 12         18 my $was_change = $false;
336              
337 12         41 foreach my $i (0..(length($error_line)-1)) {
338 252         444 my $symbol = substr($error_line, $i, 1);
339 252         579 my $rest = substr($error_line, $i+1);
340              
341 252 100 100     1633 if ($symbol eq "\t" or $symbol eq "\r") {
    100 66        
342 6         20 $symbol =~ s{\t}{\\t}g;
343 6         15 $symbol =~ s{\r}{\\r}g;
344              
345 6         12 $error_part .= $symbol;
346 6 100       18 if ($prev_status eq 'correct') {
347 2         7 $was_change = $true
348             }
349 6         11 $prev_status = 'error';
350             } elsif ($symbol =~ / / and $rest =~ /^\s*$/) {
351 9         14 $error_part .= "_";
352              
353 9 100       29 if ($prev_status eq 'correct') {
354 8         15 $was_change = $true
355             }
356 9         14 $prev_status = 'error';
357             } else {
358 237         401 $symbol =~ s{\n}{\\n}g;
359 237         278 $correct_part .= $symbol;
360              
361 237 100       655 if ($prev_status eq 'error') {
362 12         20 $was_change = $true
363             }
364 237         335 $prev_status = 'correct';
365             }
366              
367 252 100       718 if ($was_change) {
368 22 100       60 if ($prev_status eq 'error') {
    50          
369 10         39 push @parsed_line, { status => 'correct', text => $correct_part };
370 10         19 $correct_part = '';
371             } elsif ($prev_status eq 'correct') {
372 12         692 push @parsed_line, { status => 'error', text => $error_part };
373 12         22 $error_part = '';
374             }
375 22         54 $was_change = $false;
376             }
377             }
378              
379 12 100       33 if ($prev_status eq 'correct') {
    50          
380 11         39 push @parsed_line, { status => 'correct', text => $correct_part };
381             } elsif ($prev_status eq 'error') {
382 1         4 push @parsed_line, { status => 'error', text => $error_part };
383 1         3 $error_part = '';
384             }
385              
386 12         45 return @parsed_line;
387             }
388              
389             sub _file_is_in_vcs_index {
390 0     0     my ($filename) = @_;
391              
392 0 0         if (-d $filename) {
393 0           croak "Internal error. $filename is dir. It can't happen. Stopped";
394             }
395              
396 0 0         if (not -T $filename) {
397 0           croak "Internal error. $filename is not a text file. It can't happen. Stopped";
398             }
399              
400 0           my @vcs_dirs = qw(
401             .git
402             .hg
403             .svn
404             );
405              
406 0           my @parts = split "/", $filename;
407 0           pop @parts; # last element is filename, but we need only directories
408              
409 0           foreach my $part (@parts) {
410 0           foreach my $vcs (@vcs_dirs) {
411 0 0         return $true if $part eq $vcs;
412             }
413             }
414              
415 0           return $false;
416             }
417              
418             sub _check_file {
419 0     0     my ($filename) = @_;
420              
421 0 0         return if not defined $filename;
422              
423 0           $filename = realpath($filename);
424              
425 0 0         if (-T $filename) {
426 0 0         return if _file_is_in_vcs_index($filename);
427              
428 0           foreach my $regexp (@ignore) {
429 0 0         return if $filename =~ $regexp;
430             }
431              
432 0           my $content = _read_file($filename);
433 0           my $fixed_content = _get_fixed_text($content);
434              
435 0           my $module_path = realpath("$Bin/..") . "/";
436 0           my $relative_filename = $filename;
437 0           $relative_filename =~ s{^$module_path}{};
438              
439 0           _is($content, $fixed_content, "$relative_filename");
440             }
441              
442             }
443              
444             sub _check_dir {
445 0     0     my ($dir) = @_;
446              
447             find(
448             {
449 0     0     wanted => sub { _check_file($File::Find::fullname) },
450 0           follow => 1,
451             },
452             $dir,
453             );
454             }
455              
456             sub _fix_file {
457 0     0     my ($filename) = @_;
458              
459 0 0         return if not defined $filename;
460              
461 0 0         if (-T $filename) {
462 0 0         return if _file_is_in_vcs_index($filename);
463              
464 0           my $content = _read_file($filename);
465 0           my $fixed_content = _get_fixed_text($content);
466              
467 0 0         if ($content ne $fixed_content) {
468 0           _write_file($filename, $fixed_content);
469 0 0         print "Repairing $filename\n" if $verbose;
470             } else {
471 0 0         print "File is correct $filename\n" if $verbose;
472             }
473             }
474             }
475              
476             sub _fix_dir {
477 0     0     my ($dir) = @_;
478              
479             find(
480             {
481 0     0     wanted => sub { _fix_file($File::Find::fullname) },
482 0           follow => 1,
483             },
484             $dir,
485             );
486             }
487              
488             1;
489              
490             __END__