chiark / gitweb /
d0b6e07074a96e620b8b491506b03861d2c27c15
[dgit.git] / i18n-diff-auditor
1 #!/usr/bin/perl -w
2 use strict;
3 use Carp;
4 use Data::Dumper;
5 use Getopt::Long;
6
7 our $debug = 0;
8 GetOptions("debug|D+" => \$debug
9            );
10
11 our @debug;
12 sub debug ($$) {
13     my ($i,$s) = @_;
14     push @{ $debug[$i] }, $s if $debug;
15 }
16
17 my @d = <>;
18 unshift @d, "# dummy line to make line 1 index 1 in \@d\n";
19
20 our $i_last_l_ok = -1;
21 our $count_i_last_l_ok;
22
23 sub l_ok ($) {
24     my ($i) = @_;
25
26     if ($i == $i_last_l_ok) {
27         confess $i if $count_i_last_l_ok++ > 50;
28     } else {
29         $count_i_last_l_ok = 0;
30         $i_last_l_ok = $i;
31     }
32
33     return unless $i < @d;
34     $_ = $d[$i];
35     #print STDERR "L $i\n";
36     1;
37 }
38
39 sub l ($) {
40     my ($i) = @_;
41     confess $i unless l_ok $i;
42 };
43
44 our $perlop_text = <<'END'; # c&p from man perlop
45            left        terms and list operators (leftward)
46            left        ->
47            nonassoc    ++ --
48            right       **
49            right       ! ~ \ and unary + and -
50            left        =~ !~
51            left        * / % x
52            left        + - .
53            left        << >>
54            nonassoc    named unary operators
55            nonassoc    < > <= >= lt gt le ge
56            nonassoc    == != <=> eq ne cmp ~~
57            left        &
58            left        | ^
59            left        &&
60            left        || //
61            nonassoc    ..  ...
62            right       ?:
63            right       = += -= *= etc. goto last next redo dump
64            left        , =>
65            nonassoc    list operators (rightward)
66            right       not
67            left        and
68            left        or xor
69
70            **=    +=    *=    &=    &.=    <<=    &&=
71                   -=    /=    |=    |.=    >>=    ||=
72                   .=    %=    ^=    ^.=           //=
73                         x=
74 END
75
76 our $perlop_re;
77
78 sub prep_perlop () {
79     my @ops;
80     foreach (split /\n/, $perlop_text) {
81         next unless m{\S};
82         s{\s+$}{};
83         s{^\s+}{};
84         s{^(?: left | right | nonassoc ) \s+}{}x;
85         next if m{^terms and list operators};
86         next if m{^named unary};
87         next if m{^list operators};
88         s{ and unary.*}{};
89         s{ etc\. }{ };
90         s{\?\:}{ ? : };
91         foreach my $op (split /\s+/) {
92             next unless length $op;
93             next if $op =~ m{^\w+$};
94             $op =~ s/\W/\\$&/g;
95             push @ops, $op;
96         }
97     }
98     $perlop_re = '(?: '.(join ' | ', @ops).' )';
99     $perlop_re = qr{$perlop_re}x;
100     #print STDERR "$perlop_re\n";
101 }
102
103 prep_perlop();
104
105 our ($ifilehead, $ifirsthunkhead);
106 our ($ihunkhead, $ihunkend);
107 our ($ichunkstart, $ichunkend);
108 our ($before, $after);
109
110 sub is_string ($) { $_[0]{T} =~ m/heredoc|string/; }
111 sub is_trans ($) { grep { $_[0]{E} eq $_ } qw(__ f_ i_); }
112
113 sub qp ($) {
114     my ($p) = @_;
115     $p =~ s{\\}{\\\\}g;
116     $p =~ s{\'}{\\'}g;
117     $p =~ s{\n}{\\n}g;
118     $p =~ s{\t}{\\t}g;
119     return "'$p'";
120 };
121
122 sub semiparse ($) {
123     ($_) = @_;
124     my @o;
125     #my $in = $_;
126     # entries contain
127     #   T     type
128     #   E     exact input text (does not contain here doc contents)
129     #   P     something to print in messages
130     #   V     value, only for: heredoc string
131     #   Q     quote characcter, only for: heredoc string
132     for (;;) {
133         s{^\s+}{};
134         if (s{^[\$\@\%]?[_0-9a-zA-Z]+}{}) {
135             push @o, { T => 'ident', E => $&, P => $& };
136         } elsif (s{^\<\<(['"]?)([A-Z_]+)\1}{}) {
137             my ($q,$d) = ($1,$2);
138             $q ||= '"';
139             push @o, { T => 'heredoc', Q => $q, Delim => $d,
140                        E => $&, P => "<<$q$d$q" };
141             if (s{^
142                     (                 .* \n     )
143                     ( (?: (?! $d \n ) .* \n )*? )
144                               $d         \n
145                  }{ $1 }xe) {
146                 $o[$#o]{V} = $2;
147             } else {
148                 m{^.*\n} or confess;
149                 $_ = $&;
150                 $o[$#o]{V} = $';
151                 $o[$#o]{Invented} = 1;
152             }
153         } elsif (s{^ (["'])( (?: [^\\'"]
154                                | \\ [^"']
155                                | (?! \1 ) [^"]
156                               )*
157                        ) \1 }{}x) {
158             my ($q,$v) = ($1,$2);
159             push @o, { T => 'string', E => $&, P => "$q$q",
160                        Q => $q, V => $v};
161         } elsif (s{^$perlop_re|^\;}{}) {
162             push @o, { T => 'op', E => $&, P => $& };
163         } elsif (s/^[[{(]//) {
164             push @o, { T => 'bra', E => $&, P => $& };
165         } elsif (s/^[]})]//) {
166             push @o, { T => 'ket', E => $&, P => $& };
167         } elsif (s/^( [\$\@\%] )( \{ )//x) {
168             push @o, { T => 'deref', E => $1, P => $1 },
169                      { T => 'bra',   E => $2, P => $2 };
170         } elsif (s/^ [\$\@\%] [^[^{] //x) {
171             push @o, { T => 'specvar', E => $&, P => $& };
172         } elsif (!length) {
173             last;
174         } else {
175             m{^.{0,10}};
176             die "cannot tokenise \`$&'";
177         }
178     }
179     for (my $i=0; $i+2 < @o; $i++) {
180         next unless $o[$i+1]{E} eq '.';
181         my @inputs = @o[$i, $i+2];
182         #print STDERR Dumper(\@inputs);
183         next if grep { !is_string($_) } @inputs;
184         my $q = $inputs[0]{Q};
185         next if grep { $_->{Q} ne $q } @inputs;
186         next if grep { $_->{Invented} } @inputs;
187         my $new = { T => 'joinedstrings',
188                     E => (join '.', map { $_->{E} } @inputs),
189                     P => (join '.', map { $_->{P} } @inputs),
190                     V => (join '',  map { $_->{V} } @inputs),
191                     Q => $q,
192                   };
193         @o = (@o[0..$i-1], $new, @o[$i+3..$#o]);
194         $i--; # counteracts $i++
195     }
196     debug $ichunkstart, "semiparsed: ".join ' ', map { $_->{P} } @o;
197     # debug $ichunkstart, "semiparsed V: ".join ' ', map { defined $_->{V} ? ">$_->{V}<" : '-' } @o;
198     return @o;
199 }           
200
201 our @analysed_x;
202 our @analysed_y;
203
204 sub analyse_chunk_core () {
205     die "plain deletion\n" unless defined $after;
206     die "plain insertion\n" unless defined $before;
207     my @xs = semiparse $before;
208     my @ys = semiparse $after;
209     @analysed_x = @analysed_y = ();
210     my $next_something = sub {
211         my ($ary,$anal,$var,$what) = @_;
212         die "ran out of $what\n" unless @$ary;
213         my $r = shift @$ary;
214         push @$anal, $r->{P};
215         $$var = $r;
216     };
217     my ($x,$y);
218     my $next_x = sub { $next_something->(\@xs, \@analysed_x, \$x, 'before'); };
219     my $next_y = sub { $next_something->(\@ys, \@analysed_y, \$y, 'after' ); };
220     our @y_expect_suffix = ();
221     for (;;) {
222         while (my $e = shift @y_expect_suffix) {
223             $next_y->();
224             $y->{E} eq $e
225                 or die "suffix mismatch, expected $e got $y->{E}\n";
226         }
227         last unless @xs or @ys;
228         $next_x->();
229         $next_y->();
230         next if $x->{E} eq $y->{E};
231         next if $x->{E} eq 'sprintf' and $y->{E} eq 'f_';
232         next if $x->{E} eq 'die'     and $y->{E} eq 'confess';
233         next if $x->{E} eq 'die'     and $y->{E} eq 'fail';
234         if ($y->{E} eq '+'
235             and @ys >= 3
236             and $ys[0]{E} eq '('
237             and is_trans($ys[1])) {
238             $next_y->(); # (
239             $next_y->(); # __ f_ i_
240             @y_expect_suffix = ')';
241         } elsif ($y->{E} eq '('
242             and @ys > 2
243             and is_trans($ys[0])
244             and @analysed_y
245             and (grep { $_ eq $analysed_y[-1] } (qw( => [ { ? : . ),
246                                                  '(', ',') )) {
247             $next_y->(); # __ f_ i_
248             @y_expect_suffix = ')';
249         }
250         my $string_changed;
251         my $ye = $y->{E};
252         if (is_trans($y)) {
253             $next_y->();
254             die "__ on non-string $y->{P}\n"     unless is_string($y);
255             die "__ on was non-string $x->{P}\n" unless is_string($x);
256             if ($y->{Q} ne "'") {
257                 die "var subst in new string\n"
258                     if $y->{V} =~ m{(?<!\\) [\$\@]};
259             }
260             eval {
261                 die "__ string changed\n"       unless $y->{V} eq $x->{V};
262                 die "__ string quote changed\n" unless $y->{Q} eq $x->{Q};
263             };
264             $string_changed = $@;
265         }
266         if ($ye eq '__') {
267             $_ = $y->{V};
268             die "percent $& in __ ' string\n" if m{\%};
269             die $string_changed if length $string_changed;
270             next;
271         }
272         if ($ye eq 'i_') {
273             die $string_changed if length $string_changed;
274             next;
275         }
276         if ($ye eq 'f_') {
277             my $fmt = $y->{V};
278             die "no percent in f_ string\n" unless $fmt =~ m{\%};
279             next unless $string_changed;
280             die "f_ old string '-quoted\n" if $x->{Q} ne '"';
281             my $xs = $x->{V};
282             my $exactly = sub {
283                 my ($lit, $what) = @_;
284                 my $xl = substr($xs, 0, length($lit));
285                 if ($xl ne $lit) {
286                     debug $ichunkstart, "not exactly x: ..".qp($xs);
287                     debug $ichunkstart, "not exactly y:   ".qp($lit);
288                     my $next = @ys ? $ys[0]{P} : '(end)';
289                     die "string contents mismatch near $what before $next\n";
290                 }
291                 $xs = substr($xs, length($lit));
292             };
293             for (;;) {
294                 #print STDERR Dumper($fmt, $xs, \@xs, @ys);
295                 if ($fmt !~ m{\%[^\%]}) {
296                     $exactly->($fmt, '(tail)');
297                     $fmt = '';
298                     die "text deleted from end of string: ".qp($xs)."\n"
299                         if length $xs;
300                     last;
301                 }
302                 $exactly->($`, '(literal)');
303                 $fmt = $';
304                 if ($& eq '%%') { $exactly->('%', '%%'); next; }
305                 elsif ($& ne '%s') { die "unhandled %-subst $&\n"; }
306                 $next_y->();
307                 die "expected comma, got $y->{P}\n" unless $y->{E} eq ',';
308                 if (!length $fmt and
309                     !length $xs and
310                     @xs and
311                     $xs[0]{E} eq '.') {
312                     # X has   "<earlier>" .                <something>
313                     # Y has   "<earlier>%s" [other args] , <something>
314                     $next_x->(); # eat the '.'
315                     next;
316                 }
317                 if ($xs =~ m{^\@}) {
318                     $next_y->();
319                     die "\@... => not string" unless is_string($y);
320                     die "\@... => $y->{P}" if $y->{Q} ne '"';
321                     $exactly->($y->{V}, $y->{P});
322                     next;
323                 }
324                 my $bras = 0;
325                 for (;;) {
326                     if (!$bras and !@ys) {
327                         last;
328                     }
329                     $next_y->();
330                     if (!$bras and
331                         (grep { $y->{E} eq $_ } qw( or xor and not ; :
332                                                     if unless while when )
333                          or $y->{E} eq ','
334                          or $y->{T} eq 'ket'
335                         )) {
336                         # lookahead shows close of containing scope
337                         # or lower precedence operator
338                         unshift @ys, $y;
339                         pop @analysed_y;
340                         last;
341                     }
342                     $xs =~ s{^\s+}{} if $bras;
343                     if (is_string($y) and $y->{Q} eq '"') {
344                         $exactly->($y->{V}, $y->{P});
345                         next;
346                     }
347                     $exactly->($y->{E}, $y->{P});
348                     if ($y->{T} eq 'bra' or $y->{E} eq '?') {
349                         $bras++;
350                     } elsif ($y->{T} eq 'ket' or $y->{E} eq ':') {
351                         die "too many kets at $y->{E}\n" unless $bras;
352                         $bras--;
353                     }
354                 }
355             }
356             next;
357         }
358         die "mismatch $x->{P} => $y->{P}\n";
359     }
360 }
361
362 sub analyse_chunk () {
363     for (;;) {
364         eval { analyse_chunk_core(); };
365         return unless length $@;
366         if ($@ =~ m{^missing end of here doc (\S+)\n}) {
367             # fudge this
368             # (this never happens now, but in the future we might
369             # want this code again eg to try adding to the chunk)
370             $before .= "\n$1\n";
371             $after .= "\n$1\n";
372             next;
373         } else {
374             die $@;
375         }
376     }
377 }
378
379 our @report;
380 our $last_filehead = -1;
381
382 sub report_on_hunk () {
383     return unless @report;
384     if ($last_filehead != $ifilehead) {
385         foreach (my $i=$ifilehead; $i<$ifirsthunkhead; $i++) {
386             print $d[$i];
387         }
388         $last_filehead = $ifilehead;
389     }
390     my $dummy_r = { S => (scalar @d)+1, E => (scalar @d)+1 };
391     my $r;
392     for (my $i=$ihunkhead; ; $i++) {
393         for (;;) {
394             $r //= shift @report;
395             $r //= $dummy_r;
396             last if $i < $r->{E};
397             confess unless $r->{Done} == 03;
398             $r = undef;
399         }
400
401         last unless $i<$ihunkend;
402
403         foreach my $ds (@{ $debug[$i] }) {
404             print "# $ds\n";
405         }
406
407         if ($i == $r->{S}) {
408             print "!! $r->{M}";
409             $r->{Done} |= 01;
410         }
411         if ($i >= $r->{S}) {
412             print "!";
413             $r->{Done} |= 02;
414         } else {
415             print " ";
416         }
417         print $d[$i];
418     }
419     confess unless $r = $dummy_r;
420 }
421
422 for ($ifilehead = 0; l_ok $ifilehead; $ifilehead++) {
423     m{^diff} or next;
424     $ifirsthunkhead = $ifilehead;
425     while (l_ok $ifirsthunkhead and
426            m{^diff|^index|^---|^\Q+++\E}) {
427         $ifirsthunkhead++
428     }
429     $ihunkhead = $ifirsthunkhead;
430     while (l_ok $ihunkhead) {
431         m{^\@\@} or confess "$ihunkhead $_ ?";
432         my $i = $ihunkhead + 1;
433         for (; ; $i++) {
434             if (!l_ok $i or m{^ } or m{^\@\@}) {
435                 if (defined $ichunkstart) {
436                     $ichunkend = $i;
437                     eval { analyse_chunk(); 1; };
438                     if (length $@) {
439                         debug $ichunkstart, "done x: @analysed_x";
440                         debug $ichunkstart, "done y: @analysed_y";
441                         push @report, { M => $@,
442                                         S => $ichunkstart,
443                                         E => $ichunkend };
444                     }
445                     $ichunkstart = $ichunkend = $before = $after = undef;
446                 }
447                 l_ok $i or last;
448                 m{^\@\@} and last;
449             } elsif (m{^[-+]}) {
450                 my $which = $& eq '-' ? \$before : \$after;
451                 $ichunkstart //= $i;
452                 $$which //= '';
453                 $$which .= $';
454             } else {
455                 confess "$i $_ ?";
456             }
457         }
458         $ihunkend = $i;
459         report_on_hunk();
460         $ichunkend = $i;
461         $ihunkhead = $i;
462     }
463 }