File Coverage

blib/lib/Eshu.pm
Criterion Covered Total %
statement 131 146 89.7
branch 71 88 80.6
condition 18 24 75.0
subroutine 9 9 100.0
pod 0 2 0.0
total 229 269 85.1


line stmt bran cond sub pod time code
1             package Eshu;
2              
3 62     62   6159950 use 5.008003;
  62         195  
4 62     62   303 use strict;
  62         110  
  62         1844  
5 62     62   251 use warnings;
  62         95  
  62         5402  
6              
7             our $VERSION = '0.01';
8              
9             require XSLoader;
10             XSLoader::load('Eshu', $VERSION);
11              
12 62     62   317 use File::Find ();
  62         149  
  62         1065  
13 62     62   242 use File::Spec ();
  62         79  
  62         83060  
14              
15             my $MAX_FILE_SIZE = 1_048_576; # 1MB
16              
17             sub indent_file {
18 39     39 0 96 my ($class, $path, %opts) = @_;
19              
20 39         101 my $result = {
21             file => $path,
22             status => 'unchanged',
23             };
24              
25             # Check file exists and is readable
26 39 50 33     767 unless (-f $path && -r _) {
27 0         0 $result->{status} = 'error';
28 0         0 $result->{error} = 'not a readable file';
29 0         0 return $result;
30             }
31              
32             # Check file size
33 39         298 my $size = -s $path;
34 39 50       78 if ($size > $MAX_FILE_SIZE) {
35 0         0 $result->{status} = 'skipped';
36 0         0 $result->{reason} = 'file too large';
37 0         0 return $result;
38             }
39              
40             # Read file
41 39 50       1186 open my $fh, '<', $path or do {
42 0         0 $result->{status} = 'error';
43 0         0 $result->{error} = "cannot open: $!";
44 0         0 return $result;
45             };
46 39         93 my $src = do { local $/; <$fh> };
  39         179  
  39         1107  
47 39         320 close $fh;
48              
49             # Binary detection: NUL in first 8KB
50 39 50       130 if (length($src) > 0) {
51 39         85 my $sample = substr($src, 0, 8192);
52 39 100       114 if ($sample =~ /\0/) {
53 1         4 $result->{status} = 'skipped';
54 1         3 $result->{reason} = 'binary file';
55 1         7 return $result;
56             }
57             }
58              
59             # Detect language
60 38   66     470 my $lang = $opts{lang} || $class->detect_lang($path);
61 38 100       95 unless (defined $lang) {
62 2         4 $result->{status} = 'skipped';
63 2         3 $result->{reason} = 'unrecognised extension';
64 2         6 return $result;
65             }
66              
67             # Build indent options
68 36         115 my %indent_opts = (lang => $lang);
69 36         69 for my $k (qw(indent_char indent_width indent_pp range_start range_end)) {
70 180 50       292 $indent_opts{$k} = $opts{$k} if exists $opts{$k};
71             }
72              
73             # Indent
74 36         297 my $fixed = $class->indent_string($src, %indent_opts);
75              
76 36 100       87 if ($fixed eq $src) {
77 2         6 $result->{status} = 'unchanged';
78             } else {
79 34 100       88 $result->{status} = $opts{fix} ? 'changed' : 'needs_fixing';
80 34 100       62 if ($opts{fix}) {
81 12 50       990 open my $out, '>', $path or do {
82 0         0 $result->{status} = 'error';
83 0         0 $result->{error} = "cannot write: $!";
84 0         0 return $result;
85             };
86 12         51 print $out $fixed;
87 12         942 close $out;
88             }
89 34 100       83 $result->{diff} = _simple_diff($path, $src, $fixed) if $opts{diff};
90             }
91 36         70 $result->{lang} = $lang;
92 36         160 return $result;
93             }
94              
95             sub indent_dir {
96 16     16 0 29436 my ($class, $path, %opts) = @_;
97              
98 16 100       68 my $recursive = exists $opts{recursive} ? $opts{recursive} : 1;
99 16         30 my $exclude = $opts{exclude};
100 16         28 my $include = $opts{include};
101              
102 16         26 my @files;
103              
104 16 50       390 if (-f $path) {
    50          
105 0         0 push @files, $path;
106             } elsif (-d $path) {
107 16 100       41 if ($recursive) {
108             File::Find::find({
109             wanted => sub {
110 68 100   68   2953 return unless -f $_;
111 45 50 33     290 return if -l $File::Find::dir && $File::Find::dir ne $path;
112 45         563 push @files, $File::Find::name;
113             },
114 15         1447 follow_skip => 2,
115             no_chdir => 1,
116             }, $path);
117             } else {
118 1 50       70 opendir my $dh, $path or die "Eshu: cannot opendir '$path': $!\n";
119 1         25 while (my $entry = readdir $dh) {
120 4 100       11 next if $entry =~ /^\./;
121 2         17 my $full = File::Spec->catfile($path, $entry);
122 2 100       30 push @files, $full if -f $full;
123             }
124 1         10 closedir $dh;
125             }
126             } else {
127 0         0 die "Eshu: '$path' is not a file or directory\n";
128             }
129              
130 16         119 @files = sort @files;
131              
132 16         89 my $report = {
133             files_checked => 0,
134             files_changed => 0,
135             files_skipped => 0,
136             files_errored => 0,
137             changes => [],
138             };
139              
140 16         30 for my $file (@files) {
141             # Apply exclude filter
142 46 100       90 if (defined $exclude) {
143 12 100       25 my @pats = ref $exclude eq 'ARRAY' ? @$exclude : ($exclude);
144 12         12 my $skip = 0;
145 12         13 for my $pat (@pats) {
146 15 100       54 if ($file =~ $pat) { $skip = 1; last; }
  4         4  
  4         6  
147             }
148 12 100       18 if ($skip) {
149 4         4 $report->{files_skipped}++;
150 4         4 push @{$report->{changes}}, { file => $file, status => 'skipped', reason => 'excluded' };
  4         11  
151 4         5 next;
152             }
153             }
154              
155             # Apply include filter
156 42 100       72 if (defined $include) {
157 7 50       44 my @pats = ref $include eq 'ARRAY' ? @$include : ($include);
158 7         11 my $match = 0;
159 7         9 for my $pat (@pats) {
160 7 100       35 if ($file =~ $pat) { $match = 1; last; }
  4         3  
  4         6  
161             }
162 7 100       17 unless ($match) {
163 3         7 $report->{files_skipped}++;
164 3         4 push @{$report->{changes}}, { file => $file, status => 'skipped', reason => 'not included' };
  3         10  
165 3         7 next;
166             }
167             }
168              
169 39         145 my $result = $class->indent_file($file, %opts);
170 39         57 push @{$report->{changes}}, $result;
  39         81  
171              
172 39 100 100     168 if ($result->{status} eq 'changed' || $result->{status} eq 'needs_fixing') {
    100          
    50          
    0          
173 34         50 $report->{files_changed}++;
174 34         67 $report->{files_checked}++;
175             } elsif ($result->{status} eq 'unchanged') {
176 2         4 $report->{files_checked}++;
177             } elsif ($result->{status} eq 'skipped') {
178 3         23 $report->{files_skipped}++;
179             } elsif ($result->{status} eq 'error') {
180 0         0 $report->{files_errored}++;
181             }
182             }
183              
184 16         87 return $report;
185             }
186              
187             sub _simple_diff {
188 1     1   2 my ($label, $old, $new) = @_;
189 1         3 my @old_lines = split /^/m, $old;
190 1         3 my @new_lines = split /^/m, $new;
191 1         2 my $out = "--- a/$label\n+++ b/$label\n";
192 1 50       3 my $max = @old_lines > @new_lines ? scalar @old_lines : scalar @new_lines;
193 1         1 my $hunk_start = -1;
194 1         2 my (@hunk_old, @hunk_new);
195              
196 1         2 for (my $i = 0; $i <= $max; $i++) {
197 4 100       5 my $ol = $i < @old_lines ? $old_lines[$i] : undef;
198 4 100       6 my $nl = $i < @new_lines ? $new_lines[$i] : undef;
199 4   100     11 my $same = defined $ol && defined $nl && $ol eq $nl;
200              
201 4 100 66     9 if (!$same && $hunk_start < 0) {
202 2         2 $hunk_start = $i;
203 2         2 @hunk_old = ();
204 2         6 @hunk_new = ();
205             }
206 4 100 100     9 if ($hunk_start >= 0 && !$same) {
207 2 100       3 push @hunk_old, $ol if defined $ol;
208 2 100       3 push @hunk_new, $nl if defined $nl;
209             }
210 4 100 100     8 if ($same || $i == $max) {
211 3 100       5 if ($hunk_start >= 0) {
212 2         6 $out .= sprintf "@@ -%d,%d +%d,%d @@\n",
213             $hunk_start + 1, scalar @hunk_old,
214             $hunk_start + 1, scalar @hunk_new;
215 2         3 for my $l (@hunk_old) {
216 1 50       4 $l = "$l\n" unless $l =~ /\n$/;
217 1         1 $out .= "-$l";
218             }
219 2         2 for my $l (@hunk_new) {
220 1 50       2 $l = "$l\n" unless $l =~ /\n$/;
221 1         2 $out .= "+$l";
222             }
223 2         3 $hunk_start = -1;
224             }
225             }
226             }
227 1         3 return $out;
228             }
229              
230             1;
231              
232             __END__