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