skip to Main Content

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.

2

Answers


  1. Any suggestion on how to detect the potential bugs like this?

    You can’t, at least not in standard C.

    enums are a very weak abstraction in C; they’re a way to introduce named integer constants and not much more. enums don’t create their own namespaces, there aren’t any constraints against assigning a value of one enum type to a variable of another enum type, etc. Enumeration constants belong to the "all other identifiers" namespace, so two different enumeration types can’t use the same enumeration constants.

    Do not expect C enumeration types to have the same type safety as enumeration types in Java or C#.

    Login or Signup to reply.
  2. As others have noted, there does not appear to be any
    warning built in to GCC or Clang to do this. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search