File Coverage

blib/lib/Error/Show.pm
Criterion Covered Total %
statement 180 258 69.7
branch 55 104 52.8
condition 27 66 40.9
subroutine 17 18 94.4
pod 2 4 50.0
total 281 450 62.4


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