99# Input arguments are pathnames of shell scripts containing test definitions,
1010# or globs referencing a collection of scripts. For each problem discovered,
1111# the pathname of the script containing the test is printed along with the test
12- # name and the test body with a `?!FOO ?!` annotation at the location of each
13- # detected problem, where "FOO " is a tag such as "AMP" which indicates a broken
14- # &&-chain. Returns zero if no problems are discovered, otherwise non-zero.
12+ # name and the test body with a `?!LINT: ... ?!` annotation at the location of
13+ # each detected problem, where "... " is an explanation of the problem. Returns
14+ # zero if no problems are discovered, otherwise non-zero.
1515
1616use warnings;
1717use strict;
@@ -181,7 +181,7 @@ sub swallow_heredocs {
181181 $self -> {lineno } += () = $body =~ / \n /sg ;
182182 next ;
183183 }
184- push (@{$self -> {parser }-> {problems }}, [' UNCLOSED- HEREDOC' , $tag ]);
184+ push (@{$self -> {parser }-> {problems }}, [' HEREDOC' , $tag ]);
185185 $$b =~ / (?:\G |\n ).*\z /gc ; # consume rest of input
186186 my $body = substr ($$b , $start , pos ($$b ) - $start );
187187 $self -> {lineno } += () = $body =~ / \n /sg ;
@@ -238,6 +238,7 @@ sub new {
238238 stop => [],
239239 output => [],
240240 heredocs => {},
241+ insubshell => 0,
241242 } => $class ;
242243 $self -> {lexer } = Lexer-> new($self , $s );
243244 return $self ;
@@ -296,8 +297,11 @@ sub parse_group {
296297
297298sub parse_subshell {
298299 my $self = shift @_ ;
299- return ($self -> parse(qr / ^\) $ / ),
300- $self -> expect(' )' ));
300+ $self -> {insubshell }++;
301+ my @tokens = ($self -> parse(qr / ^\) $ / ),
302+ $self -> expect(' )' ));
303+ $self -> {insubshell }--;
304+ return @tokens ;
301305}
302306
303307sub parse_case_pattern {
@@ -528,7 +532,7 @@ sub parse_loop_body {
528532 return @tokens if ends_with(\@tokens , [qr / ^\|\| $ / , " \n " , qr / ^echo$ / , qr / ^.+$ / ]);
529533 # flag missing "return/exit" handling explicit failure in loop body
530534 my $n = find_non_nl(\@tokens );
531- push (@{$self -> {problems }}, [' LOOP ' , $tokens [$n ]]);
535+ push (@{$self -> {problems }}, [$self -> { insubshell } ? ' LOOPEXIT ' : ' LOOPRETURN ' , $tokens [$n ]]);
532536 return @tokens ;
533537}
534538
@@ -587,6 +591,7 @@ sub new {
587591 my $class = shift @_ ;
588592 my $self = $class -> SUPER::new(@_ );
589593 $self -> {ntests } = 0;
594+ $self -> {nerrs } = 0;
590595 return $self ;
591596}
592597
@@ -619,6 +624,15 @@ sub unwrap {
619624 return $s
620625}
621626
627+ sub format_problem {
628+ local $_ = shift ;
629+ / ^AMP$ / && return " missing '&&'" ;
630+ / ^LOOPRETURN$ / && return " missing '|| return 1'" ;
631+ / ^LOOPEXIT$ / && return " missing '|| exit 1'" ;
632+ / ^HEREDOC$ / && return ' unclosed heredoc' ;
633+ die (" unrecognized problem type '$_ '\n " );
634+ }
635+
622636sub check_test {
623637 my $self = shift @_ ;
624638 my $title = unwrap(shift @_ );
@@ -634,22 +648,26 @@ sub check_test {
634648 my $parser = TestParser-> new(\$body );
635649 my @tokens = $parser -> parse();
636650 my $problems = $parser -> {problems };
651+ $self -> {nerrs } += @$problems ;
637652 return unless $emit_all || @$problems ;
638653 my $c = main::fd_colors(1);
654+ my ($erropen , $errclose ) = -t 1 ? (" $c ->{rev}$c ->{red}" , $c -> {reset }) : (' ?!' , ' ?!' );
639655 my $start = 0;
640656 my $checked = ' ' ;
641657 for (sort {$a -> [1]-> [2] <=> $b -> [1]-> [2]} @$problems ) {
642658 my ($label , $token ) = @$_ ;
643659 my $pos = $token -> [2];
644- $checked .= substr ($body , $start , $pos - $start ) . " ?!$label ?! " ;
660+ my $err = format_problem($label );
661+ $checked .= substr ($body , $start , $pos - $start );
662+ $checked .= ' ' unless $checked =~ / \s $ / ;
663+ $checked .= " ${erropen} LINT: $err$errclose " ;
664+ $checked .= ' ' unless $pos >= length ($body ) ||
665+ substr ($body , $pos , 1) =~ / ^\s / ;
645666 $start = $pos ;
646667 }
647668 $checked .= substr ($body , $start );
648669 $checked =~ s / ^/ $lineno ++ . ' '/ mge ;
649670 $checked =~ s / ^\d + \n // ;
650- $checked =~ s / (\s ) \? !/ $1 ?!/ mg ;
651- $checked =~ s /\? ! (\s )/ ?!$1 / mg ;
652- $checked =~ s / (\? ![^?]+\? !)/ $c ->{rev}$c ->{red}$1 $c ->{reset}/ mg ;
653671 $checked =~ s / ^\d +/ $c ->{dim}$& $c ->{reset}/ mg ;
654672 $checked .= " \n " unless $checked =~ / \n $ / ;
655673 push (@{$self -> {output }}, " $c ->{blue}# chainlint: $title$c ->{reset}\n $checked " );
@@ -791,9 +809,9 @@ sub check_script {
791809 my $c = fd_colors(1);
792810 my $s = join (' ' , @{$parser -> {output }});
793811 $emit -> (" $c ->{bold}$c ->{blue}# chainlint: $path$c ->{reset}\n " . $s );
794- $nerrs += () = $s =~ / \? ![^?]+\? !/g ;
795812 }
796813 $ntests += $parser -> {ntests };
814+ $nerrs += $parser -> {nerrs };
797815 }
798816 return [$id , $nscripts , $ntests , $nerrs ];
799817}
0 commit comments