File Coverage

lib/Test/DocClaims/Lines.pm
Criterion Covered Total %
statement 136 137 99.2
branch 53 62 85.4
condition 9 11 81.8
subroutine 16 16 100.0
pod 5 5 100.0
total 219 231 94.8


line stmt bran cond sub pod time code
1             package Test::DocClaims::Lines;
2              
3             # Copyright (c) Scott E. Lee
4              
5 11     11   552632 use 5.008009;
  11         99  
6 11     11   48 use strict;
  11         30  
  11         265  
7 11     11   44 use warnings;
  11         15  
  11         315  
8 11     11   55 use Carp;
  11         19  
  11         536  
9              
10 11     11   3530 use Test::DocClaims::Line;
  11         22  
  11         13430  
11              
12             # Tell croak to skip over calls from here.
13             our @CARP_NOT = qw< Test::DocClaims >;
14              
15             # Keys in the blessed hash
16             # {lines} array of Test::DocClaims::Line objects
17             # {current} the current index into {lines}
18             # {paths} list of paths and/or globs used to read the lines
19              
20             =head1 NAME
21              
22             Test::DocClaims::Lines - Represent lines form one of more files
23              
24             =head1 SYNOPSIS
25              
26             use Test::DocClaims::Lines;
27             my $lines = Test::DocClaims::Lines->new("t/Foo*.t");
28             my %files;
29             while ( !$lines->is_eof ) {
30             my $line = $lines->current_line;
31             $files{ $line->path }[ $line->lnum - 1 ] = $line->text;
32             $lines->advance_line;
33             }
34              
35             =head1 DESCRIPTION
36              
37             This holds a collection of lines from one or more files.
38             The file path and line number of each line is recorded as well as
39             other attributes of both the file and the individual lines.
40             For example, it records whether a file supports POD documentation
41             and whether each line is POD documentation or not.
42             Each line in the list is represented as a Test::DocClaims::Line object.
43              
44             There is a concept of current line.
45             This can be used to step through the lines sequentially.
46              
47             =head1 CONSTRUCTOR
48              
49             =head2 new I
50              
51             The I argument specifies a list of one or more files.
52             It can be one of:
53              
54             - a string which is the path to a file or a wildcard which is
55             expanded by the glob built-in function.
56             - a ref to a hash with these keys:
57             - path: path or wildcard (required)
58             - has_pod: true if the file can have POD (optional)
59             - a ref to an array, where each element is a path, wildcard or hash
60             as above
61              
62             If a list of files is given, those files are read in order and the
63             lines in each are concatenated.
64             If a wildcard expands to more than one file they are read in the order
65             returned by the glob built-in.
66              
67             =cut
68              
69             sub new {
70 151     151 1 1346 my $class = shift;
71 151         182 my $file_spec = shift;
72 151 50       251 croak "missing arg to Test::DocClaims::Line->new" unless $file_spec;
73 151   33     448 my $self = bless {}, ref($class) || $class;
74 151         358 $self->{lines} = [];
75 151         217 $self->{current} = 0;
76 151         199 $self->{paths} = [];
77 151         305 foreach my $attrs ( $self->_file_spec_to_list($file_spec) ) {
78 203         335 $self->_add_file($attrs);
79             }
80 151         324 return $self;
81             }
82              
83             =head1 ACCESSORS
84              
85             =head2 is_eof
86              
87             This returns true if the end of the lines has been reached.
88              
89             =cut
90              
91             sub is_eof {
92 12458     12458 1 13567 my $self = shift;
93 12458         12941 return $self->{current} >= scalar( @{ $self->{lines} } );
  12458         23252  
94             }
95              
96             =head2 advance_line
97              
98             This advances to the next line and returns the Test::DocClaims::Line
99             object for that line.
100             If there is no next line, undef is returned.
101              
102             =cut
103              
104             sub advance_line {
105 5311     5311 1 5767 my $self = shift;
106 5311         5622 $self->{current}++;
107 5311         6241 return $self->current_line;
108             }
109              
110             =head2 current_line
111              
112             Return the current line, a Test::DocClaims::Line object.
113             If there is no current line because the end has been reached, undef is
114             returned.
115              
116             =cut
117              
118             sub current_line {
119 10640     10640 1 10962 my $self = shift;
120 10640 100       12310 return undef if $self->is_eof;
121 10494         19807 return $self->{lines}[ $self->{current} ];
122             }
123              
124             =head2 paths
125              
126             Return a list of strings for the paths and/or globs used to read the file
127             or files.
128              
129             =cut
130              
131             sub paths {
132 1     1 1 3 my $self = shift;
133 1         2 return @{ $self->{paths} };
  1         5  
134             }
135              
136             # Convert a file spec arg to a list of attribute hashes representing the
137             # files.
138             sub _file_spec_to_list {
139 151     151   183 my $self = shift;
140 151         174 my $arg = shift;
141 151 100       330 $arg = [$arg] unless ref $arg eq "ARRAY";
142              
143             # Expand wildcards to a list of paths (or hashes), putting the results
144             # into @specs.
145 151         183 my @specs;
146 151         230 foreach my $item (@$arg) {
147 166 100       268 if ( ref $item eq "HASH" ) {
148             croak "file spec is hash, but it has no 'path' key"
149 29 50       67 unless length $item->{path};
150 29         32 push @{ $self->{paths} }, "$item->{path}";
  29         65  
151 29         59 my @list = _glob( $item->{path} );
152 29         1441 @list = sort @list;
153 29 50       59 croak "no such file ($item->{path})" unless @list;
154 29         45 foreach my $path (@list) {
155 37         131 push @specs, { %$item, path => $path };
156             }
157             } else {
158 137         143 push @{ $self->{paths} }, "$item";
  137         302  
159 137         249 my @list = _glob($item);
160 137         10344 @list = sort @list;
161 137 50       264 croak "no such file ($item)" unless @list;
162 137         289 push @specs, @list;
163             }
164             }
165              
166             # Convert each item in the list to a hash if it isn't already and fill
167             # in any missing attributes with default values.
168 151         257 foreach my $item (@specs) {
169 203 100       338 if ( ref $item eq "HASH" ) {
170 37         72 my %default = $self->_attrs_of_file( $item->{path} );
171 37         71 foreach my $key ( keys %default ) {
172 74 100       163 $item->{$key} = $default{$key} unless defined $item->{$key};
173             }
174             } else {
175 166         307 $item = { path => $item, $self->_attrs_of_file($item) };
176             }
177             }
178 151         310 return @specs;
179             }
180              
181             # This wrapper for the glob function can be overridden at run time (by the
182             # TestTester module), where the system glob can only be overridden at
183             # compile time.
184             sub _glob {
185 17     17   565 return glob( $_[0] );
186             }
187              
188             # Each attribute hash has at least these keys:
189             # path the path of the file
190             # has_pod true if it should be parsed as POD
191             # white true if amount of white space at beginning of lines is preserved
192             # TODO remove white attribute
193             sub _attrs_of_file {
194 203     203   242 my $self = shift;
195 203         255 my $path = shift;
196 203         365 my %attrs = (
197             has_pod => 0,
198             white => 0,
199             );
200 203 100       695 if ( $path =~ /\.p[lm]$/ ) {
    100          
    100          
201 70         100 $attrs{has_pod} = 1;
202             } elsif ( $path =~ /\.pod$/ ) {
203 1         2 $attrs{has_pod} = 1;
204             } elsif ( $path =~ /\.t$/ ) {
205 128         180 $attrs{has_pod} = 1;
206             }
207 203         723 return %attrs;
208             }
209              
210             sub _add_file {
211 203     203   226 my $self = shift;
212 203         221 my $attrs = shift;
213 203         378 my $lines = _read_file( $attrs->{path} );
214 203         1211 my $lnum = 0;
215 203         289 my $doc_mode = !$attrs->{has_pod};
216 203         229 my $code = undef;
217 203         261 my $todo = undef;
218 203         223 my $in_data = 0; # ignore TestTester files in __DATA__ section
219              
220 203         281 foreach my $text (@$lines) {
221 5488 100       9117 $in_data = 1 if $text =~ /^__(END|DATA)__$/;
222 5488 100 100     7914 last if $in_data && $text =~ /^FILE:<.*>-/;
223 5478         10069 my %hash = ( orig => $text, lnum => ++$lnum );
224 5478         5653 my $this_line_doc;
225 5478 100       7916 if ( $attrs->{has_pod} ) {
226 5477 100       8820 if ( $text =~ /^=([a-zA-Z]\S*)(\s+(.*))?\s*$/ ) {
227 615         1462 my ( $cmd, $cmd_text ) = ( $1, $2 );
228 615         758 $hash{is_doc} = 1;
229 615         669 $doc_mode = 1;
230 615 100       1847 if ( $cmd eq "pod" ) {
    100          
    100          
    100          
    100          
231 34         44 $this_line_doc = 0;
232             } elsif ( $cmd =~ /^cut/ ) {
233 143         223 my ( $format, $args ) = _parse_pod_command($cmd_text);
234 143         181 $this_line_doc = 0;
235 143         204 $doc_mode = 0;
236             } elsif ( $cmd =~ /^begin/ ) {
237 28         59 my ( $format, $args ) = _parse_pod_command($cmd_text);
238 28 50       72 if ( $format eq "DC_CODE" ) {
239 28         38 $this_line_doc = 0;
240 28         40 $code = $args;
241             }
242             } elsif ( $cmd =~ /^end/ ) {
243 28         45 my ( $format, $args ) = _parse_pod_command($cmd_text);
244 28 50       73 if ( $format eq "DC_CODE" ) {
245 28         33 $this_line_doc = 0;
246 28         49 $code = undef;
247             }
248             } elsif ( $cmd =~ /^for/ ) {
249 4         17 my ( $format, $args ) = _parse_pod_command($cmd_text);
250 4 50       13 if ( $format eq "DC_TODO" ) {
251 4         6 $this_line_doc = 0;
252 4         7 $todo = $args;
253             }
254             }
255             }
256             }
257 5478 100       7224 if ( !defined $this_line_doc ) {
258 5241 100 100     10997 $this_line_doc = 1 if $code || $doc_mode;
259             }
260 5478 100       7815 $hash{is_doc} = $this_line_doc ? 1 : 0;
261 5478         6195 $hash{has_pod} = $attrs->{has_pod};
262 5478         5778 $hash{code} = $code;
263 5478         5548 $hash{todo} = $todo;
264 5478         5477 $todo = undef;
265 5478         14647 $text =~ s/\s+$//; # remove CRLF, NL and trailing white space
266 5478 50       11131 $text =~ s/^\s+/ / if !$attrs->{white};
267 5478         7608 $hash{text} = $text;
268 5478         5965 $hash{file} = $attrs;
269 5478         5327 push @{ $self->{lines} }, Test::DocClaims::Line->new(%hash);
  5478         14956  
270             }
271 203         575 return $self;
272             }
273              
274             sub _parse_pod_command {
275 203     203   266 my $text = shift;
276 203         262 my ( $format, %args );
277 203 100       508 if ( $text =~ /^\s*(\S+)(\s+(.*))?$/ ) {
278 60         109 $format = $1;
279             %args =
280 15 100       46 map { /^(.+?)=(.*)$/ ? ( $1 => $2 ) : ( $1 => 1 ) }
281 60   100     225 grep { length $_ }
  15         24  
282             split " ", $3 || "";
283             } else {
284 143         180 $format = "";
285             }
286 203         419 return ( $format, \%args );
287             }
288              
289             sub _read_file {
290 26     26   31 my $path = shift;
291 26         33 my @lines;
292 26 50       784 if ( open my $fh, "<", $path ) {
293 26         2080 @lines = <$fh>;
294 26         225 close $fh;
295             } else {
296 0         0 die "cannot read $path: $!\n";
297             }
298 26         122 return \@lines;
299             }
300              
301             =head1 COPYRIGHT
302              
303             Copyright (c) Scott E. Lee
304              
305             =cut
306              
307             1;
308