Title: [clang-tidy] RFC: Y2038 check for lossy time_t conversions
Hi,
This RFC proposes a new clang-tidy check for Y2038-related bugs. The goal is to warn when a value involving time_t is converted to a type that may not be able to represent it, for example storing or passing time_t through a narrower integer type, or through a type with incompatible signedness.
The motivation is to catch code that is valid C/C++, but may truncate or otherwise lose information when time_t is 64-bit. For example:
void f(void)
{
int i = (int)time(NULL);
int i2;
i2 = time(NULL);
struct S { int sec; };
struct S s;
s.sec = time(NULL);
time_t t = time(NULL);
uint32_t u32 = t; // warn
int32_t i32 = t; // warn
uint64_t u64 = t; // warn
int64_t i64 = t; // no warning
}
The check would produce diagnostics such as:
warning: conversion from 'time_t' to 'int' may lose information [portability-y2038-lossy]
int i = (int)time(NULL);
^
warning: conversion from 'time_t' to 'int' may lose information [portability-y2038-lossy]
i2 = time(NULL);
^
warning: conversion from 'time_t' to 'int' may lose information [portability-y2038-lossy]
s.i = time(NULL);
^
The check should also catch cases where the conversion happens as part of a larger expression, such as function arguments, return statements, conditions, and conditional expressions.
The current prototype works by matching cast expressions in the AST, instead of trying to handle assignments, variable declarations, function calls, etc. separately. After finding a cast, the check looks at whether the source expression involves time_t or something in its typedef chain, for example __time_t, and then checks whether the destination type may be narrower or otherwise lossy.
Very roughly, the matcher looks like this:
Finder->addMatcher(
traverse(TK_AsIs,
castExpr(unless(isExpansionInSystemHeader()),
anyOf(hasCastKind(CK_IntegralCast),
hasCastKind(CK_IntegralToBoolean)))
.bind("cast")),
this);
Most of the logic is then in the check callback: inspect the source expression, desugar typedefs where needed, and compare the source and destination types.
For fix-its, the intention is to keep them conservative. In straightforward local cases, the check can walk up the AST until it finds the related VarDecl and suggest changing a too-narrow destination type. For example:
- int i = (int)time(NULL);
+ time_t i = time(NULL);
- int i2;
+ time_t i2;
i2 = time(NULL);
- uint32_t u32 = t;
+ time_t u32 = t;
Fix-its would be avoided where the change affects an API boundary. For example, struct S { int sec; }; would not be changed by default just because of s.sec = time(NULL), since that changes the layout/API of the struct.
Feedback on the intended scope and diagnostic behavior would be appreciated. In particular: does matching cast expressions sound like the right approach here?
Thanks!