File Coverage

lib/Text/Locus.pm
Criterion Covered Total %
statement 113 115 98.2
branch 34 44 77.2
condition 2 6 33.3
subroutine 20 20 100.0
pod 10 11 90.9
total 179 196 91.3


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