This "fork" of llvm-project adds a proof-of-concept clang-tidy checker
that converts occurrences of printf
and fprintf
to fmt::print
(as
provided by the {fmt} library) and modifies the format string
appropriately. In other words, it turns:
fprintf(stderr, "The %s is %3d\n", answer, value);
into:
fmt::print(stderr, "The {} is {:3}\n", answer, value);
It doesn't do a bad job, but it's not perfect. In particular:
-
It assumes that the input is mostly sane. If you get any warnings when compiling with
-Wformat
then misbehaviour is possible. -
At the point that the check runs, the AST contains a single
StringLiteral
for the format string and any macro expansion, token pasting, adjacent string literal concatenation and escaping has been handled. Although it's possible for the check to automatically put the escapes back, they may not be exactly as they were written (e.g."\x0a"
will become"\n"
and"ab" "cd"
will become"abcd"
.) It turns out that it's probably quite important that macro expansion and adjacent string literal concatenation happen before we parse the format string in order to cope with the<inttypes.h>
PRI macros. -
It tries to support field widths, precision, positional arguments, leading zeros, leading +, alignment and alternative forms.
-
It is assumed that the
fmt/format.h
header has already been included. No attempt is made to include it. -
Use of any unsupported flags or specifiers will cause the entire statement to be left alone. Known unsupported features are:
-
The
%
flag for thousands separators. It looks like this could be translated to{:L}
, but I'm not sure it will do exactly the same thing. -
The glibc extension
%m
. This could be supported relatively easily if we can assume thatstrerror
is thread safe (which the glibc version is.)
-
-
It has some tests in
clang-tools-extra/test/clang-tidy/checkers/fmt-printf-convert.cpp
but they probably don't cover the full set of possibilities. -
It copes with calls to
printf
,::printf
andstd::printf
. Unfortunately this means that it also changesmine::printf
which is probably incorrect. My attempts to fix this usingisInStdNamespace()
have failed. -
This is my first attempt at a clang-tidy checker, so it's probably full of things that aren't done the idiomatic LLVM way.
Build clang-tidy following the upstream instructions. Install it if you wish, or just run from the build directory with something like:
bin/clang-tidy -checks='-*,fmt-printf-convert' --fix input.cpp
There are no clang-tidy checks for fmt yet, so I've added
clangTidyFmtModule
. The FormatStringConverter
class makes use of
Clang's own ParsePrintfString
to walk the format string deciding what to
do. If the format string can be converted then PrintfConvertCheck
simply
needs to replace printf
or fprintf
with fmt::print
, and tell
FormatStringConverter
to apply the necessary fixes. The applied fixes are:
printf
/fprintf
becomesfmt::print
- rewrite the format string to use the {fmt} format language
- wrap any arguments that corresponded to
%p
specifiers that {fmt} won't deal with in a call tofmt::ptr
.
Maybe. In addition to the fmt-printf-convert
check, there are two other
checks that are unlikely to be useful as they are, but they may be
modifiable to do what you want: fmt-strprintf-convert
and
fmt-trace-convert
.
The fmt-strprintf-convert
check converts calls to a commonly-implemented
sprintf
wrapper function strprintf(const char *format, ...)
that is
expected to return std::string
to the equivalent fmt::format
call. For
example, it turns:
const std::string s = strprintf("%d:%s", 42, "hello");
into:
const std::string s = fmt::format("{}:{}", 42, "hello");
The fmt-trace-convert
check converts calls to operator()(const char *format, ...)
on an object that derives from a particular class (in this
case, the class is BaseTrace
. For example, it converts:
class DerivedTrace : public BaseTrace {};
BaseTrace TRACE;
DerivedTrace TRACE2;
TRACE("%s=%d\n", name, value);
TRACE2("%s\n", name);
into:
class DerivedTrace : public BaseTrace {};
BaseTrace TRACE;
DerivedTrace TRACE2;
TRACE("{}={}\n", name, value);
TRACE2("{}\n", name);
(It is assumed that the implementation of BaseTrace
will be modified at
the same time to expect the new form of format string.)
Once the calls have been converted to use fmt::format
or fmt::print
,
the range of types that can be passed is greatly increased. In particular,
any std::string
parameters that were being converted to C-style strings
by calling std::string::c_str()
would be better passed as is. In fact, it
may even be more efficient to do so in some circumstances since it won't be
necessary to walk the string to determine its length. This tree also
contains improvements to the readability-redundant-string-cstr
check to
detect such arguments to fmt::print
and fmt::format
.
Once you've built everything, run something like:
bin/llvm-lit -v ../clang-tools-extra/test/clang-tidy/checkers/fmt/printf-convert.cpp
bin/llvm-lit -v ../clang-tools-extra/test/clang-tidy/checkers/fmt/strprintf-convert.cpp
bin/llvm-lit -v ../clang-tools-extra/test/clang-tidy/checkers/fmt/trace-convert.cpp
bin/llvm-lit -v ../clang-tools-extra/test/clang-tidy/checkers/readability/redundant-string-cstr.cpp
The {fmt} library has gradually been standardised. fmt::format
is in
C++20 as std::format
and fmt::print
is in C++23 as
std::print
. It would not be hard to adapt these checks to convert to
the standard versions instead.