chiark / gitweb /
f40c8e97e7ec0f94685b76c55815df04f248f46f
[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
112 sub semiparse ($) {
113     ($_) = @_;
114     my @o;
115     # entries contain
116     #   T     type
117     #   E     exact input text (does not contain here doc contents)
118     #   P     something to print in messages
119     #   V     value, only for: heredoc string
120     #   Q     quote characcter, only for: heredoc string
121     for (;;) {
122         s{^\s+}{};
123         if (s{^[\$\@\%]?[_0-9a-zA-Z]+}{}) {
124             push @o, { T => 'ident', E => $&, P => $& };
125         } elsif (s{^\<\<(['"]?)([A-Z_]+)\1}{}) {
126             my ($q,$d) = ($1,$2);
127             $q ||= '"';
128             push @o, { T => 'heredoc', Q => $q, Delim => $d,
129                        E => $&, P => "<<$q$d$q" };
130             s{^
131                 (             .* \n    )
132                 ( (?: (?! $d) .* \n )* )
133                           $d     \n
134               }{ $1 }x or die "missing end of here doc $d\n";
135             $o[$#o]{V} = $2;
136         } elsif (s{^ (["'])( (?: [^\\'"]
137                                | \\ [^"']
138                                | (?! \1 ) [^"]
139                               )*
140                        ) \1 }{}x) {
141             my ($q,$v) = ($1,$2);
142             push @o, { T => 'string', E => $&, P => "$q-string",
143                        Q => $q, V => $v};
144         } elsif (s{^$perlop_re|^\;}{}) {
145             push @o, { T => 'op', E => $&, P => $& };
146         } elsif (s/[[{(]//) {
147             push @o, { T => 'bra', E => $&, P => $& };
148         } elsif (s/[]})]//) {
149             push @o, { T => 'ket', E => $&, P => $& };
150         } elsif (s/^( [\$\@\%] )( \{ )//x) {
151             push @o, { T => 'deref', E => $1, P => $1 },
152                      { T => 'bra',   E => $2, P => $2 };
153         } elsif (s/^ [\$\@\%] [^[^{] //x) {
154             push @o, { T => 'specvar', E => $&, P => $& };
155         } elsif (!length) {
156             last;
157         } else {
158             m{^.{0,10}};
159             die "cannot tokenise \`$&'";
160         }
161     }
162     for (my $i=@o-2; $i>0; --$i) {
163         next unless $o[$i+1]{E} eq '.';
164         my @inputs = @o[$i, $i+2];
165         next if grep { !is_string($_) } @inputs;
166         my $q = $inputs[0]{Q};
167         next if grep { $_->{Q} ne $q } @inputs;
168         my $new = { T => 'joinedstrings',
169                     E => (join '.', map { $_->{E} } @inputs),
170                     P => (join '.', map { $_->{P} } @inputs),
171                     V => (join '',  map { $_->{V} } @inputs),
172                     Q => $q,
173                   };
174         @o = (@o[0..$i-1], $new, @o[$i+3..$#o]);
175         print STDERR Dumper(\@o);
176     }
177     debug $ichunkstart, "semiparsed: ".join ' ', map { $_->{P} } @o;
178     return @o;
179 }           
180
181 sub analyse_chunk_core () {
182     die "plain deletion\n" unless defined $after;
183     die "plain insertion\n" unless defined $before;
184     my @xs = semiparse $before;
185     my @ys = semiparse $after;
186     my $next_something = sub {
187         my ($ary,$var,$what) = @_;
188         die "ran out of $what\n" unless @$ary;
189         $$var = shift @$ary;
190     };
191     my ($x,$y);
192     my $next_x = sub { $next_something->(\@xs, \$x, 'before'); };
193     my $next_y = sub { $next_something->(\@ys, \$y, 'after' ); };
194     for (;;) {
195         last unless @xs or @ys;
196         $next_x->();
197         $next_y->();
198         next if $x->{E} eq $y->{E};
199         my $string_changed;
200         my $ye = $y->{E};
201         if ($ye eq '__' or $ye eq 'f_') {
202             $next_y->();
203             die "__ on non-string $y->{P}\n"     unless is_string($y);
204             die "__ on was non-string $y->{P}\n" unless is_string($x);
205             if ($y->{Q} ne "'") {
206                 die "var subst in new string\n"
207                     if $y->{V} =~ m{(?<!\\) [\$\@]};
208             }
209             eval {
210                 die "__ string changed\n"       unless $y->{V} eq $x->{V};
211                 die "__ string quote changed\n" unless $y->{Q} eq $x->{Q};
212             };
213             $string_changed = $@;
214         }
215         if ($ye eq '__') {
216             $_ = $y->{V};
217             die "percent $& in __ ' string\n" if m{\%};
218             die $string_changed if length $string_changed;
219             next;
220         }
221         if ($ye eq 'f_') {
222             my $fmt = $y->{V};
223             die "no percent in _f string\n" unless $fmt =~ m{\%};
224             next unless $string_changed;
225             die "f_ old string '-quoted\n" if $x->{Q} ne '"';
226             my $xs = $x->{V};
227             my $exactly = sub {
228                 my ($lit, $what) = @_;
229                 my $xl = substr($xs, 0, length($lit));
230                 if ($xl ne $lit) {
231                     debug $ichunkstart, "not exactly x: $xl";
232                     debug $ichunkstart, "not exactly y: $lit";
233                     my $next = @ys ? $ys[0]{P} : '(end)';
234                     die "string contents mismatch near $what before $next\n";
235                 }
236                 $xs = substr($xs, length($lit));
237             };
238             for (;;) {
239                 if ($fmt !~ m{\%[^\%]}) {
240                     $exactly->($fmt, '(tail)');
241                     $fmt = '';
242                     last;
243                 }
244                 $exactly->($`, '(literal)');
245                 $fmt = $';
246                 if ($& eq '%%') { $exactly->('%', '%%'); next; }
247                 elsif ($& ne '%s') { die "unhandled %-subst $&\n"; }
248                 $next_y->();
249                 die "expected comma, got $y->{P}\n" unless $y->{E} eq ',';
250                 if ($xs =~ m{^\@}) {
251                     $next_y->();
252                     die "\@... => not string" unless is_string($y);
253                     die "\@... => $y->{P}" if $y->{Q} ne '"';
254                     $exactly->($y->{V}, $y->{P});
255                     next;
256                 }
257                 my $bras = 0;
258                 for (;;) {
259                     if (!$bras and !@ys) {
260                         last;
261                     }
262                     $next_y->();
263                     if (!$bras and
264                         (grep { $y->{E} eq $_ } qw( or xor and not ; : )
265                          or $y->{T} eq 'ket'
266                         )) {
267                         unshift @ys, $y;
268                         last;
269                     }
270                     $xs =~ s{^\s+}{};
271                     #debug $ichunkstart, "TOKEN $y->{P}\n";
272                     $exactly->($y->{E}, $y->{P});
273                     if ($y->{T} eq 'bra' or $y->{E} eq '?') {
274                         $bras++;
275                     } elsif ($y->{T} eq 'ket' or $y->{E} eq ':') {
276                         die "too many kets at $y->{E}\n" unless $bras;
277                         $bras--;
278                     }
279                 }
280             }
281             next;
282         }
283         die "mismatch $x->{P} => $y->{P}\n";
284     }
285 }
286
287 sub analyse_chunk () {
288     for (;;) {
289         eval { analyse_chunk_core(); };
290         return unless length $@;
291         if ($@ =~ m{^missing end of here doc (\S+)\n}) {
292             # fudge this
293             $before .= "\n$1\n";
294             $after .= "\n$1\n";
295             next;
296         } else {
297             die $@;
298         }
299     }
300 }
301
302 our @report;
303 our $last_filehead = -1;
304
305 sub report_on_hunk () {
306     return unless @report;
307     if ($last_filehead != $ifilehead) {
308         foreach (my $i=$ifilehead; $i<$ifirsthunkhead; $i++) {
309             print $d[$i];
310         }
311         $last_filehead = $ifilehead;
312     }
313     my $dummy_r = { S => (scalar @d)+1, E => (scalar @d)+1 };
314     my $r;
315     for (my $i=$ihunkhead; ; $i++) {
316         for (;;) {
317             $r //= shift @report;
318             $r //= $dummy_r;
319             last if $i < $r->{E};
320             confess unless $r->{Done} == 03;
321             $r = undef;
322         }
323
324         last unless $i<$ihunkend;
325
326         foreach my $ds (@{ $debug[$i] }) {
327             print "# $ds\n";
328         }
329
330         if ($i == $r->{S}) {
331             print "!! $r->{M}";
332             $r->{Done} |= 01;
333         }
334         if ($i >= $r->{S}) {
335             print "!";
336             $r->{Done} |= 02;
337         } else {
338             print " ";
339         }
340         print $d[$i];
341     }
342     confess unless $r = $dummy_r;
343 }
344
345 for ($ifilehead = 0; l_ok $ifilehead; $ifilehead++) {
346     m{^diff} or next;
347     $ifirsthunkhead = $ifilehead;
348     while (l_ok $ifirsthunkhead and
349            m{^diff|^index|^---|^\Q+++\E}) {
350         $ifirsthunkhead++
351     }
352     $ihunkhead = $ifirsthunkhead;
353     while (l_ok $ihunkhead) {
354         m{^\@\@} or confess "$ihunkhead $_ ?";
355         my $i = $ihunkhead + 1;
356         for (; ; $i++) {
357             if (!l_ok $i or m{^ } or m{^\@\@}) {
358                 if (defined $ichunkstart) {
359                     $ichunkend = $i;
360                     eval { analyse_chunk(); 1; };
361                     if (length $@) {
362                         push @report, { M => $@,
363                                         S => $ichunkstart,
364                                         E => $ichunkend };
365                     }
366                     $ichunkstart = $ichunkend = $before = $after = undef;
367                 }
368                 l_ok $i or last;
369                 m{^\@\@} and last;
370             } elsif (m{^[-+]}) {
371                 my $which = $& eq '-' ? \$before : \$after;
372                 $ichunkstart //= $i;
373                 $$which //= '';
374                 $$which .= $';
375             } else {
376                 confess "$i $_ ?";
377             }
378         }
379         $ihunkend = $i;
380         report_on_hunk();
381         $ichunkend = $i;
382         $ihunkhead = $i;
383     }
384 }