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>
This commit is contained in:
Eric Sunshine 2022-09-01 00:29:50 +00:00 committed by Junio C Hamano
parent 832c68b3c2
commit fd4094c3ca
12 changed files with 153 additions and 7 deletions

View File

@ -482,6 +482,17 @@ sub match_ending {
return undef; return undef;
} }
sub parse_loop_body {
my $self = shift @_;
my @tokens = $self->SUPER::parse_loop_body(@_);
# did loop signal failure via "|| return" or "|| exit"?
return @tokens if !@tokens || grep(/^(?:return|exit|\$\?)$/, @tokens);
# flag missing "return/exit" handling explicit failure in loop body
my $n = find_non_nl(\@tokens);
splice(@tokens, $n + 1, 0, '?!LOOP?!');
return @tokens;
}
my @safe_endings = ( my @safe_endings = (
[qr/^(?:&&|\|\||\||&)$/], [qr/^(?:&&|\|\||\||&)$/],
[qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/], [qr/^(?:exit|return)$/, qr/^(?:\d+|\$\?)$/],

View File

@ -4,6 +4,6 @@
: :
else else
echo >file echo >file
fi fi ?!LOOP?!
done) && done) &&
test ! -f file test ! -f file

View File

@ -2,10 +2,10 @@
for i in a b c for i in a b c
do do
echo $i ?!AMP?! echo $i ?!AMP?!
cat <<-EOF cat <<-EOF ?!LOOP?!
done ?!AMP?! done ?!AMP?!
for i in a b c; do for i in a b c; do
echo $i && echo $i &&
cat $i cat $i ?!LOOP?!
done done
) )

View File

@ -0,0 +1,15 @@
git init r1 &&
for n in 1 2 3 4 5
do
echo "This is file: $n" > r1/file.$n &&
git -C r1 add file.$n &&
git -C r1 commit -m "$n" || return 1
done &&
git init r2 &&
for n in 1000 10000
do
printf "%"$n"s" X > r2/large.$n &&
git -C r2 add large.$n &&
git -C r2 commit -m "$n" ?!LOOP?!
done

View File

@ -0,0 +1,17 @@
git init r1 &&
# LINT: loop handles failure explicitly with "|| return 1"
for n in 1 2 3 4 5
do
echo "This is file: $n" > r1/file.$n &&
git -C r1 add file.$n &&
git -C r1 commit -m "$n" || return 1
done &&
git init r2 &&
# LINT: loop fails to handle failure explicitly with "|| return 1"
for n in 1000 10000
do
printf "%"$n"s" X > r2/large.$n &&
git -C r2 add large.$n &&
git -C r2 commit -m "$n"
done

View File

@ -0,0 +1,18 @@
( while test $i -le $blobcount
do
printf "Generating blob $i/$blobcount\r" >& 2 &&
printf "blob\nmark :$i\ndata $blobsize\n" &&
printf "%-${blobsize}s" $i &&
echo "M 100644 :$i $i" >> commit &&
i=$(($i+1)) ||
echo $? > exit-status
done &&
echo "commit refs/heads/main" &&
echo "author A U Thor <author@email.com> 123456789 +0000" &&
echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
echo "data 5" &&
echo ">2gb" &&
cat commit ) |
git fast-import --big-file-threshold=2 &&
test ! -f exit-status

View File

@ -0,0 +1,19 @@
# LINT: "$?" handled explicitly within loop body
(while test $i -le $blobcount
do
printf "Generating blob $i/$blobcount\r" >&2 &&
printf "blob\nmark :$i\ndata $blobsize\n" &&
#test-tool genrandom $i $blobsize &&
printf "%-${blobsize}s" $i &&
echo "M 100644 :$i $i" >> commit &&
i=$(($i+1)) ||
echo $? > exit-status
done &&
echo "commit refs/heads/main" &&
echo "author A U Thor <author@email.com> 123456789 +0000" &&
echo "committer C O Mitter <committer@email.com> 123456789 +0000" &&
echo "data 5" &&
echo ">2gb" &&
cat commit) |
git fast-import --big-file-threshold=2 &&
test ! -f exit-status

View File

@ -4,7 +4,7 @@
while true while true
do do
echo "pop" ?!AMP?! echo "pop" ?!AMP?!
echo "glup" echo "glup" ?!LOOP?!
done ?!AMP?! done ?!AMP?!
foo foo
fi ?!AMP?! fi ?!AMP?!

View File

@ -0,0 +1,31 @@
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" ?!LOOP?!
done ?!LOOP?!
done &&
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" || return 1
done
done &&
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" ?!LOOP?!
done || return 1
done &&
for i in 0 1 2 3 4 5 6 7 8 9 ;
do
for j in 0 1 2 3 4 5 6 7 8 9 ;
do
echo "$i$j" > "path$i$j" || return 1
done || return 1
done

View File

@ -0,0 +1,35 @@
# LINT: neither loop handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j"
done
done &&
# LINT: inner loop handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j" || return 1
done
done &&
# LINT: outer loop handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j"
done || return 1
done &&
# LINT: inner & outer loops handles failure explicitly with "|| return 1"
for i in 0 1 2 3 4 5 6 7 8 9;
do
for j in 0 1 2 3 4 5 6 7 8 9;
do
echo "$i$j" >"path$i$j" || return 1
done || return 1
done

View File

@ -15,5 +15,5 @@
) && ) &&
(cd foo && (cd foo &&
for i in a b c; do for i in a b c; do
echo; echo; ?!LOOP?!
done) done)

View File

@ -2,10 +2,10 @@
while true while true
do do
echo foo ?!AMP?! echo foo ?!AMP?!
cat <<-EOF cat <<-EOF ?!LOOP?!
done ?!AMP?! done ?!AMP?!
while true; do while true; do
echo foo && echo foo &&
cat bar cat bar ?!LOOP?!
done done
) )