For the following code, I hope there is a warning because a
is type enum A
, but the case B1
and B2
are type enum B
; but I couldn't find a way to make gcc/clang warn about it.
Any suggestion on how to detect the potential bugs like this?
Thanks
enum A { A1, A2 };
enum B { B1, B2 };
int foo(enum A a) {
switch(a) {
case B1:
return 1;
case B2:
return 2;
default:
return 3;
}
}
$ clang -Wall -Wextra -Wpedantic -Werror -c enum3.c; echo $?
0
$ gcc -Wall -Wextra -Wpedantic -Werror -c enum3.c; echo $?
0
$ clang --version
Ubuntu clang version 14.0.0-1ubuntu1.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
As others have noted, there does not appear to be any
warning built in to GCC or Clang to do this when compiling as C, although Kevin points out in a comment that -Wenum-compare-switch
, which is enabled by default, will warn about this in C++ mode. There is also no
clang-tidy
check for it.
However, it is possible to write a check condition for this using
clang-query
.
The following check will report any switch
whose condition expression
has enumeration type, and it has a case
that has a case constant that
has a different enumeration type:
#!/bin/sh
PATH=$HOME/opt/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04/bin:$PATH
# In this query, the comments are stripped by a 'grep' command further
# below. (clang-query also ignores comments, but its mechanism is
# a little buggy.)
query='m
# Look for a "switch" statement
switchStmt(
# where its condition expression
hasCondition(
# after skipping past any implicit casts
ignoringImpCasts(
# is an expression
expr(
# whose type
hasType(
# after stripping typedefs, etc.,
hasUnqualifiedDesugaredType(
# is an enumeration
enumType(
# whose declaration
hasDeclaration(
# is an enumeration declaration. Bind that
# declaration to the name "switch-enum".
enumDecl().bind("switch-enum")
)
)
)
)
)
)
),
# Furthermore, having found a relevant "switch", examine each of its
# "case" and "default" statements and report any that
forEachSwitchCase(
# is a "case" statement
caseStmt(
# whose constant expression
hasCaseConstant(
# has any descendant
hasDescendant(
# that is a reference to a declaration
declRefExpr(
# where that declaration
hasDeclaration(
# is an enumerator declaration
enumConstantDecl(
# whose parent
hasParent(
# is an enumeration declaration
enumDecl(
# unless
unless(
# that enumeration is the same one that we found
# when matching the "switch" condition.
equalsBoundNode("switch-enum")
)
)
)
).bind("case-enumerator")
)
)
)
)
).bind("case-stmt")
)
).bind("switch")
'
# Strip the comments.
query=$(echo "$query" | egrep -v '^ +#')
if [ "x$1" = "x" ]; then
echo "usage: $0 filename.c -- <compile options like -I, etc.>"
exit 2
fi
# Run the query. Setting 'bind-root' to false means clang-query will
# not also print a redundant "root" binding where I have bound "switch".
clang-query \
-c="set bind-root false" \
-c="$query" \
"$@"
# EOF
When run on this input:
// test.c
// Testing mismatched switch(enum).
enum A { A1, A2 };
enum B { B1, B2 };
int f(enum A a)
{
switch (a) {
case B1: // reported
return 1;
case B2: // reported
return 2;
default:
return 3;
}
}
int f2(enum A a)
{
switch (a) {
case 1: // not reported
return 1;
case 3: // not reported (but clang -Wswitch warns)
return 3;
case A1: // not reported: correct enumeration
return B2; // not reported: not in the 'case' constant expr
default:
return B2; // not reported: not in a 'case'
}
}
typedef enum A AAlias;
int f3(AAlias a)
{
// Make sure we do not get confused by a typedef.
switch (a) {
case B1: // reported
return 1;
default:
return 0;
}
}
// EOF
it produces this output:
$ ./cmd.sh test.c --
$(PWD)/test.c:25:10: warning: case value not in enumerated type 'enum A' [-Wswitch]
case 3: // not reported (but clang -Wswitch warns)
^
Match #1:
$(PWD)/test.c:6:14: note: "case-enumerator" binds here
enum B { B1, B2 };
^~
$(PWD)/test.c:13:5: note: "case-stmt" binds here
case B2: // reported
^~~~~~~~~~~~~~~~~~~~~~~~~~
$(PWD)/test.c:10:3: note: "switch" binds here
switch (a) {
^~~~~~~~~~~~
$(PWD)/test.c:4:1: note: "switch-enum" binds here
enum A { A1, A2 };
^~~~~~~~~~~~~~~~~
Match #2:
$(PWD)/test.c:6:10: note: "case-enumerator" binds here
enum B { B1, B2 };
^~
$(PWD)/test.c:11:5: note: "case-stmt" binds here
case B1: // reported
^~~~~~~~~~~~~~~~~~~~~~~~~~
$(PWD)/test.c:10:3: note: "switch" binds here
switch (a) {
^~~~~~~~~~~~
$(PWD)/test.c:4:1: note: "switch-enum" binds here
enum A { A1, A2 };
^~~~~~~~~~~~~~~~~
Match #3:
$(PWD)/test.c:6:10: note: "case-enumerator" binds here
enum B { B1, B2 };
^~
$(PWD)/test.c:40:5: note: "case-stmt" binds here
case B1: // reported
^~~~~~~~~~~~~~~~~~~~~~~~~~
$(PWD)/test.c:39:3: note: "switch" binds here
switch (a) {
^~~~~~~~~~~~
$(PWD)/test.c:4:1: note: "switch-enum" binds here
enum A { A1, A2 };
^~~~~~~~~~~~~~~~~
3 matches.
For more details on what the elements of the query do, see the Clang AST Matcher Reference. It's pretty terse though, so trial and error is required to make use of it.
FWIW, I ran this over a couple large-ish C++ translation units I had at hand and it didn't report anything. So while I haven't really done any "tuning" of the query it appears to not explode with noise.
Of course, adding a custom clang-query
command to your build is much
more work than just adding a compiler warning option, but it's perhaps
something to experiment with at least.