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    '==' => \&equals;
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