dev-precommit 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. #!/usr/bin/perl
  2. use local::lib;
  3. use v5.10;
  4. use strict;
  5. use warnings;
  6. use FindBin qw( $RealBin );
  7. use lib "$RealBin/lib";
  8. use Cwd qw( abs_path );
  9. use File::Basename qw( basename );
  10. use File::Find qw( find );
  11. use List::Util qw( any );
  12. use LogBot::Util qw( file_time run );
  13. use Mojo::File ();
  14. use Mojo::Util qw( trim );
  15. use Perl::Critic ();
  16. use Perl::Tidy ();
  17. $| = 1;
  18. chdir($RealBin);
  19. our @ARGV = ();
  20. # tabs
  21. find(
  22. sub {
  23. return unless -f $File::Find::name && -s $File::Find::name;
  24. my $file = substr($File::Find::name, 2);
  25. return if $file =~ m{/} && $file !~ m{^(?:lib|web)/};
  26. return if basename($file) =~ /^\./ || $file =~ /\.(bak|swp|png|ttf)$/;
  27. return if $file eq 'makefile' or $file eq 'perltidy.ERR';
  28. my $content = Mojo::File->new("$RealBin/$file")->slurp();
  29. return unless $content =~ /\t/;
  30. say "\e[31m$file contains tabs\e[0m";
  31. },
  32. '.'
  33. );
  34. # tidy sass (sort selectors)
  35. foreach my $file (glob('web/*.sass')) {
  36. next if basename($file) =~ /^_/;
  37. print $file;
  38. if (!is_modified($file)) {
  39. say ' unchanged';
  40. next;
  41. }
  42. my @original = split(/\n/, Mojo::File->new($file)->slurp());
  43. my @output;
  44. my @variables;
  45. my $chunk = {};
  46. reset_chunk($chunk);
  47. foreach my $line (@original) {
  48. my $indent = $line =~ /^(\s+)/ ? length($1) : 0;
  49. if ($line eq '') {
  50. push @output, process_chunk($chunk);
  51. push @output, '';
  52. $chunk->{indent} = $indent;
  53. next;
  54. }
  55. if ($line =~ /^(\$[^:]+):.+$/) {
  56. push @variables, $1;
  57. }
  58. if ($indent != $chunk->{indent}) {
  59. push @output, process_chunk($chunk);
  60. $chunk->{indent} = $indent;
  61. push @{ $chunk->{lines} }, $line;
  62. next;
  63. }
  64. push @{ $chunk->{lines} }, $line;
  65. }
  66. push @output, process_chunk($chunk);
  67. sub reset_chunk {
  68. my ($ch) = @_;
  69. $ch->{indent} = -1;
  70. $ch->{lines} = [];
  71. }
  72. sub process_chunk {
  73. my ($ch) = @_;
  74. my @lines =
  75. sort {
  76. return -1 if $a =~ m{^\s*//};
  77. return 1 if $b =~ m{^\s*//};
  78. return $a cmp $b;
  79. } @{ $ch->{lines} };
  80. reset_chunk($ch);
  81. return @lines;
  82. }
  83. my $original = join("\n", @original) . "\n";
  84. my $output = join("\n", @output) . "\n";
  85. if ($original ne $output) {
  86. say " \e[34mupdated\e[0m";
  87. Mojo::File->new($file)->spurt($output);
  88. } else {
  89. say ' unchanged';
  90. }
  91. set_modified($file);
  92. foreach my $var (@variables) {
  93. my $count = 0;
  94. $count++ while $output =~ /\Q$var\E/g;
  95. say "\e[33munused sass variable: ", $var, "\e[0m" if $count == 1;
  96. }
  97. }
  98. # tidy all the perl as per .perltidy
  99. # critic all the perl as per .perlcriticrc
  100. my $critic = Perl::Critic->new(-profile => $RealBin . '/.perlcriticrc');
  101. find(
  102. sub {
  103. # look for files in . and under lib/
  104. my $file = $File::Find::name;
  105. return unless -f $file && -s $file;
  106. my $rel_file = substr($file, length($RealBin) + 1);
  107. return if $rel_file =~ m{/} && $rel_file !~ m{^lib/};
  108. return if basename($rel_file) =~ /^\./ || $rel_file =~ /\.(bak|swp)$/;
  109. # detect perl by file extension or #!
  110. my $is_perl = 0;
  111. if ($file =~ /\.p[ml]$/) {
  112. $is_perl = 1;
  113. } else {
  114. open(my $fh, '<', $file) or die "open $file $!\n";
  115. my ($first_line) = <$fh>;
  116. close($fh) or die $!;
  117. $is_perl = $first_line =~ m{\#!/usr/bin/perl};
  118. }
  119. return unless $is_perl;
  120. print $rel_file;
  121. if (!is_modified($file)) {
  122. say ' unchanged';
  123. return;
  124. }
  125. # init
  126. my $original = Mojo::File->new($file)->slurp();
  127. my $input = $original;
  128. my $output = '';
  129. # sort use lines, as well as their imports
  130. $input = sort_use($input);
  131. # apply perltidy
  132. Perl::Tidy::perltidy(
  133. source => \$input,
  134. destination => \$output,
  135. );
  136. # apply changes
  137. if ($original ne $output) {
  138. Mojo::File->new($file)->spurt($output);
  139. say " \e[34mupdated\e[0m";
  140. } else {
  141. say ' unchanged';
  142. }
  143. # critic
  144. my @issues = $critic->critique($file);
  145. my $has_issues = !!scalar(@issues);
  146. foreach my $issue (@issues) {
  147. (my $policy = $issue->policy) =~ s/^Perl::Critic::Policy:://;
  148. say "\e[33m", $rel_file, ':', $issue->line_number, ' ', $issue->description, ' (', $policy, ")\e[0m";
  149. }
  150. # warn for unused imports
  151. @issues = find_unused_imports($input);
  152. $has_issues ||= !!scalar(@issues);
  153. foreach my $issue (@issues) {
  154. say "\e[33m", $rel_file, ': ', $issue, "\e[0m";
  155. }
  156. if ($has_issues) {
  157. clear_modified($file);
  158. } else {
  159. set_modified($file);
  160. }
  161. },
  162. $RealBin
  163. );
  164. # javascript
  165. foreach my $js_file (qw( web/logbot.js web/redirect.js )) {
  166. run('js-beautify', '-r', '-n', $js_file);
  167. run('jshint', $js_file);
  168. }
  169. # make static assets
  170. run("$RealBin/dev-make", '-B');
  171. sub sort_use {
  172. my ($input) = @_;
  173. return foreach_use_block(
  174. $input,
  175. sub {
  176. my @uses = @_;
  177. # sort imports
  178. foreach my $line (@uses) {
  179. next unless $line =~ /^(use\s+\S+)\s+qw\(([^\)]+)\);$/;
  180. my ($module, $imports) = ($1, $2);
  181. my @imports = split(/\s+/, trim($imports));
  182. $imports = join(' ', sort @imports);
  183. $line = "$module qw( $imports );";
  184. }
  185. # sort modules
  186. return [sort { lc($a) cmp lc($b) } @uses];
  187. }
  188. );
  189. }
  190. sub find_unused_imports {
  191. my ($input) = @_;
  192. my (%modules, %imports);
  193. foreach_use_block(
  194. $input,
  195. sub {
  196. my @uses = @_;
  197. foreach my $line (@uses) {
  198. if ($line =~ /^use\s+(\S+)\s+\(\);$/) {
  199. $modules{$1} = 1;
  200. } elsif ($line =~ /^(use\s+\S+)\s+qw\(([^\)]+)\);$/) {
  201. my ($module, $imports) = ($1, $2);
  202. my @imports = split(/\s+/, trim($imports));
  203. foreach my $import (@imports) {
  204. next if $import =~ m{^[:/]};
  205. $imports{$import} = 1;
  206. }
  207. }
  208. }
  209. return undef;
  210. }
  211. );
  212. $input = join("\n", grep { !/^use\s/ } split(/\n/, $input));
  213. my @issues;
  214. foreach my $module (sort keys %modules) {
  215. next if $input =~ /\b$module(?:->|::)/ || $input =~ /'$module'/;
  216. push @issues, "package $module is unused";
  217. }
  218. foreach my $import (sort keys %imports) {
  219. if ($import =~ /^\$/) {
  220. next if $input =~ /\Q$import\E\b/;
  221. } else {
  222. next if $input =~ /\b\Q$import\E\b/;
  223. }
  224. push @issues, "import $import is unused";
  225. }
  226. return @issues;
  227. }
  228. sub foreach_use_block {
  229. my ($input, $callback) = @_;
  230. # use-block is replaced by callback's return, unless callback returns undef
  231. # find use blocks, assumes they are always followed by a blank line
  232. my @output;
  233. my $in_use_block = 0;
  234. my @uses;
  235. foreach my $line (split(/\n/, $input)) {
  236. if ($in_use_block) {
  237. if ($line eq '') {
  238. $in_use_block = 0;
  239. process_uses(\@uses, \@output, $callback);
  240. @uses = ();
  241. next;
  242. }
  243. }
  244. if ($line =~ /^use /) {
  245. $in_use_block = 1;
  246. push @uses, $line;
  247. next;
  248. }
  249. push @output, $line;
  250. }
  251. process_uses(\@uses, \@output, $callback);
  252. return join("\n", @output) . "\n";
  253. }
  254. sub process_uses {
  255. my ($uses, $output, $callback) = @_;
  256. return unless @{$uses};
  257. # skip setup / lib blocks
  258. if (any { /^use local::lib/ || /^use lib / || /^use base / } @{$uses}) {
  259. push @{$output}, @{$uses}, '';
  260. } else {
  261. my $replace = $callback->(@{$uses});
  262. if (defined $replace) {
  263. push @{$output}, @{$replace}, '';
  264. } else {
  265. push @{$output}, @{$uses}, '';
  266. }
  267. }
  268. }
  269. # lastmod tracking
  270. my $lastmod;
  271. sub _init_modified {
  272. return if defined $lastmod;
  273. $lastmod = {};
  274. if (open(my $fh, '<', "$RealBin/.dev-precommit")) {
  275. while (<$fh>) {
  276. next unless /^(.+)\t(\d+)$/;
  277. $lastmod->{$1} = $2;
  278. }
  279. close($fh) || die $!;
  280. }
  281. if (is_modified("$RealBin/dev-precommit")) {
  282. $lastmod = {};
  283. _write_modified();
  284. }
  285. }
  286. sub _write_modified {
  287. open(my $fh, '>', "$RealBin/.dev-precommit") or die $!;
  288. foreach my $fn (sort keys %{$lastmod}) {
  289. say {$fh} "$fn\t" . $lastmod->{$fn};
  290. }
  291. close($fh) or die $!;
  292. }
  293. sub is_modified {
  294. my ($file) = @_;
  295. _init_modified();
  296. my $mtime = file_time($file);
  297. my $abs_file = abs_path($file);
  298. $lastmod->{$abs_file} //= 0;
  299. return $lastmod->{$abs_file} != $mtime;
  300. }
  301. sub set_modified {
  302. my ($file) = @_;
  303. _init_modified();
  304. $lastmod->{ abs_path($file) } = file_time($file);
  305. _write_modified();
  306. }
  307. sub clear_modified {
  308. my ($file) = @_;
  309. _init_modified();
  310. delete $lastmod->{ abs_path($file) };
  311. _write_modified();
  312. }