File Coverage

lib/Text/Locus.pm
Criterion Covered Total %
statement 103 105 98.1
branch 33 42 78.5
condition 2 6 33.3
subroutine 16 16 100.0
pod 6 7 85.7
total 160 176 90.9


line stmt bran cond sub pod time code
1             package Text::Locus;
2              
3 4     4   250754 use strict;
  4         32  
  4         100  
4 4     4   17 use warnings;
  4         5  
  4         112  
5 4     4   1675 use parent 'Exporter';
  4         1105  
  4         19  
6              
7 4     4   180 use Carp;
  4         6  
  4         181  
8 4     4   1529 use Clone;
  4         8887  
  4         162  
9 4     4   26 use Scalar::Util qw(blessed);
  4         8  
  4         2941  
10              
11             our $VERSION = '1.01';
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 278 my $class = shift;
58            
59 8         25 my $self = bless { _table => {}, _order => 0 }, $class;
60              
61 8 50       23 croak "line numbers not given" if @_ == 1;
62 8 100       24 $self->add(@_) if @_ > 1;
63            
64 8         30 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 2     2 1 2 my $self = shift;
79 2         33 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 20     20 1 42 my ($self, $file) = (shift, shift);
95 20 100       120 unless (exists($self->{_table}{$file})) {
96 12         31 $self->{_table}{$file}{_order} = $self->{_order}++;
97 12         19 $self->{_table}{$file}{_lines} = [];
98             }
99 20         26 push @{$self->{_table}{$file}{_lines}}, @_;
  20         41  
100 20         27 delete $self->{_string};
101 20         47 return $self;
102             }
103              
104             =head2 union
105              
106             $locus->union($locus2);
107              
108             Converts B<$locus> to a union of B<$locus> and B<$locus2>.
109              
110             =cut
111              
112             sub union {
113 1     1 1 2 my ($self, $other) = @_;
114 1 50 33     16 croak "not the same class"
115             unless blessed($other) && $other->isa(__PACKAGE__);
116 1         2 while (my ($file, $tab) = each %{$other->{_table}}) {
  2         39  
117 1         3 $self->add($file, @{$tab->{_lines}});
  1         2  
118             }
119 1         6 return $self;
120             }
121              
122             =head2 format
123              
124             $s = $locus->format($msg);
125              
126             Returns string representation of the locus. Argument, if supplied,
127             will be prepended to the formatted locus with a C<: > in between. If multiple
128             arguments are supplied, their string representations will be concatenated,
129             separated by horizontal space characters. This is useful for formatting error
130             messages.
131              
132             If the locus contains multiple file locations, B tries to compact
133             them by representing contiguous line ranges as B-I> and outputting
134             each file name once. Line ranges are separated by commas. File locations
135             are separated by semicolons. E.g.:
136              
137             $locus = new Text::Locus("foo", 1);
138             $locus->add("foo", 2);
139             $locus->add("foo", 3);
140             $locus->add("foo", 5);
141             $locus->add("bar", 2);
142             $locus->add("bar", 7);
143             print $locus->format("here it goes");
144              
145             will produce the following:
146              
147             foo:1-3,5;bar:2,7: here it goes
148              
149             =cut
150              
151             sub format {
152 15     15 1 32 my $self = shift;
153 15 100       62 unless (exists($self->{_string})) {
154 13         25 $self->{_string} = '';
155 13         14 foreach my $file (sort {
156             $self->{_table}{$a}{_order} <=> $self->{_table}{$b}{_order}
157 12         29 }
158 13         51 keys %{$self->{_table}}) {
159 24 100       53 $self->{_string} .= ';' if $self->{_string};
160 24         36 $self->{_string} .= "$file";
161 24 50       24 if (my @lines = @{$self->{_table}{$file}{_lines}}) {
  24         53  
162 24         29 $self->{_string} .= ':';
163 24         27 my $beg = shift @lines;
164 24         26 my $end = $beg;
165 24         17 my @ranges;
166 24         27 foreach my $line (@lines) {
167 46 100       58 if ($line == $end + 1) {
168 26         25 $end = $line;
169             } else {
170 20 100       28 if ($end > $beg) {
171 6         13 push @ranges, "$beg-$end";
172             } else {
173 14         16 push @ranges, $beg;
174             }
175 20         23 $beg = $end = $line;
176             }
177             }
178            
179 24 100       36 if ($end > $beg) {
180 6         15 push @ranges, "$beg-$end";
181             } else {
182 18         19 push @ranges, $beg;
183             }
184 24         61 $self->{_string} .= join(',', @ranges);
185             }
186             }
187             }
188 15 100       35 if (@_) {
189 2 100       8 if ($self->{_string} ne '') {
190 1         6 return "$self->{_string}: " . join(' ', @_);
191             } else {
192 1         5 return join(' ', @_);
193             }
194             }
195 13         54 return $self->{_string};
196             }
197              
198             =head1 OVERLOADED OPERATIONS
199              
200             When used in a string, the locus object formats itself. E.g. to print
201             a diagnostic message one can write:
202              
203             print "$locus: some text\n";
204              
205             In fact, this method is preferred over calling B<$locus-Eformat>.
206              
207             Two objects can be added:
208              
209             $loc1 + $loc2
210              
211             This will produce a new B containing locations from both I<$loc1>
212             and I<$loc2>.
213              
214             Moreover, a term can also be a string in the form C:I>:
215              
216             $loc + "file:10"
217              
218             or
219              
220             "file:10" + $loc
221            
222             =cut
223              
224             use overload
225 4     4   20 '""' => sub { shift->format() },
226             '+' => sub {
227 3     3   8 my ($self, $other, $swap) = @_;
228 3 100 33     24 if (blessed $other) {
    50          
229 1         5 return $self->clone->union($other);
230             } elsif (!ref($other) && $other =~ m/^(.+):(\d+)$/) {
231 2 100       3 if ($swap) {
232 1         2 return new Text::Locus($1, $2) + $self;
233             } else {
234 1         3 return $self->clone->add($1, $2);
235             }
236             } else {
237 0         0 croak "bad argument type in locus addition";
238             }
239 4     4   4189 };
  4         3508  
  4         31  
