1package Text::Locus; 2 3use strict; 4use warnings; 5use parent 'Exporter'; 6 7use Carp; 8use Clone; 9use Scalar::Util qw(blessed); 10 11our $VERSION = '1.04'; 12 13=head1 NAME 14 15Text::Locus - text file locations 16 17=head1 SYNOPSIS 18 19use Text::Locus; 20 21$locus = new Text::Locus; 22 23$locus = new Text::Locus($file, $line); 24 25$locus->add($file, $line); 26 27$s = $locus->format; 28 29$locus->fixup_names('old' => 'new'); 30 31$locus->fixup_lines(%hash); 32 33print "$locus: text\n"; 34 35$res = $locus1 + $locus2; 36 37=head1 DESCRIPTION 38 39B<Text::Locus> provides a class for representing locations in text 40files. A simple location consists of file name and line number. 41e.g. C<file:10>. In its more complex form, the location represents a 42text fragment spanning several lines, such as C<file:10-45>. Such a 43fragment need not be contiguous, a valid location can also look like 44this: C<file:10-35,40-48>. Moreover, it can span multiple files as 45well: C<foo:10-35,40-48;bar:15,18>. 46 47=head1 CONSTRUCTOR 48 49 $locus = new Text::Locus($file, $line, ...); 50 51Creates a new locus object. Arguments are optional. If given, they 52indicate the source file name and line numbers this locus is to represent. 53 54=cut 55 56sub new { 57 my $class = shift; 58 59 my $self = bless { _table => {}, _order => 0 }, $class; 60 61 croak "line numbers not given" if @_ == 1; 62 $self->add(@_) if @_ > 1; 63 64 return $self; 65} 66 67=head1 METHODS 68 69=head2 clone 70 71 $locus->clone 72 73Creates a new B<Text::Locus> which is exact copy of B<$locus>. 74 75=cut 76 77sub clone { 78 my $self = shift; 79 return Clone::clone($self); 80} 81 82=head2 add 83 84 $locus->add($file, $line, [$line1 ...]); 85 86Adds new location to the locus. Use this for statements spanning several 87lines and/or files. 88 89Returns B<$locus>. 90 91=cut 92 93sub add { 94 my ($self, $file) = (shift, shift); 95 unless (exists($self->{_table}{$file})) { 96 $self->{_table}{$file}{_order} = $self->{_order}++; 97 $self->{_table}{$file}{_lines} = []; 98 } 99 push @{$self->{_table}{$file}{_lines}}, @_; 100 delete $self->{_string}; 101 return $self; 102} 103 104=head2 has_file 105 106 if ($locus->has_file($file)) ... 107 108Returns true if the filename B<$file> is present in the locus. 109 110=cut 111 112sub has_file { 113 my ($self, $file) = @_; 114 return exists($self->{_table}{$file}); 115} 116 117=head2 filenames 118 119 @list = $locus->filenames 120 121Returns a list of file names from the locus. The list preserves the 122order in which filenames were added to the locus. 123 124=cut 125 126sub filenames { 127 my ($self) = @_; 128 sort { $self->{_table}{$a}{_order} <=> $self->{_table}{$b}{_order} } 129 keys %{$self->{_table}}; 130} 131 132=head2 filelines 133 134 @list = $locus->filelines($file) 135 136Returns the list of lines in <$file> which are part of this locus. 137 138=cut 139 140sub filelines { 141 my ($self, $file) = @_; 142 return unless $self->has_file($file); 143 return @{$self->{_table}{$file}{_lines}} 144} 145 146=head2 union 147 148 $locus->union($locus2); 149 150Converts B<$locus> to a union of B<$locus> and B<$locus2>. 151 152=cut 153 154sub union { 155 my ($self, $other) = @_; 156 croak "not the same class" 157 unless blessed($other) && $other->isa(__PACKAGE__); 158 while (my ($file, $tab) = each %{$other->{_table}}) { 159 $self->add($file, @{$tab->{_lines}}); 160 } 161 return $self; 162} 163 164=head2 format 165 166 $s = $locus->format($msg); 167 168Returns string representation of the locus. Argument, if supplied, 169will be prepended to the formatted locus with a C<: > in between. If multiple 170arguments are supplied, their string representations will be concatenated, 171separated by horizontal space characters. This is useful for formatting error 172messages. 173 174If the locus contains multiple file locations, B<format> tries to compact 175them by representing contiguous line ranges as B<I<X>-I<Y>> and outputting 176each file name once. Line ranges are separated by commas. File locations 177are separated by semicolons. E.g.: 178 179 $locus = new Text::Locus("foo", 1); 180 $locus->add("foo", 2); 181 $locus->add("foo", 3); 182 $locus->add("foo", 5); 183 $locus->add("bar", 2); 184 $locus->add("bar", 7); 185 print $locus->format("here it goes"); 186 187will produce the following: 188 189 foo:1-3,5;bar:2,7: here it goes 190 191=cut 192 193sub format { 194 my $self = shift; 195 unless (exists($self->{_string})) { 196 $self->{_string} = ''; 197 foreach my $file ($self->filenames) { 198 $self->{_string} .= ';' if $self->{_string}; 199 $self->{_string} .= "$file"; 200 if (my @lines = @{$self->{_table}{$file}{_lines}}) { 201 $self->{_string} .= ':'; 202 my $beg = shift @lines; 203 my $end = $beg; 204 my @ranges; 205 foreach my $line (@lines) { 206 if ($line == $end + 1) { 207 $end = $line; 208 } else { 209 if ($end > $beg) { 210 push @ranges, "$beg-$end"; 211 } else { 212 push @ranges, $beg; 213 } 214 $beg = $end = $line; 215 } 216 } 217 218 if ($end > $beg) { 219 push @ranges, "$beg-$end"; 220 } else { 221 push @ranges, $beg; 222 } 223 $self->{_string} .= join(',', @ranges); 224 } 225 } 226 } 227 if (@_) { 228 if ($self->{_string} ne '') { 229 return "$self->{_string}: " . join(' ', @_); 230 } else { 231 return join(' ', @_); 232 } 233 } 234 return $self->{_string}; 235} 236 237=head2 equals 238 239 $bool = $locus->equals($other); 240 241Returns true if $locus and $other are equal (i.e. refer to the same 242source file location). 243 244=cut 245 246sub equals { 247 my ($self, $other) = @_; 248 return $self->format eq $other->format; 249} 250 251=head1 OVERLOADED OPERATIONS 252 253When used in a string, the locus object formats itself. E.g. to print 254a diagnostic message one can write: 255 256 print "$locus: some text\n"; 257 258In fact, this method is preferred over calling B<$locus-E<gt>format>. 259 260Two objects can be added: 261 262 $loc1 + $loc2 263 264This will produce a new B<Text::Locus> containing locations from both I<$loc1> 265and I<$loc2>. 266 267Moreover, a term can also be a string in the form C<I<file>:I<line>>: 268 269 $loc + "file:10" 270 271or 272 273 "file:10" + $loc 274 275Two locus objects can be compared for equality using B<==> or B<eq> operators. 276 277=cut 278 279use overload 280 '""' => sub { shift->format() }, 281 '+' => sub { 282 my ($self, $other, $swap) = @_; 283 if (blessed $other) { 284 return $self->clone->union($other); 285 } elsif (!ref($other) && $other =~ m/^(.+):(\d+)$/) { 286 if ($swap) { 287 return new Text::Locus($1, $2) + $self; 288 } else { 289 return $self->clone->add($1, $2); 290 } 291 } else { 292 croak "bad argument type in locus addition"; 293 } 294 }, 295 'eq' => \&equals, 296 '==' => \= 297 298=head1 FIXUPS 299 300=head2 fixup_names 301 302 $locus->fixup_names('foo' => 'bar', 'baz' => 'quux'); 303 304Replaces file names in B<$locus> according to the arguments. In the example 305above, C<foo> becomes C<bar>, and C<baz> becomes C<quux>. 306 307=cut 308 309sub fixup_names { 310 my $self = shift; 311 local %_ = @_; 312 while (my ($oldname, $newname) = each %_) { 313 next unless exists $self->{_table}{$oldname}; 314 croak "target name already exist" if exists $self->{_table}{$newname}; 315 $self->{_table}{$newname} = delete $self->{_table}{$oldname}; 316 } 317 delete $self->{_string}; 318} 319 320=head2 fixup_lines 321 322 $locus->fixup_lines('foo' => 1, 'baz' => -2); 323 324Offsets line numbers for each named file by the given number of lines. E.g.: 325 326 $locus = new Text::Locus("foo", 1); 327 $locus->add("foo", 2); 328 $locus->add("foo", 3); 329 $locus->add("bar", 3); 330 $locus->fixup_lines(foo => 1. bar => -1); 331 print $locus->format; 332 333will produce 334 335 foo:2-4,bar:2 336 337Given a single argument, the operation affects all locations. E.g., 338adding the following to the example above: 339 340 $locus->fixup_lines(10); 341 print $locus->format; 342 343will produce 344 345 foo:22-24;bar:22 346 347=cut 348 349sub fixup_lines { 350 my $self = shift; 351 return unless @_; 352 if ($#_ == 0) { 353 my $offset = shift; 354 while (my ($file, $ref) = each %{$self->{_table}}) { 355 $ref->{_lines} = [map { $_ + $offset } @{$ref->{_lines}}]; 356 } 357 } elsif ($#_ % 2) { 358 local %_ = @_; 359 while (my ($file, $offset) = each %_) { 360 if (exists($self->{_table}{$file})) { 361 $self->{_table}{$file}{_lines} = 362 [map { $_ + $offset } 363 @{$self->{_table}{$file}{_lines}}]; 364 } 365 } 366 } else { 367 croak "bad number of arguments"; 368 } 369 delete $self->{_string}; 370} 371 372=head1 AUTHOR 373 374Sergey Poznyakoff, E<lt>gray@gnu.orgE<gt> 375 376=head1 COPYRIGHT AND LICENSE 377 378Copyright (C) 2018-2021 by Sergey Poznyakoff 379 380This library is free software; you can redistribute it and/or modify it 381under the terms of the GNU General Public License as published by the 382Free Software Foundation; either version 3 of the License, or (at your 383option) any later version. 384 385It is distributed in the hope that it will be useful, 386but WITHOUT ANY WARRANTY; without even the implied warranty of 387MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 388GNU General Public License for more details. 389 390You should have received a copy of the GNU General Public License along 391with this library. If not, see <http://www.gnu.org/licenses/>. 392 393=cut 394 3951; 396