How can I get a warning when comparing unsigned integers of different sizes in C and C++?

There does not appear to be a warning option built in to gcc or
clang that does what is requested. However, we can use
clang-query
instead.

Below is a clang-query command that will report comparison of
32-bit and 64-bit integers, on the assumption that int is 32 bits and
long is 64 bits. (More about that below.)

#!/bin/sh

PATH=$HOME/opt/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04/bin:$PATH

# In this query, the comments are ignored because clang-query (not the
# shell) recognizes and discards them.
query='m
  binaryOperator(                            # Find a binary operator expression
    anyOf(                                   #  such that any of:
      hasOperatorName("<"),                  #   is operator <, or
      hasOperatorName("<="),                 #   is operator <=, or
      hasOperatorName(">"),                  #   is operator >, or
      hasOperatorName(">="),                 #   is operator >=, or
      hasOperatorName("=="),                 #   is operator ==, or
      hasOperatorName("!=")                  #   is operator !=;
    ),

    hasEitherOperand(                        #  and where either operand
      implicitCastExpr(                      #   is an implicit cast
        has(                                 #    from
          expr(                              #     an expression
            hasType(                         #      whose type
              hasCanonicalType(              #       after resolving typedefs
                anyOf(                       #        is either
                  asString("int"),           #         int or
                  asString("unsigned int")   #         unsigned int,
                )
              )
            ),
            unless(                          #      unless that expression
              integerLiteral()               #       is an integer literal,
            )
          )
        ),
        hasImplicitDestinationType(          #    and to a type
          hasCanonicalType(                  #     that after typedefs
            anyOf(                           #      is either
              asString("long"),              #       long or
              asString("unsigned long")      #       unsigned long.
            )
          )
        )
      ).bind("operand")
    )
  )
'

# Run the query on test.c.
clang-query \
  -c="set bind-root false" \
  -c="$query" \
  test.c -- -w

# EOF

When run on the following test.c it reports all of the indicated cases:

// test.c
// Demonstrate reporting comparisons of different-size operands.

#include <stddef.h>          // size_t
#include <stdint.h>          // int32_t, etc.

void test(int32_t i32, int64_t i64, uint32_t u32, uint64_t u64)
{
  i32 < i32;                 // Not reported: same sizes.
  i32 < i64;                 // reported
  i64 < i64;

  u32 < u32;
  u32 < u64;                 // reported
  u64 < u64;

  i32 < u64;                 // reported
  u32 < i64;                 // reported

  i32 <= i64;                // reported

  i64 > i32;                 // reported
  i64 >= i32;                // reported

  i32 == i64;                // reported
  u64 != u32;                // reported

  i32 + i64;                 // Not reported: not a comparison operator.

  ((int64_t)i32) < i64;      // Not reported: explicit cast.

  u64 < 3;                   // Not reported: comparison with integer literal.

  // Example #1 in question.
  size_t n = 0;
  for (unsigned int i = 0; i < n; i++) {}        // reported
}

// Example #2 in question.
void f(uint64_t n)
{
  for (uint32_t i = 0; i < n; ++i) {             // reported
  }
}

// EOF

Some details about the clang-query command:

  • The command passes -w to clang-query to suppress other warnings.
    That’s just because I wrote the test in a way that provokes warnings
    about unused values, and is not necessary with normal code.

  • It passes set bind-root false so the only reported site is the
    operand of interest rather than also reporting the entire expression.

  • Unfortunately it is not possible to have the query also print the
    names of the types involved. Attempting to do so with a binding
    causes clang-query to complain, “Matcher does not support binding.”

The unsatisfying aspect of the query is it explicitly lists the source
and destination types. Unfortunately, clang-query does not have a
matcher to, say, report any 32-bit type, so they have to be listed
individually. You might want to add [unsigned] long long on the
destination side. You might also need to remove [unsigned] long if running this
code with compiler options that target an IL32 platform like Windows.

Relatedly, note that clang-query accepts compiler options after
the --, or alternatively in a
compile_commands.json
file.
Unfortunately there isn’t dedicated documentation of the clang-query
command line, and even its --help does not mention the -- command
line option. The best I can link is the
documentation for libtooling,
as clang-query uses that library internally for command line
processing.

Finally, I’ll note that I haven’t done any “tuning” of this query on
real code. It is likely to produce a lot of noise, and will need
further tweaking. For a tutorial on how to work with clang-query,
I recommend the blog post
Exploring Clang Tooling Part 2: Examining the Clang AST with clang-query
by Stephen Kelly. There is also the
AST Matcher Reference,
but the documentation there is quite terse.

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)