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   347688 use strict;
  5         40  
  5         142  
4 5     5   24 use warnings;
  5         8  
  5         138  
5 5     5   2605 use parent 'Exporter';
  5         1557  
  5         25  
6              
7 5     5   263 use Carp;
  5         12  
  5         279  
8 5     5   2155 use Clone;
  5         12482  
  5         235  
9 5     5   34 use Scalar::Util qw(blessed);
  5         7  
  5         5179  
10              
11             our $VERSION = '1.03';
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 441 my $class = shift;
58            
59 8         37 my $self = bless { _table => {}, _order => 0 }, $class;
60              
61 8 50       33 croak "line numbers not given" if @_ == 1;
62 8 100       34 $self->add(@_) if @_ > 1;
63            
64 8         34 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         62 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 79 my ($self, $file) = (shift, shift);
95 22 100       196 unless (exists($self->{_table}{$file})) {
96 14         47 $self->{_table}{$file}{_order} = $self->{_order}++;
97 14         35 $self->{_table}{$file}{_lines} = [];
98             }
99 22         25 push @{$self->{_table}{$file}{_lines}}, @_;
  22         55  
100 22         34 delete $self->{_string};
101 22         70 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 11 my ($self, $file) = @_;
114 4         60 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 20         68 sort { $self->{_table}{$a}{_order} <=> $self->{_table}{$b}{_order} }
129 16         19 keys %{$self->{_table}};
  16         71  
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 3 my ($self, $file) = @_;
142 1 50       4 return unless $self->has_file($file);
143 1         2 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     12 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         3  
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 37 my $self = shift;
195 17 100       82 unless (exists($self->{_string})) {
196 15         29 $self->{_string} = '';
197 15         33 foreach my $file ($self->filenames) {
198 28 100       60 $self->{_string} .= ';' if $self->{_string};
199 28         50 $self->{_string} .= "$file";
200 28 50       30 if (my @lines = @{$self->{_table}{$file}{_lines}}) {
  28         80  
201 28         35 $self->{_string} .= ':';
202 28         33 my $beg = shift @lines;
203 28         36 my $end = $beg;
204 28         30 my @ranges;
205 28         38 foreach my $line (@lines) {
206 50 100       75 if ($line == $end + 1) {
207 30         35 $end = $line;
208             } else {
209 20 100       31 if ($end > $beg) {
210 6         16 push @ranges, "$beg-$end";
211             } else {
212 14         18 push @ranges, $beg;
213             }
214 20         31 $beg = $end = $line;
215             }
216             }
217            
218 28 100       47 if ($end > $beg) {
219 8         19 push @ranges, "$beg-$end";
220             } else {
221 20         26 push @ranges, $beg;
222             }
223 28         84 $self->{_string} .= join(',', @ranges);
224             }
225             }
226             }
227 17 100       49 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         65 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 3 my ($self, $other) = @_;
248 1         3 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   24 '""' => sub { shift->format() },
281             '+' => sub {
282 3     3   10 my ($self, $other, $swap) = @_;
283 3 100 33     29 if (blessed $other) {
    50          
284 1         4 return $self->clone->union($other);
285             } elsif (!ref($other) && $other =~ m/^(.+):(\d+)$/) {
286 2 100       6 if ($swap) {
287 1         4 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         50 'eq' => \&equals,
296 5     5   6052 '==' => \=
  5         4800  
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         7 while (my ($oldname, $newname) = each %_) {
313 2 50       5 next unless exists $self->{_table}{$oldname};
314 2 50       5 croak "target name already exist" if exists $self->{_table}{$newname};
315 2         9 $self->{_table}{$newname} = delete $self->{_table}{$oldname};
316             }
317 1         4 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 6 my $self = shift;
351 2 50       7 return unless @_;
352 2 100       9 if ($#_ == 0) {
    50          
353 1         2 my $offset = shift;
354 1         3 while (my ($file, $ref) = each %{$self->{_table}}) {
  4         12  
355 3         4 $ref->{_lines} = [map { $_ + $offset } @{$ref->{_lines}}];
  10         16  
  3         8  
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         2 @{$self->{_table}{$file}{_lines}}];
  2         5  
364             }
365             }
366             } else {
367 0         0 croak "bad number of arguments";
368             }
369 2         5 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 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;