File Coverage

blib/lib/Error/Show.pm
Criterion Covered Total %
statement 173 251 68.9
branch 51 102 50.0
condition 26 66 39.3
subroutine 17 18 94.4
pod 2 4 50.0
total 269 441 61.0


line stmt bran cond sub pod time code
1             package Error::Show;
2              
3 3     3   200948 use 5.024000;
  3         28  
4 3     3   17 use strict;
  3         4  
  3         59  
5 3     3   15 use warnings;
  3         6  
  3         71  
6 3     3   16 use feature "say";
  3         6  
  3         311  
7 3     3   20 use Carp;
  3         12  
  3         177  
8 3     3   1584 use POSIX; #For _exit;
  3         19528  
  3         17  
9 3     3   9859 use IPC::Open3;
  3         11845  
  3         170  
10 3     3   23 use Symbol 'gensym'; # vivify a separate handle for STDERR
  3         6  
  3         118  
11 3     3   20 use Scalar::Util qw;
  3         7  
  3         125  
12              
13             #use Exporter qw;
14 3     3   22 use base "Exporter";
  3         5  
  3         631  
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.0';
27 3     3   23 use constant DEBUG=>undef;
  3         6  
  3         197  
28 3         18 use enum ("PACKAGE=0",qw
29             HASARGS WANTARRAY EVALTEXT IS_REQUIRE HINTS BITMASK
30 3     3   1499 HINT_HASH MESSAGE SEQUENCE CODE_LINES>);
  3         3385  
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 3     3   29 my $package=shift;
47 3         12 my @caller=caller;
48 3         8 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 3 50       1782 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 22 my $error=pop;
176 13         47 my %opts=@_;
177              
178 13         34 my @error_lines;
179             my @errors;
180             #my @entry;
181 13         0 my %entry;
182 13 50       25 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         18 my $i=0;
190 13         61 for(split "\n", $error){
191 10         14 DEBUG and say STDERR "ERROR LINE: ".$_;
192 10 50 33     111 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         15 DEBUG and say STDERR "PROCESSING: ".$_;
198 10         14 DEBUG and say STDERR "file: $1 and line $2";
199 10   50     61 my $entry=$entry{$1}//=[];
200             #push @$entry, {file=>$1, line=>$2,message=>$_, sequence=>$i++};
201 10         20 my $a=[];
202 10         32 $a->[FILENAME]=$1;
203 10         30 $a->[LINE]=$2-1;
204 10         25 $a->[MESSAGE]=$_;
205 10 50       21 $a->[MESSAGE]=$opts{message} if $opts{message};
206 10         20 $a->[SEQUENCE]=$i++;
207 10 100       25 $a->[EVALTEXT]=$opts{program} if $opts{program};
208 10         25 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         55 \%entry;
223              
224             }
225              
226             # Takes a hash ref error sources
227              
228             sub text_output {
229 17     17 0 25 my $info_ref=pop;
230 17         53 my %opts=@_;
231 17         31 my $total="";
232              
233             # Sort by sequence number
234             # Errors are stored by filename internally. Sort by sequence number.
235             #
236              
237            
238             my @sorted_info=
239 3         11 sort { $a->[SEQUENCE] <=> $b->[SEQUENCE] }
240 17         49 map { $_->@* } values %$info_ref;
  14         48  
241              
242             # Reverse the order if we want the first error listed last
243             #
244 17 50       47 @sorted_info=reverse (@sorted_info)if $opts{reverse};
245              
246             # Process each of the errors in sequence
247 17         31 my $counter=0;
248 17   50     65 my $limit=$opts{limit}//100;
249 17         37 for my $info (@sorted_info){
250 14 50 33     55 last if $counter>=$limit and $limit >0;
251 14         21 $counter++;
252 14 50       34 unless(exists $info->[CODE_LINES]){
253 14         19 my @code;
254            
255 14 100       27 if($info->[EVALTEXT]){
256 4         27 @code=split "\n", $info->[EVALTEXT];
257             }
258             else {
259 10         15 @code=split "\n", do {
260 10 50       370 open my $fh, "<", $info->[FILENAME] or warn "Could not open file for reading: $info->[FILENAME]";
261 10         56 local $/=undef;
262 10         479 <$fh>;
263             };
264             }
265 14         51 $info->[CODE_LINES]=\@code;
266             }
267              
268             # At this point we have lines of code in an array
269             #
270            
271             #Find start mark and end mark
272             #
273 14         21 my $start_line=0;
274 14 100       38 if($opts{start_mark}){
275 2         5 my $counter=0;
276 2         6 my $start_mark=$opts{start_mark};
277 2         5 for($info->[CODE_LINES]->@*){
278 8 100       32 if(/$start_mark/){
279 2         8 $start_line+=$counter+1;
280 2         4 last;
281             }
282 6         19 $counter++;
283             }
284             # Don't include the start marker in the results
285             }
286              
287 14         29 my $end_line=$info->[CODE_LINES]->@*-1;
288              
289 14 100       35 if($opts{end_mark}){
290 2         4 my $counter=0;
291 2         3 my $end_mark=$opts{end_mark};
292 2         4 for (reverse($info->[CODE_LINES]->@*)){
293 8 100       26 if(/$end_mark/){
294 2         3 $end_line-=$counter;
295 2         13 last;
296             }
297 6         8 $counter++;
298             }
299             }
300              
301 14 50       33 $start_line+=$opts{start_offset} if $opts{start_offset};
302 14 50       26 $end_line-=$opts{end_offset } if $opts{end_offset};
303              
304             # preclamp the error line to within this range so that 'Unmatched ' errors
305             # at least show ssomething.
306             #
307 14 50       42 $info->[LINE]=$end_line if $info->[LINE]>$end_line;
308              
309 14         20 DEBUG and say "START LINE after offset: $start_line";
310 14         25 DEBUG and say "END LINE after offset: $end_line";
311             # At this point the file min and max lines we should consider are
312             # start_line and end line inclusive. The $start_line is also used as an
313             # offset to shift error sources
314             #
315              
316 14         27 my $min=$info->[LINE]-$opts{pre_lines};
317 14         26 my $max=$info->[LINE]+$opts{post_lines};
318              
319 14         21 my $target= $info->[LINE];#-$start_line;
320 14         16 DEBUG and say "TARGET: $target";
321              
322 14 100       38 $min=$min<$start_line ? $start_line: $min;
323              
324 14 100       24 $max=$max>$end_line?$end_line:$max;
325              
326             #
327             # format counter on the largest number to be expected
328             #
329 14         28 my $f_len=length("$max");
330              
331 14         40 my $out="$opts{indent}$info->[FILENAME]\n";
332            
333 14   50     32 my $indent=$opts{indent}//"";
334 14         31 my $format="$indent%${f_len}d% 2s %s\n";
335 14         25 my $mark="";
336              
337             #Change min and max to one based index
338             #$min++;
339             #$max--;
340 14         16 DEBUG and say "min before print $min";
341 14         17 DEBUG and say "max before print $max";
342 14         42 for my $l($min..$max){
343 131         171 $mark="";
344              
345 131         169 my $a=$l-$start_line+1;
346              
347             #Perl line number is 1 based
348 131 100       206 $mark="=>" if $l==$target;
349              
350              
351             # Print lines as per the index in file array
352 131         300 $out.=sprintf $format, $a, $mark, $info->[CODE_LINES][$l];
353             }
354              
355 14         33 $total.=$out;
356            
357             # Modifiy the message now with updated line numbers
358             # TODO: Tidy this up
359 14 50       90 $info->[MESSAGE]=~s/line (\d+)(?:\.|,)/(($1-1)>$max?$max:$1-1)-$start_line+1/e;
  10         56  
360              
361 14 50       61 $total.=$info->[MESSAGE]."\n" unless $opts{clean};
362              
363             }
364 17 50       38 if($opts{splain}){
365 0         0 $total=splain($total);
366             }
367 17         54 $total;
368             }
369              
370              
371             #Take an error string and attempt to contextualize it
372             # context options_pairs, error string
373             sub _context{
374             #use feature ":all";
375 17     17   24 DEBUG and say STDERR "IN context call";
376             #my ($package, $file, $caller_line)=caller;
377             #
378             # Error is set by single argument, key/value pair, or if no
379             # argument $@ is used
380             #
381 17         43 my %opts=@_;
382              
383 17         32 my $error= $opts{error};
384              
385              
386              
387              
388             #$opts{start_mark};#//=qr|.*|; #regex which matches the start of the code
389 17   50     72 $opts{pre_lines}//=5; #Number of lines to show before target line
390 17   50     75 $opts{post_lines}//=5; #Number of lines to show after target line
391 17   50     61 $opts{start_offset}//=0; #Offset past start mark to consider as min line
392 17   50     59 $opts{end_offset}//=0; #Offset before end to consider as max line
393 17   50     58 $opts{translation}//=0; #A static value added to the line numbering
394 17   100     71 $opts{indent}//="";
395 17   50     63 $opts{file}//="";
396              
397             # Get the all the info we need to process
398 17         21 my $info_ref;
399 17 100 66     59 if(defined($error) and ref($error) eq ""){
400             #A string error. A normal string die/warn or compile time errors/warnings
401 13         42 $info_ref=process_string_error %opts, $error;
402             #say "infor ref ".join ", ", $info_ref;
403             }
404             else{
405             #Some kind of object, converted into line and file hash
406 4         20 $info_ref= {$error->[FILENAME]=>[$error]};# {$error->{file}=>[$error]};
407 4   50     11 $error->[MESSAGE]=$opts{message}//""; #Store the message
408 4 50       12 $error->[EVALTEXT]=$opts{program} if $opts{program};
409             }
410            
411             # Override text/file to search
412 17         30 my $output;
413 17         54 $output=text_output %opts, $info_ref;
414            
415             #TODO:
416             #
417 17         96 $output;
418            
419             }
420              
421              
422             #
423             # Front end to the main processing sub. Configures and checks the inputs
424             #
425             my $msg= "Trace must be a ref to array of {file=>.., line=>..} pairs";
426             sub context{
427 14     14 1 6825 my %opts;
428             my $out;
429 14 100       56 if(@_==0){
    100          
430 3         9 $opts{error}=$@;
431             }
432             elsif(@_==1){
433 3         9 $opts{error}=shift;
434             }
435             else {
436 8         31 %opts=@_;
437             }
438              
439 14 100       41 if($opts{frames}){
440 1         5 $opts{error}=delete $opts{frames};
441             }
442            
443              
444             # Convert from supported exceptions classes to internal format
445              
446 14         27 my $ref=ref $opts{error};
447 14         22 my $dstf="Devel::StackTrace::Frame";
448              
449 14 50 50     124 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            
463             }
464             elsif($ref eq "ARRAY" and blessed($opts{error}[0]) eq $dstf){
465             #Array of DSTF object
466             }
467             else {
468             #warn "Expecting a string, caller() type array or a $dstf object, or arrays of these";
469 0         0 $opts{error}="$opts{error}";
470             }
471            
472              
473              
474             # Check for trace kv pair. If this is present. We ignore the error
475             #
476 14 100 66     48 if(ref($opts{error}) eq "ARRAY" and ref $opts{error}[0]){
477             # Iterate through the list
478 1   50     7 my $_indent=$opts{indent}//=" ";
479 1         2 my $current_indent="";
480              
481 1         6 my %_opts=%opts;
482 1         4 for my $e ($opts{error}->@*) {
483              
484 4 50 50     21 if((blessed($e)//"") eq "Devel::StackTrace::Frame"){
485             #Convert to an array
486 0         0 my @a;
487 0         0 $a[PACKAGE]=$e->package;
488 0         0 $a[FILENAME]=$e->filename;
489 0         0 $a[LINE]=$e->line;
490 0         0 $a[SUBROUTINE]=$e->subroutine;
491 0         0 $a[HASARGS]=$e->hasargs;
492 0         0 $a[WANTARRAY]=$e->wantarray;
493 0         0 $a[EVALTEXT]=$e->evaltext;
494 0         0 $a[IS_REQUIRE]=$e->is_require;
495 0         0 $a[HINTS]=$e->hints;
496 0         0 $a[BITMASK]=$e->bitmask;
497 0         0 $a[HINT_HASH]=$e->hints;
498 0         0 $e=\@a;
499             }
500 4 50 33     35 if($e->[FILENAME] and $e->[LINE]){
501 4   50     18 $e->[MESSAGE]//="";
502              
503             #Force a message if one is provided
504 4         6 $e->[LINE]--; #Make the error 0 based
505 4 50       11 $e->[MESSAGE]=$opts{message} if $opts{message};
506 4         8 $_opts{indent}=$current_indent;
507              
508 4         5 $_opts{error}=$e;
509 4         12 $out.=_context %_opts;
510              
511 4         12 $current_indent.=$_indent;
512             }
513             else{
514 0         0 die $msg;
515             }
516             }
517              
518             }
519             else {
520             #say "NOT AN ARRAY: ". join ", ", %opts;
521              
522 13         40 $out=_context %opts;
523             }
524 14         48 $out;
525             }
526              
527             my ($chld_in, $chld_out, $chld_err);
528             my @cmd="splain";
529             my $pid;
530             sub splain {
531 0     0 1   my $out;
532             #Attempt to open splain process if it isn't already
533 0 0         unless($pid){
534 0           eval{
535 0           $pid= open3($chld_in, $chld_out, $chld_err = gensym, @cmd);
536             #$chld_in->autoflush(1);
537              
538             };
539 0 0 0       if(!$pid and $@){
540 0           warn "Error::Show Could not splain the results";
541             }
542             };
543              
544             #Attempt to write to the process and read from it
545 0           eval {
546 0           print $chld_in $_[0], "\n";;
547 0           close $chld_in;
548 0           $out=<$chld_out>;
549 0           close $chld_out;
550 0           close $chld_err;
551             };
552              
553 0 0         if($@){
554 0           $pid=undef;
555 0           close $chld_in;
556 0           close $chld_out;
557 0           close $chld_err;
558 0           warn "Error::Show Could not splain the results";
559             }
560 0           $out;
561             }
562              
563             #sub wrap_eval{
564             # my $program=shift;
565             # "sub { $program }";
566             #}
567              
568             1;
569             __END__