chainlint.pl: complain about loops lacking explicit failure handling

Shell `for` and `while` loops do not terminate automatically just because a command fails within the loop body. Instead, the loop continues to iterate and eventually returns the exit status of the final command of the final iteration, which may not be the command which failed, thus it is possible for failures to go undetected. Consequently, it is important for test authors to explicitly handle failure within the loop body by terminating the loop manually upon failure. This can be done by returning a non-zero exit code from within the loop body (i.e. `|| return 1`) or exiting (i.e. `|| exit 1`) if the loop is within a subshell, or by manually checking `$?` and taking some appropriate action. Therefore, add logic to detect and complain about loops which lack explicit `return` or `exit`, or `$?` check. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Eric Sunshine committed Sep 1, 2022 at 00:29 UTC fd4094c3cad7c62adb0b7080e0dca37f66bf0c6e
12 files changed +153 -7
t/chainlint.pl
+11
@@ -482,6 +482,17 @@ sub match_ending {
482 return undef;
483 }
484
485 +sub parse_loop_body {
486 + my $self = shift @_;
487 + my @tokens = $self->SUPER::parse_loop_body(@_);
488 + # did loop signal failure via "|| return" or "|| exit"?
489 + return @tokens if !@tokens || grep(/^(?:return|exit|\$\?)$/, @tokens);
490 + # flag missing "return/exit" handling explicit failure in loop body
491 + my $n = find_non_nl(\@tokens);
492 + splice(@tokens, $n + 1, 0, '?!LOOP?!');
493 + return @tokens;
494 +}
495 +
496 my @safe_endings = (
497 [qr/^(?:&&|\|\||\||&)$/],
498 [qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/],
t/chainlint/complex-if-in-cuddled-loop.expect
+1 -1
@@ -4,6 +4,6 @@
4 :
5 else
6 echo >file
7 - fi
7 + fi ?!LOOP?!
8 done) &&
9 test ! -f file
t/chainlint/for-loop.expect
+2 -2
@@ -2,10 +2,10 @@
2 for i in a b c
3 do
4 echo $i ?!AMP?!
5 - cat <<-EOF
5 + cat <<-EOF ?!LOOP?!
6 done ?!AMP?!
7 for i in a b c; do
8 echo $i &&
9 - cat $i
9 + cat $i ?!LOOP?!
10 done
11 )
t/chainlint/loop-detect-failure.expect new
+15
@@ -0,0 +1,15 @@
1 +git init r1 &&
2 +for n in 1 2 3 4 5
3 +do
4 + echo "This is file: $n" > r1/file.$n &&
5 + git -C r1 add file.$n &&
6 + git -C r1 commit -m "$n" || return 1
7 +done &&
8 +
9 +git init r2 &&
10 +for n in 1000 10000
11 +do
12 + printf "%"$n"s" X > r2/large.$n &&
13 + git -C r2 add large.$n &&
14 + git -C r2 commit -m "$n" ?!LOOP?!
15 +done
t/chainlint/loop-detect-failure.test new
+17
@@ -0,0 +1,17 @@
1 +git init r1 &&
2 +# LINT: loop handles failure explicitly with "|| return 1"
3 +for n in 1 2 3 4 5
4 +do
5 + echo "This is file: $n" > r1/file.$n &&
6 + git -C r1 add file.$n &&
7 + git -C r1 commit -m "$n" || return 1
8 +done &&
9 +
10 +git init r2 &&
11 +# LINT: loop fails to handle failure explicitly with "|| return 1"
12 +for n in 1000 10000
13 +do
14 + printf "%"$n"s" X > r2/large.$n &&
15 + git -C r2 add large.$n &&
16 + git -C r2 commit -m "$n"
17 +done
t/chainlint/loop-detect-status.expect new
+18
@@ -0,0 +1,18 @@
1 +( while test $i -le $blobcount
2 +do
3 + printf "Generating blob $i/$blobcount\r" >& 2 &&
4 + printf "blob\nmark :$i\ndata $blobsize\n" &&
5 +
6 + printf "%-${blobsize}s" $i &&
7 + echo "M 100644 :$i $i" >> commit &&
8 + i=$(($i+1)) ||
9 + echo $? > exit-status
10 +done &&
11 +echo "commit refs/heads/main" &&
12 +echo "author A U Thor <author@email.com> 123456789 +0000" &&
13 +echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
14 +echo "data 5" &&
15 +echo ">2gb" &&
16 +cat commit ) |
17 +git fast-import --big-file-threshold=2 &&
18 +test ! -f exit-status
t/chainlint/loop-detect-status.test new
+19
@@ -0,0 +1,19 @@
1 +# LINT: "$?" handled explicitly within loop body
2 +(while test $i -le $blobcount
3 + do
4 + printf "Generating blob $i/$blobcount\r" >&2 &&
5 + printf "blob\nmark :$i\ndata $blobsize\n" &&
6 + #test-tool genrandom $i $blobsize &&
7 + printf "%-${blobsize}s" $i &&
8 + echo "M 100644 :$i $i" >> commit &&
9 + i=$(($i+1)) ||
10 + echo $? > exit-status
11 + done &&
12 + echo "commit refs/heads/main" &&
13 + echo "author A U Thor <author@email.com> 123456789 +0000" &&
14 + echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
15 + echo "data 5" &&
16 + echo ">2gb" &&
17 + cat commit) |
18 +git fast-import --big-file-threshold=2 &&
19 +test ! -f exit-status
t/chainlint/loop-in-if.expect
+1 -1
@@ -4,7 +4,7 @@
4 while true
5 do
6 echo "pop" ?!AMP?!
7 - echo "glup"
7 + echo "glup" ?!LOOP?!
8 done ?!AMP?!
9 foo
10 fi ?!AMP?!
t/chainlint/nested-loop-detect-failure.expect new
+31
@@ -0,0 +1,31 @@
1 +for i in 0 1 2 3 4 5 6 7 8 9 ;
2 +do
3 + for j in 0 1 2 3 4 5 6 7 8 9 ;
4 + do
5 + echo "$i$j" > "path$i$j" ?!LOOP?!
6 + done ?!LOOP?!
7 +done &&
8 +
9 +for i in 0 1 2 3 4 5 6 7 8 9 ;
10 +do
11 + for j in 0 1 2 3 4 5 6 7 8 9 ;
12 + do
13 + echo "$i$j" > "path$i$j" || return 1
14 + done
15 +done &&
16 +
17 +for i in 0 1 2 3 4 5 6 7 8 9 ;
18 +do
19 + for j in 0 1 2 3 4 5 6 7 8 9 ;
20 + do
21 + echo "$i$j" > "path$i$j" ?!LOOP?!
22 + done || return 1
23 +done &&
24 +
25 +for i in 0 1 2 3 4 5 6 7 8 9 ;
26 +do
27 + for j in 0 1 2 3 4 5 6 7 8 9 ;
28 + do
29 + echo "$i$j" > "path$i$j" || return 1
30 + done || return 1
31 +done
t/chainlint/nested-loop-detect-failure.test new
+35
@@ -0,0 +1,35 @@
1 +# LINT: neither loop handles failure explicitly with "|| return 1"
2 +for i in 0 1 2 3 4 5 6 7 8 9;
3 +do
4 + for j in 0 1 2 3 4 5 6 7 8 9;
5 + do
6 + echo "$i$j" >"path$i$j"
7 + done
8 +done &&
9 +
10 +# LINT: inner loop handles failure explicitly with "|| return 1"
11 +for i in 0 1 2 3 4 5 6 7 8 9;
12 +do
13 + for j in 0 1 2 3 4 5 6 7 8 9;
14 + do
15 + echo "$i$j" >"path$i$j" || return 1
16 + done
17 +done &&
18 +
19 +# LINT: outer loop handles failure explicitly with "|| return 1"
20 +for i in 0 1 2 3 4 5 6 7 8 9;
21 +do
22 + for j in 0 1 2 3 4 5 6 7 8 9;
23 + do
24 + echo "$i$j" >"path$i$j"
25 + done || return 1
26 +done &&
27 +
28 +# LINT: inner & outer loops handles failure explicitly with "|| return 1"
29 +for i in 0 1 2 3 4 5 6 7 8 9;
30 +do
31 + for j in 0 1 2 3 4 5 6 7 8 9;
32 + do
33 + echo "$i$j" >"path$i$j" || return 1
34 + done || return 1
35 +done
t/chainlint/semicolon.expect
+1 -1
@@ -15,5 +15,5 @@
15 ) &&
16 (cd foo &&
17 for i in a b c; do
18 - echo;
18 + echo; ?!LOOP?!
19 done)
t/chainlint/while-loop.expect
+2 -2
@@ -2,10 +2,10 @@
2 while true
3 do
4 echo foo ?!AMP?!
5 - cat <<-EOF
5 + cat <<-EOF ?!LOOP?!
6 done ?!AMP?!
7 while true; do
8 echo foo &&
9 - cat bar
9 + cat bar ?!LOOP?!
10 done
11 )