240              
241             =head1 FIXUPS
242              
243             =head2 fixup_names
244              
245             $locus->fixup_names('foo' => 'bar', 'baz' => 'quux');
246              
247             Replaces file names in B<$locus> according to the arguments. In the example
248             above, C becomes C, and C becomes C.
249              
250             =cut
251              
252             sub fixup_names {
253 1     1 1 2 my $self = shift;
254 1         3 local %_ = @_;
255 1         36 while (my ($oldname, $newname) = each %_) {
256 2 50       5 next unless exists $self->{_table}{$oldname};
257 2 50       5 croak "target name already exist" if exists $self->{_table}{$newname};
258 2         7 $self->{_table}{$newname} = delete $self->{_table}{$oldname};
259             }
260 1         4 delete $self->{_string};
261             }
262              
263             =head2 fixup_lines
264              
265             $locus->fixup_lines('foo' => 1, 'baz' => -2);
266              
267             Offsets line numbers for each named file by the given number of lines. E.g.:
268              
269             $locus = new Text::Locus("foo", 1);
270             $locus->add("foo", 2);
271             $locus->add("foo", 3);
272             $locus->add("bar", 3);
273             $locus->fixup_lines(foo => 1. bar => -1);
274             print $locus->format;
275              
276             will produce
277              
278             foo:2-4,bar:2
279              
280             Given a single argument, the operation affects all locations. E.g.,
281             adding the following to the example above:
282              
283             $locus->fixup_lines(10);
284             print $locus->format;
285              
286             will produce
287              
288             foo:22-24;bar:22
289            
290             =cut
291              
292             sub fixup_lines {
293 2     2 1 5 my $self = shift;
294 2 50       5 return unless @_;
295 2 100       11 if ($#_ == 0) {
    50          
296 1         2 my $offset = shift;
297 1         3 while (my ($file, $ref) = each %{$self->{_table}}) {
  4         11  
298 3         4 $ref->{_lines} = [map { $_ + $offset } @{$ref->{_lines}}];
  10         14  
  3         4  
299             }
300             } elsif ($#_ % 2) {
301 1         3 local %_ = @_;
302 1         5 while (my ($file, $offset) = each %_) {
303 2 50       4 if (exists($self->{_table}{$file})) {
304             $self->{_table}{$file}{_lines} =
305 8         16 [map { $_ + $offset }
306 2         2 @{$self->{_table}{$file}{_lines}}];
  2         4  
307             }
308             }
309             } else {
310 0         0 croak "bad number of arguments";
311             }
312 2         7 delete $self->{_string};
313             }
314              
315             =head1 AUTHOR
316              
317             Sergey Poznyakoff, Egray@gnu.orgE
318              
319             =head1 COPYRIGHT AND LICENSE
320              
321             Copyright (C) 2018 by Sergey Poznyakoff
322              
323             This library is free software; you can redistribute it and/or modify it
324             under the terms of the GNU General Public License as published by the
325             Free Software Foundation; either version 3 of the License, or (at your
326             option) any later version.
327              
328             It is distributed in the hope that it will be useful,
329             but WITHOUT ANY WARRANTY; without even the implied warranty of
330             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
331             GNU General Public License for more details.
332              
333             You should have received a copy of the GNU General Public License along
334             with this library. If not, see .
335            
336             =cut
337              
338             1;