File Coverage

blib/lib/Error/Show.pm
Criterion Covered Total %
statement 178 259 68.7
branch 58 108 53.7
condition 29 69 42.0
subroutine 12 13 92.3
pod 2 4 50.0
total 279 453 61.5


line stmt bran cond sub pod time code
1             package Error::Show;
2              
3 4     4   278315 use 5.024000;
  4         43  
4 4     4   21 use strict;
  4         7  
  4         76  
5 4     4   17 use warnings;
  4         7  
  4         99  
6 4     4   26 use feature "say";
  4         7  
  4         664  
7              
8              
9              
10             our $VERSION = 'v0.3.0';
11              
12 4     4   33 use constant DEBUG=>undef;
  4         9  
  4         579  
13 4         27 use enum ("PACKAGE=0",qw
14             HASARGS WANTARRAY EVALTEXT IS_REQUIRE HINTS BITMASK
15 4     4   2109 HINT_HASH MESSAGE SEQUENCE CODE_LINES>);
  4         4723  
16              
17              
18              
19             #
20             # A list of top level file paths or scalar refs to check for syntax errors
21             #
22             my @IINC;
23             sub context;
24              
25            
26             sub import {
27 4     4   35 my $package=shift;
28 4         16 my @caller=caller;
29 4         23 my @options=@_;
30              
31              
32             # Only have one sub to export and we only export it if the caller has a line
33             # number. Otherise we are being invoked from the CLI
34             #
35 4 50       15 if($caller[LINE]){
36 4     4   5106 no strict "refs";
  4         9  
  4         13541  
37 4         11 my $name=$caller[0]."::context";
38 4         8 *{$name}=\&{"context"};
  4         18  
  4         10  
39 4         3354 return;
40             }
41              
42             #
43             # CLI Options include
44             #
45              
46 0         0 require POSIX; #For _exit;
47 0         0 require IPC::Open3;
48 0         0 require Symbol;
49 0         0 my %options;
50              
51 0         0 my $clean=grep /clean/i, @options;
52 0         0 my $splain=grep /splain/i, @options;
53 0         0 my $do_warn=grep /warn/i, @options;
54              
55 0 0       0 my @warn=$do_warn?():"-MError::Show::Internal";
56              
57              
58             #
59             # 1. Command line argument activation ie -MError::Show
60             #
61             # Find out any extra lib paths used. To do this we:
62             #
63             # a. fork/exec a new perl process using the value of $^X.
64             # b. The new process dumps the @INC array to STDOUT
65             # c. This process reads the output and stores in @IINC
66             #
67             # Only run it the first time its used
68             # Is this the best way? Not sure. At least this way there is no argument
69             # processing, perl process does it for us.
70             #
71            
72 0 0       0 @IINC=map {chomp; $_} do {
  0         0  
  0         0  
73 0 0       0 open my $fh, "-|", $^X . q| -E 'map print("$_\n"), @INC'| or die "$!";
74 0         0 <$fh>;
75             } unless @IINC;
76              
77             #
78             # 2. Extract the extra include paths
79             #
80             # Built up the 'extra' array of any include paths not already listed
81             # from the STDOUT dumping above
82             #
83 0         0 my @extra=map {("-I", $_)} grep {my $i=$_; !grep { $i eq $_} @IINC} @INC;
  0         0  
  0         0  
  0         0  
  0         0  
84              
85              
86              
87             #
88             # 3. Syntax checking the program
89             #
90             # Now we have the include paths sorted,
91             # a. fork/exec again, this time with the -c switch for perl to check syntax
92             # b. slurp STDERR from child process
93             # c. execute the context routine to parse and show more source code context
94             # d. print!
95             # The proc
96              
97 0         0 local $/=undef;
98 0         0 my $file=$0;
99              
100             #push @file, @ARGV;
101              
102             #my $runnable=not $^C;#$options{check};
103             #for my $file(@file){
104 0 0 0     0 die "Error::Show cannot process STDIN, -e and -E programs" if $file eq "-e" or $file eq "-E" or $file eq "-";
      0        
105 0 0       0 die "Error::Show cannot access \"$file\"" unless -f $file;
106 0         0 my @cmd= ($^X ,@warn, @extra, "-c", $file);
107              
108 0         0 my $pid;
109             my $result;
110 0         0 eval {
111 0         0 $pid=IPC::Open3::open3(my $chld_in, my $chld_out, my $chld_err = Symbol::gensym(), @cmd);
112 0         0 $result=<$chld_err>;
113 0         0 close $chld_in;
114 0         0 close $chld_out;
115 0         0 close $chld_err;
116 0         0 wait;
117             };
118 0 0 0     0 if(!$pid and $@){
119 0         0 die "Error::Show failed to syntax check";
120             }
121              
122              
123             #
124             # 4. Status code from child indicates success
125             # When 0 this means syntax was ok. Otherwise error
126             # Attempt to propogate code to exit status
127             #
128 0 0       0 my $code=$?>255? (0xFF & ~$?): $?;
129              
130 0         0 my $runnable=$?==0;
131              
132 0         0 my $status=context(splain=>$splain, clean=>$clean, error=>$result )."\n";
133              
134 0 0       0 if($^C){
135 0 0       0 if($runnable){
136             #only print status if we want warnings
137 0 0       0 print STDERR $do_warn?$status: "$file syntax OK\n";
138              
139             }
140             else{
141             #Not runnable, thus syntax error. Always print
142 0         0 print STDERR $status;
143              
144             }
145 0         0 POSIX::_exit $code;
146              
147             }
148             else{
149             #not checking, we want to run
150 0 0       0 if($runnable){
151             # don't bother with warnings
152              
153             }
154             else{
155             #Not runnable, thus syntax error. Always print
156 0         0 print STDERR $status;
157 0         0 POSIX::_exit $code;
158             }
159             }
160             }
161              
162              
163             sub process_string_error{
164 13     13 0 24 my $error=pop;
165 13         58 my %opts=@_;
166              
167 13         33 my @error_lines;
168             my @errors;
169             #my @entry;
170 13         0 my %entry;
171 13 50       24 if(defined $error){
172             #local $_=$error;
173             #Substitue with a line number relative to the start marker
174             #Reported line numbers are 1 based, stored lines are 0 based
175             #my $translation=$opts{translation};
176             #my $start=$opts{start};
177            
178 13         16 my $i=0;
179 13         42 for(split "\n", $error){
180 10         19 DEBUG and say STDERR "ERROR LINE: ".$_;
181 10 50 33     84 if(/at (.*?) line (\d+)/
182             or /Missing right curly or square bracket at (.*?) (\d+) at end of line/){
183             #
184             # Group by file names
185             #
186 10         15 DEBUG and say STDERR "PROCESSING: ".$_;
187 10         11 DEBUG and say STDERR "file: $1 and line $2";
188 10   50     67 my $entry=$entry{$1}//=[];
189             #push @$entry, {file=>$1, line=>$2,message=>$_, sequence=>$i++};
190 10         24 my $a=[];
191 10         43 $a->[FILENAME]=$1;
192 10         32 $a->[LINE]=$2-1;
193 10         19 $a->[MESSAGE]=$_;
194 10 50       22 $a->[MESSAGE]=$opts{message} if $opts{message};
195 10         19 $a->[SEQUENCE]=$i++;
196 10 100       21 $a->[EVALTEXT]=$opts{program} if $opts{program};
197 10         24 push @$entry, $a;
198             }
199             }
200              
201            
202             }
203             else {
204             #Assume a target line
205             #push @error_lines, $opts{line}-1;
206             }
207              
208             #Key is file name
209             # value is a hash of filename,line number, perl error string and the sequence number
210              
211 13         48 \%entry;
212              
213             }
214              
215             # Takes a hash ref error sources
216              
217             sub text_output {
218 24     24 0 45 my $info_ref=pop;
219 24         95 my %opts=@_;
220 24         37 my $total="";
221              
222 24         28 DEBUG and say STDERR "Reverse flag in text output set to: $opts{reverse}";
223              
224             # Sort by sequence number
225             # Errors are stored by filename internally. Sort by sequence number.
226             #
227              
228             my @sorted_info=
229 3         15 sort {$a->[SEQUENCE] <=> $b->[SEQUENCE] }
230 24         63 map { $_->@* } values %$info_ref;
  21         82  
231              
232             # Reverse the order if we want the first error listed last
233             #
234 24 100       80 @sorted_info=reverse (@sorted_info) if $opts{reverse};
235              
236             # Process each of the errors in sequence
237 24         34 my $counter=0;
238 24   50     70 my $limit=$opts{limit}//100;
239 24         40 for my $info (@sorted_info){
240 21 50 33     53 last if $counter>=$limit and $limit >0;
241 21         31 $counter++;
242 21 50       47 unless(exists $info->[CODE_LINES]){
243 21         25 my @code;
244            
245 21 100       39 if($info->[EVALTEXT]){
246 4         42 @code=split "\n", $info->[EVALTEXT];
247             }
248             else {
249 17         24 @code=split "\n", do {
250 17 50       645 open my $fh, "<", $info->[FILENAME] or warn "Could not open file for reading: $info->[FILENAME]";
251 17         102 local $/=undef;
252 17         782 <$fh>;
253             };
254             }
255 21         76 $info->[CODE_LINES]=\@code;
256             }
257              
258             # At this point we have lines of code in an array
259             #
260            
261             #Find start mark and end mark
262             #
263 21         35 my $start_line=0;
264 21 100       55 if($opts{start_mark}){
265 2         5 my $counter=0;
266 2         3 my $start_mark=$opts{start_mark};
267 2         6 for($info->[CODE_LINES]->@*){
268 8 100       40 if(/$start_mark/){
269 2         4 $start_line+=$counter+1;
270 2         4 last;
271             }
272 6         10 $counter++;
273             }
274             # Don't include the start marker in the results
275             }
276              
277 21         50 my $end_line=$info->[CODE_LINES]->@*-1;
278              
279 21 100       57 if($opts{end_mark}){
280 2         4 my $counter=0;
281 2         5 my $end_mark=$opts{end_mark};
282 2         4 for (reverse($info->[CODE_LINES]->@*)){
283 8 100       33 if(/$end_mark/){
284 2         3 $end_line-=$counter;
285 2         3 last;
286             }
287 6         10 $counter++;
288             }
289             }
290              
291 21 50       45 $start_line+=$opts{start_offset} if $opts{start_offset};
292 21 50       42 $end_line-=$opts{end_offset } if $opts{end_offset};
293              
294             # preclamp the error line to within this range so that 'Unmatched ' errors
295             # at least show ssomething.
296             #
297 21 50       43 $info->[LINE]=$end_line if $info->[LINE]>$end_line;
298              
299 21         26 DEBUG and say "START LINE after offset: $start_line";
300 21         27 DEBUG and say "END LINE after offset: $end_line";
301             # At this point the file min and max lines we should consider are
302             # start_line and end line inclusive. The $start_line is also used as an
303             # offset to shift error sources
304             #
305              
306 21         40 my $min=$info->[LINE]-$opts{pre_lines};
307 21         36 my $max=$info->[LINE]+$opts{post_lines};
308              
309 21         37 my $target= $info->[LINE];#-$start_line;
310 21         27 DEBUG and say "TARGET: $target";
311              
312 21 100       59 $min=$min<$start_line ? $start_line: $min;
313              
314 21 100       39 $max=$max>$end_line?$end_line:$max;
315              
316             #
317             # format counter on the largest number to be expected
318             #
319 21         41 my $f_len=length("$max");
320              
321 21         57 my $out="$opts{indent}$info->[FILENAME]\n";
322            
323 21   50     48 my $indent=$opts{indent}//"";
324 21         46 my $format="$indent%${f_len}d% 2s %s\n";
325 21         34 my $mark="";
326              
327             #Change min and max to one based index
328             #$min++;
329             #$max--;
330 21         29 DEBUG and say STDERR "min before print $min";
331 21         23 DEBUG and say STDERR "max before print $max";
332 21         60 for my $l($min..$max){
333 201         280 $mark="";
334              
335 201         246 my $a=$l-$start_line+1;
336              
337             #Perl line number is 1 based
338 201 100       330 $mark="=>" if $l==$target;
339              
340              
341             # Print lines as per the index in file array
342 201         496 $out.=sprintf $format, $a, $mark, $info->[CODE_LINES][$l];
343             }
344              
345 21         49 $total.=$out;
346            
347             # Modifiy the message now with updated line numbers
348             # TODO: Tidy this up
349 21 50       101 $info->[MESSAGE]=~s/line (\d+)(?:\.|,)/(($1-1)>$max?$max:$1-1)-$start_line+1/e;
  10         68  
350              
351 21 50       83 $total.=$info->[MESSAGE]."\n" unless $opts{clean};
352              
353             }
354 24 50       57 if($opts{splain}){
355 0         0 $total=splain($total);
356             }
357 24         90 $total;
358             }
359              
360              
361             #Take an error string and attempt to contextualize it
362             # context options_pairs, error string
363             sub _context{
364             #use feature ":all";
365 24     24   37 DEBUG and say STDERR "IN context call";
366             #my ($package, $file, $caller_line)=caller;
367             #
368             # Error is set by single argument, key/value pair, or if no
369             # argument $@ is used
370             #
371 24         62 my %opts=@_;
372              
373 24         47 my $error= $opts{error};
374              
375              
376              
377             #$opts{start_mark};#//=qr|.*|; #regex which matches the start of the code
378 24   50     96 $opts{pre_lines}//=5; #Number of lines to show before target line
379 24   50     90 $opts{post_lines}//=5; #Number of lines to show after target line
380 24   50     94 $opts{start_offset}//=0; #Offset past start mark to consider as min line
381 24   50     85 $opts{end_offset}//=0; #Offset before end to consider as max line
382 24   50     97 $opts{translation}//=0; #A static value added to the line numbering
383 24   100     76 $opts{indent}//="";
384 24   50     91 $opts{file}//="";
385              
386             # Get the all the info we need to process
387 24         29 my $info_ref;
388 24 100 66     83 if(defined($error) and ref($error) eq ""){
389             #A string error. A normal string die/warn or compile time errors/warnings
390 13         39 $info_ref=process_string_error %opts, $error;
391             }
392             else{
393             #Some kind of object, converted into line and file hash
394 11         50 $info_ref= {$error->[FILENAME]=>[$error]};# {$error->{file}=>[$error]};
395 11   100     38 $error->[MESSAGE]=$opts{message}//""; #Store the message
396 11 50       27 $error->[EVALTEXT]=$opts{program} if $opts{program};
397             }
398            
399             # Override text/file to search
400 24         37 my $output;
401 24         75 $output=text_output %opts, $info_ref;
402            
403             #TODO:
404             #
405 24         125 $output;
406            
407             }
408              
409              
410             #
411             # Front end to the main processing sub. Configures and checks the inputs
412             #
413             my $msg= "Trace must be a ref to array of {file=>.., line=>..} pairs";
414             sub context{
415 17 50 66 17 1 7430 shift if(defined $_[0] and $_[0] eq __PACKAGE__);
416 17         39 my %opts;
417             my $out;
418 17 100       56 if(@_==0){
    100          
419 3         10 $opts{error}=$@;
420             }
421             elsif(@_==1){
422 4         11 $opts{error}=shift;
423             }
424             else {
425 10         47 %opts=@_;
426             }
427 17 100       43 if($opts{frames}){
428 3         9 $opts{error}=delete $opts{frames};
429             }
430              
431             # For the special case of error undefined, we assume we want to dump the current location/context
432             #
433 17 100       39 unless(defined $opts{error}){
434 1         7 my $i=0;
435              
436             #build call frames
437 1         2 my @frame;
438             my @stack;
439              
440 1         13 while(@frame=caller($i++)){
441 1         7 push @stack, [@frame];
442             }
443 1         3 $opts{error}=\@stack;
444             }
445            
446             # Convert from supported exceptions classes to internal format
447              
448 17         33 my $ref=ref $opts{error};
449 17         26 my $dstf="Devel::StackTrace::Frame";
450              
451 17         90 require Scalar::Util;
452 17 50 50     168 if((Scalar::Util::blessed($opts{error})//"") eq $dstf){
    50 66        
    100 33        
    50 0        
    0          
453             # Single DSTF stack frame. Convert to an array
454 0         0 $opts{error}=[$opts{error}];
455             }
456             elsif($ref eq "ARRAY" and ref($opts{error}[0]) eq ""){
457             # Array of scalars - a normal stack frame - wrap it
458 0         0 $opts{error}=[[$opts{error}->@*]];
459             }
460             elsif($ref eq ""){
461             # Not a reference - A string error
462             }
463             elsif($ref eq "ARRAY" and ref($opts{error}[0]) eq "ARRAY"){
464             # Array of arrays of scalars
465 4         16 $opts{error}=[map { [$_->@*] } $opts{error}->@* ];
  11         50  
466            
467             }
468             elsif($ref eq "ARRAY" and Scalar::Util::blessed($opts{error}[0]) eq $dstf){
469             #Array of DSTF object
470             }
471             else {
472             # Force stringification of error as a last ditch attempt
473 0         0 $opts{error}="$opts{error}";
474             }
475            
476 17         26 DEBUG and say STDERR "Reverse flag set to: $opts{reverse}";
477              
478             # Reverse the ordering of errors here if requested
479             #
480 17 100       54 $opts{error}->@*=reverse $opts{error}->@* if $opts{reverse};
481             # Check for trace kv pair. If this is present. We ignore the error
482             #
483 17 100 66     59 if(ref($opts{error}) eq "ARRAY" and ref $opts{error}[0]){
484             # Iterate through the list
485 4   50     18 my $_indent=$opts{indent}//=" ";
486 4         7 my $current_indent="";
487              
488 4         23 my %_opts=%opts;
489 4         9 my $i=0; #Sequence number
490 4         10 for my $e ($opts{error}->@*) {
491              
492 11 50 50     63 if((Scalar::Util::blessed($e)//"") eq "Devel::StackTrace::Frame"){
493             #Convert to an array
494 0         0 my @a;
495 0         0 $a[PACKAGE]=$e->package;
496 0         0 $a[FILENAME]=$e->filename;
497 0         0 $a[LINE]=$e->line;
498 0         0 $a[SUBROUTINE]=$e->subroutine;
499 0         0 $a[HASARGS]=$e->hasargs;
500 0         0 $a[WANTARRAY]=$e->wantarray;
501 0         0 $a[EVALTEXT]=$e->evaltext;
502 0         0 $a[IS_REQUIRE]=$e->is_require;
503 0         0 $a[HINTS]=$e->hints;
504 0         0 $a[BITMASK]=$e->bitmask;
505 0         0 $a[HINT_HASH]=$e->hints;
506 0         0 $e=\@a;
507             }
508              
509              
510 11 50 33     66 if($e->[FILENAME] and $e->[LINE]){
511 11   50     60 $e->[MESSAGE]//="";
512              
513             #Force a message if one is provided
514 11         18 $e->[LINE]--; #Make the error 0 based
515 11 100       25 $e->[MESSAGE]=$opts{message} if $opts{message};
516 11         20 $e->[SEQUENCE]=$i++;
517            
518             # Generate the context here
519             #
520 11         23 $_opts{indent}=$current_indent;
521 11         22 $_opts{error}=$e;
522 11         54 $out.=_context %_opts;
523 11         36 $current_indent.=$_indent;
524             }
525             else{
526 0         0 die $msg;
527             }
528             }
529              
530             }
531             else {
532 13         40 $out=_context %opts;
533             }
534 17         100 $out;
535             }
536              
537              
538              
539             my ($chld_in, $chld_out, $chld_err);
540             my @cmd="splain";
541             my $pid;
542              
543             sub splain {
544 0     0 1   my $out;
545             #Attempt to open splain process if it isn't already
546 0 0         unless($pid){
547 0           eval{
548 0           $pid= IPC::Open3::open3($chld_in, $chld_out, $chld_err = Symbol::gensym(), @cmd);
549             #$chld_in->autoflush(1);
550              
551             };
552 0 0 0       if(!$pid and $@){
553 0           warn "Error::Show Could not splain the results";
554             }
555             };
556              
557             #Attempt to write to the process and read from it
558 0           eval {
559 0           print $chld_in $_[0], "\n";;
560 0           close $chld_in;
561 0           $out=<$chld_out>;
562 0           close $chld_out;
563 0           close $chld_err;
564             };
565              
566 0 0         if($@){
567 0           $pid=undef;
568 0           close $chld_in;
569 0           close $chld_out;
570 0           close $chld_err;
571 0           warn "Error::Show Could not splain the results";
572             }
573 0           $out;
574             }
575              
576             #sub wrap_eval{
577             # my $program=shift;
578             # "sub { $program }";
579             #}
580              
581             1;
582             __END__