Search code examples
cgccclang

How to make compiler warning about the enum type mismatch in switch and case?


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.


Solution

  • 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.