8 GetOptions("debug|D+" => \$debug
14 push @{ $debug[$i] }, $s if $debug;
18 unshift @d, "# dummy line to make line 1 index 1 in \@d\n";
20 our $i_last_l_ok = -1;
21 our $count_i_last_l_ok;
26 if ($i == $i_last_l_ok) {
27 confess $i if $count_i_last_l_ok++ > 50;
29 $count_i_last_l_ok = 0;
33 return unless $i < @d;
35 #print STDERR "L $i\n";
41 confess $i unless l_ok $i;
44 our $perlop_text = <<'END'; # c&p from man perlop
45 left terms and list operators (leftward)
49 right ! ~ \ and unary + and -
54 nonassoc named unary operators
55 nonassoc < > <= >= lt gt le ge
56 nonassoc == != <=> eq ne cmp ~~
63 right = += -= *= etc. goto last next redo dump
65 nonassoc list operators (rightward)
70 **= += *= &= &.= <<= &&=
80 foreach (split /\n/, $perlop_text) {
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};
91 foreach my $op (split /\s+/) {
92 next unless length $op;
93 next if $op =~ m{^\w+$};
98 $perlop_re = '(?: '.(join ' | ', @ops).' )';
99 $perlop_re = qr{$perlop_re}x;
100 #print STDERR "$perlop_re\n";
105 our ($ifilehead, $ifirsthunkhead);
106 our ($ihunkhead, $ihunkend);
107 our ($ichunkstart, $ichunkend);
108 our ($before, $after);
110 sub is_string ($) { $_[0]{T} =~ m/heredoc|string/; }
111 sub is_trans ($) { grep { $_[0]{E} eq $_ } qw(__ f_ i_); }
127 # E exact input text (does not contain here doc contents)
128 # P something to print in messages
129 # V value, only for: heredoc string
130 # Q quote characcter, only for: heredoc string
133 if (s{^[\$\@\%]?[_0-9a-zA-Z]+}{}) {
134 push @o, { T => 'ident', E => $&, P => $& };
135 } elsif (s{^\<\<(['"]?)([A-Z_]+)\1}{}) {
136 my ($q,$d) = ($1,$2);
138 push @o, { T => 'heredoc', Q => $q, Delim => $d,
139 E => $&, P => "<<$q$d$q" };
142 ( (?: (?! $d \n ) .* \n )*? )
144 }{ $1 }xe or die "missing end of here doc $d\n";
146 } elsif (s{^ (["'])( (?: [^\\'"]
151 my ($q,$v) = ($1,$2);
152 push @o, { T => 'string', E => $&, P => "$q$q",
154 } elsif (s{^$perlop_re|^\;}{}) {
155 push @o, { T => 'op', E => $&, P => $& };
156 } elsif (s/^[[{(]//) {
157 push @o, { T => 'bra', E => $&, P => $& };
158 } elsif (s/^[]})]//) {
159 push @o, { T => 'ket', E => $&, P => $& };
160 } elsif (s/^( [\$\@\%] )( \{ )//x) {
161 push @o, { T => 'deref', E => $1, P => $1 },
162 { T => 'bra', E => $2, P => $2 };
163 } elsif (s/^ [\$\@\%] [^[^{] //x) {
164 push @o, { T => 'specvar', E => $&, P => $& };
169 die "cannot tokenise \`$&'";
172 for (my $i=0; $i+2 < @o; $i++) {
173 next unless $o[$i+1]{E} eq '.';
174 my @inputs = @o[$i, $i+2];
175 #print STDERR Dumper(\@inputs);
176 next if grep { !is_string($_) } @inputs;
177 my $q = $inputs[0]{Q};
178 next if grep { $_->{Q} ne $q } @inputs;
179 my $new = { T => 'joinedstrings',
180 E => (join '.', map { $_->{E} } @inputs),
181 P => (join '.', map { $_->{P} } @inputs),
182 V => (join '', map { $_->{V} } @inputs),
185 @o = (@o[0..$i-1], $new, @o[$i+3..$#o]);
186 $i--; # counteracts $i++
188 debug $ichunkstart, "semiparsed: ".join ' ', map { $_->{P} } @o;
189 # debug $ichunkstart, "semiparsed V: ".join ' ', map { defined $_->{V} ? ">$_->{V}<" : '-' } @o;
196 sub analyse_chunk_core () {
197 die "plain deletion\n" unless defined $after;
198 die "plain insertion\n" unless defined $before;
199 my @xs = semiparse $before;
200 my @ys = semiparse $after;
201 @analysed_x = @analysed_y = ();
202 my $next_something = sub {
203 my ($ary,$anal,$var,$what) = @_;
204 die "ran out of $what\n" unless @$ary;
206 push @$anal, $r->{P};
210 my $next_x = sub { $next_something->(\@xs, \@analysed_x, \$x, 'before'); };
211 my $next_y = sub { $next_something->(\@ys, \@analysed_y, \$y, 'after' ); };
212 our @y_expect_suffix = ();
214 while (my $e = shift @y_expect_suffix) {
217 or die "suffix mismatch, expected $e got $y->{E}\n";
219 last unless @xs or @ys;
222 next if $x->{E} eq $y->{E};
223 next if $x->{E} eq 'sprintf' and $y->{E} eq 'f_';
224 next if $x->{E} eq 'die' and $y->{E} eq 'confess';
228 and is_trans($ys[1])) {
230 $next_y->(); # __ f_ i_
231 @y_expect_suffix = ')';
232 } elsif ($y->{E} eq '('
236 and (grep { $_ eq $analysed_y[-1] } (qw( => [ { ? : . ),
238 $next_y->(); # __ f_ i_
239 @y_expect_suffix = ')';
245 die "__ on non-string $y->{P}\n" unless is_string($y);
246 die "__ on was non-string $x->{P}\n" unless is_string($x);
247 if ($y->{Q} ne "'") {
248 die "var subst in new string\n"
249 if $y->{V} =~ m{(?<!\\) [\$\@]};
252 die "__ string changed\n" unless $y->{V} eq $x->{V};
253 die "__ string quote changed\n" unless $y->{Q} eq $x->{Q};
255 $string_changed = $@;
259 die "percent $& in __ ' string\n" if m{\%};
260 die $string_changed if length $string_changed;
264 die $string_changed if length $string_changed;
269 die "no percent in _f string\n" unless $fmt =~ m{\%};
270 next unless $string_changed;
271 die "f_ old string '-quoted\n" if $x->{Q} ne '"';
274 my ($lit, $what) = @_;
275 my $xl = substr($xs, 0, length($lit));
277 debug $ichunkstart, "not exactly x: ..".qp($xs);
278 debug $ichunkstart, "not exactly y: ".qp($lit);
279 my $next = @ys ? $ys[0]{P} : '(end)';
280 die "string contents mismatch near $what before $next\n";
282 $xs = substr($xs, length($lit));
285 if ($fmt !~ m{\%[^\%]}) {
286 $exactly->($fmt, '(tail)');
288 die "text deleted from end of string: ".qp($xs)."\n"
292 $exactly->($`, '(literal)');
294 if ($& eq '%%') { $exactly->('%', '%%'); next; }
295 elsif ($& ne '%s') { die "unhandled %-subst $&\n"; }
297 die "expected comma, got $y->{P}\n" unless $y->{E} eq ',';
302 # X has "<earlier>" . <something>
303 # Y has "<earlier>%s" [other args] , <something>
304 $next_x->(); # eat the '.'
309 die "\@... => not string" unless is_string($y);
310 die "\@... => $y->{P}" if $y->{Q} ne '"';
311 $exactly->($y->{V}, $y->{P});
316 if (!$bras and !@ys) {
321 (grep { $y->{E} eq $_ } qw( or xor and not ; :
322 if unless while when )
326 # lookahead shows close of containing scope
327 # or lower precedence operator
332 $xs =~ s{^\s+}{} if $bras;
333 if (is_string($y) and $y->{Q} eq '"') {
334 $exactly->($y->{V}, $y->{P});
337 $exactly->($y->{E}, $y->{P});
338 if ($y->{T} eq 'bra' or $y->{E} eq '?') {
340 } elsif ($y->{T} eq 'ket' or $y->{E} eq ':') {
341 die "too many kets at $y->{E}\n" unless $bras;
348 die "mismatch $x->{P} => $y->{P}\n";
352 sub analyse_chunk () {
354 eval { analyse_chunk_core(); };
355 return unless length $@;
356 if ($@ =~ m{^missing end of here doc (\S+)\n}) {
368 our $last_filehead = -1;
370 sub report_on_hunk () {
371 return unless @report;
372 if ($last_filehead != $ifilehead) {
373 foreach (my $i=$ifilehead; $i<$ifirsthunkhead; $i++) {
376 $last_filehead = $ifilehead;
378 my $dummy_r = { S => (scalar @d)+1, E => (scalar @d)+1 };
380 for (my $i=$ihunkhead; ; $i++) {
382 $r //= shift @report;
384 last if $i < $r->{E};
385 confess unless $r->{Done} == 03;
389 last unless $i<$ihunkend;
391 foreach my $ds (@{ $debug[$i] }) {
407 confess unless $r = $dummy_r;
410 for ($ifilehead = 0; l_ok $ifilehead; $ifilehead++) {
412 $ifirsthunkhead = $ifilehead;
413 while (l_ok $ifirsthunkhead and
414 m{^diff|^index|^---|^\Q+++\E}) {
417 $ihunkhead = $ifirsthunkhead;
418 while (l_ok $ihunkhead) {
419 m{^\@\@} or confess "$ihunkhead $_ ?";
420 my $i = $ihunkhead + 1;
422 if (!l_ok $i or m{^ } or m{^\@\@}) {
423 if (defined $ichunkstart) {
425 eval { analyse_chunk(); 1; };
427 debug $ichunkstart, "done x: @analysed_x";
428 debug $ichunkstart, "done y: @analysed_y";
429 push @report, { M => $@,
433 $ichunkstart = $ichunkend = $before = $after = undef;
438 my $which = $& eq '-' ? \$before : \$after;