Skip to content

Commit af79372

Browse files
authored
[clang-tidy] Add modernize-use-std-format check (#90397)
Add a new clang-tidy check that converts absl::StrFormat (and similar functions) to std::format (and similar functions.) Split the configuration of FormatStringConverter out to a separate Configuration class so that we don't risk confusion by passing two boolean configuration parameters into the constructor. Add AllowTrailingNewlineRemoval option since we never want to remove trailing newlines in this check.
1 parent 6993798 commit af79372

File tree

13 files changed

+473
-8
lines changed

13 files changed

+473
-8
lines changed

clang-tools-extra/clang-tidy/modernize/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_clang_library(clangTidyModernizeModule
4141
UseNullptrCheck.cpp
4242
UseOverrideCheck.cpp
4343
UseStartsEndsWithCheck.cpp
44+
UseStdFormatCheck.cpp
4445
UseStdNumbersCheck.cpp
4546
UseStdPrintCheck.cpp
4647
UseTrailingReturnTypeCheck.cpp

clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "UseNullptrCheck.h"
4343
#include "UseOverrideCheck.h"
4444
#include "UseStartsEndsWithCheck.h"
45+
#include "UseStdFormatCheck.h"
4546
#include "UseStdNumbersCheck.h"
4647
#include "UseStdPrintCheck.h"
4748
#include "UseTrailingReturnTypeCheck.h"
@@ -76,6 +77,7 @@ class ModernizeModule : public ClangTidyModule {
7677
"modernize-use-designated-initializers");
7778
CheckFactories.registerCheck<UseStartsEndsWithCheck>(
7879
"modernize-use-starts-ends-with");
80+
CheckFactories.registerCheck<UseStdFormatCheck>("modernize-use-std-format");
7981
CheckFactories.registerCheck<UseStdNumbersCheck>(
8082
"modernize-use-std-numbers");
8183
CheckFactories.registerCheck<UseStdPrintCheck>("modernize-use-std-print");
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//===--- UseStdFormatCheck.cpp - clang-tidy -------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "UseStdFormatCheck.h"
10+
#include "../utils/FormatStringConverter.h"
11+
#include "../utils/Matchers.h"
12+
#include "../utils/OptionsUtils.h"
13+
#include "clang/ASTMatchers/ASTMatchFinder.h"
14+
#include "clang/Lex/Lexer.h"
15+
#include "clang/Tooling/FixIt.h"
16+
17+
using namespace clang::ast_matchers;
18+
19+
namespace clang::tidy::modernize {
20+
21+
namespace {
22+
AST_MATCHER(StringLiteral, isOrdinary) { return Node.isOrdinary(); }
23+
} // namespace
24+
25+
UseStdFormatCheck::UseStdFormatCheck(StringRef Name, ClangTidyContext *Context)
26+
: ClangTidyCheck(Name, Context),
27+
StrictMode(Options.getLocalOrGlobal("StrictMode", false)),
28+
StrFormatLikeFunctions(utils::options::parseStringList(
29+
Options.get("StrFormatLikeFunctions", ""))),
30+
ReplacementFormatFunction(
31+
Options.get("ReplacementFormatFunction", "std::format")),
32+
IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
33+
utils::IncludeSorter::IS_LLVM),
34+
areDiagsSelfContained()),
35+
MaybeHeaderToInclude(Options.get("FormatHeader")) {
36+
if (StrFormatLikeFunctions.empty())
37+
StrFormatLikeFunctions.push_back("absl::StrFormat");
38+
39+
if (!MaybeHeaderToInclude && ReplacementFormatFunction == "std::format")
40+
MaybeHeaderToInclude = "<format>";
41+
}
42+
43+
void UseStdFormatCheck::registerPPCallbacks(const SourceManager &SM,
44+
Preprocessor *PP,
45+
Preprocessor *ModuleExpanderPP) {
46+
IncludeInserter.registerPreprocessor(PP);
47+
}
48+
49+
void UseStdFormatCheck::registerMatchers(MatchFinder *Finder) {
50+
Finder->addMatcher(
51+
callExpr(argumentCountAtLeast(1),
52+
hasArgument(0, stringLiteral(isOrdinary())),
53+
callee(functionDecl(unless(cxxMethodDecl()),
54+
matchers::matchesAnyListedName(
55+
StrFormatLikeFunctions))
56+
.bind("func_decl")))
57+
.bind("strformat"),
58+
this);
59+
}
60+
61+
void UseStdFormatCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
62+
using utils::options::serializeStringList;
63+
Options.store(Opts, "StrictMode", StrictMode);
64+
Options.store(Opts, "StrFormatLikeFunctions",
65+
serializeStringList(StrFormatLikeFunctions));
66+
Options.store(Opts, "ReplacementFormatFunction", ReplacementFormatFunction);
67+
Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
68+
if (MaybeHeaderToInclude)
69+
Options.store(Opts, "FormatHeader", *MaybeHeaderToInclude);
70+
}
71+
72+
void UseStdFormatCheck::check(const MatchFinder::MatchResult &Result) {
73+
const unsigned FormatArgOffset = 0;
74+
const auto *OldFunction = Result.Nodes.getNodeAs<FunctionDecl>("func_decl");
75+
const auto *StrFormat = Result.Nodes.getNodeAs<CallExpr>("strformat");
76+
77+
utils::FormatStringConverter::Configuration ConverterConfig;
78+
ConverterConfig.StrictMode = StrictMode;
79+
utils::FormatStringConverter Converter(Result.Context, StrFormat,
80+
FormatArgOffset, ConverterConfig,
81+
getLangOpts());
82+
const Expr *StrFormatCall = StrFormat->getCallee();
83+
if (!Converter.canApply()) {
84+
diag(StrFormat->getBeginLoc(),
85+
"unable to use '%0' instead of %1 because %2")
86+
<< StrFormatCall->getSourceRange() << ReplacementFormatFunction
87+
<< OldFunction->getIdentifier()
88+
<< Converter.conversionNotPossibleReason();
89+
return;
90+
}
91+
92+
DiagnosticBuilder Diag =
93+
diag(StrFormatCall->getBeginLoc(), "use '%0' instead of %1")
94+
<< ReplacementFormatFunction << OldFunction->getIdentifier();
95+
Diag << FixItHint::CreateReplacement(
96+
CharSourceRange::getTokenRange(StrFormatCall->getSourceRange()),
97+
ReplacementFormatFunction);
98+
Converter.applyFixes(Diag, *Result.SourceManager);
99+
100+
if (MaybeHeaderToInclude)
101+
Diag << IncludeInserter.createIncludeInsertion(
102+
Result.Context->getSourceManager().getFileID(
103+
StrFormatCall->getBeginLoc()),
104+
*MaybeHeaderToInclude);
105+
}
106+
107+
} // namespace clang::tidy::modernize
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//===--- UseStdFormatCheck.h - clang-tidy -----------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTDFORMATCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTDFORMATCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
#include "../utils/IncludeInserter.h"
14+
15+
namespace clang::tidy::modernize {
16+
17+
/// Converts calls to absl::StrFormat, or other functions via configuration
18+
/// options, to C++20's std::format, or another function via a configuration
19+
/// option, modifying the format string appropriately and removing
20+
/// now-unnecessary calls to std::string::c_str() and std::string::data().
21+
///
22+
/// For the user-facing documentation see:
23+
/// http://clang.llvm.org/extra/clang-tidy/checks/modernize/use-std-format.html
24+
class UseStdFormatCheck : public ClangTidyCheck {
25+
public:
26+
UseStdFormatCheck(StringRef Name, ClangTidyContext *Context);
27+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
28+
if (ReplacementFormatFunction == "std::format")
29+
return LangOpts.CPlusPlus20;
30+
return LangOpts.CPlusPlus;
31+
}
32+
void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
33+
Preprocessor *ModuleExpanderPP) override;
34+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
35+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
36+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
37+
std::optional<TraversalKind> getCheckTraversalKind() const override {
38+
return TK_IgnoreUnlessSpelledInSource;
39+
}
40+
41+
private:
42+
bool StrictMode;
43+
std::vector<StringRef> StrFormatLikeFunctions;
44+
StringRef ReplacementFormatFunction;
45+
utils::IncludeInserter IncludeInserter;
46+
std::optional<StringRef> MaybeHeaderToInclude;
47+
};
48+
49+
} // namespace clang::tidy::modernize
50+
51+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTDFORMATCHECK_H

clang-tools-extra/clang-tidy/modernize/UseStdPrintCheck.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,11 @@ void UseStdPrintCheck::check(const MatchFinder::MatchResult &Result) {
129129
FormatArgOffset = 1;
130130
}
131131

132+
utils::FormatStringConverter::Configuration ConverterConfig;
133+
ConverterConfig.StrictMode = StrictMode;
134+
ConverterConfig.AllowTrailingNewlineRemoval = true;
132135
utils::FormatStringConverter Converter(
133-
Result.Context, Printf, FormatArgOffset, StrictMode, getLangOpts());
136+
Result.Context, Printf, FormatArgOffset, ConverterConfig, getLangOpts());
134137
const Expr *PrintfCall = Printf->getCallee();
135138
const StringRef ReplacementFunction = Converter.usePrintNewlineFunction()
136139
? ReplacementPrintlnFunction

clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,11 @@ static bool castMismatchedIntegerTypes(const CallExpr *Call, bool StrictMode) {
198198
FormatStringConverter::FormatStringConverter(ASTContext *ContextIn,
199199
const CallExpr *Call,
200200
unsigned FormatArgOffset,
201-
bool StrictMode,
201+
const Configuration ConfigIn,
202202
const LangOptions &LO)
203-
: Context(ContextIn),
204-
CastMismatchedIntegerTypes(castMismatchedIntegerTypes(Call, StrictMode)),
203+
: Context(ContextIn), Config(ConfigIn),
204+
CastMismatchedIntegerTypes(
205+
castMismatchedIntegerTypes(Call, ConfigIn.StrictMode)),
205206
Args(Call->getArgs()), NumArgs(Call->getNumArgs()),
206207
ArgsOffset(FormatArgOffset + 1), LangOpts(LO) {
207208
assert(ArgsOffset <= NumArgs);
@@ -627,9 +628,12 @@ void FormatStringConverter::finalizeFormatText() {
627628

628629
// It's clearer to convert printf("Hello\r\n"); to std::print("Hello\r\n")
629630
// than to std::println("Hello\r");
630-
if (StringRef(StandardFormatString).ends_with("\\n") &&
631-
!StringRef(StandardFormatString).ends_with("\\\\n") &&
632-
!StringRef(StandardFormatString).ends_with("\\r\\n")) {
631+
// Use StringRef until C++20 std::string::ends_with() is available.
632+
const auto StandardFormatStringRef = StringRef(StandardFormatString);
633+
if (Config.AllowTrailingNewlineRemoval &&
634+
StandardFormatStringRef.ends_with("\\n") &&
635+
!StandardFormatStringRef.ends_with("\\\\n") &&
636+
!StandardFormatStringRef.ends_with("\\r\\n")) {
633637
UsePrintNewlineFunction = true;
634638
FormatStringNeededRewriting = true;
635639
StandardFormatString.erase(StandardFormatString.end() - 2,

clang-tools-extra/clang-tidy/utils/FormatStringConverter.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,14 @@ class FormatStringConverter
3232
public:
3333
using ConversionSpecifier = clang::analyze_format_string::ConversionSpecifier;
3434
using PrintfSpecifier = analyze_printf::PrintfSpecifier;
35+
36+
struct Configuration {
37+
bool StrictMode = false;
38+
bool AllowTrailingNewlineRemoval = false;
39+
};
40+
3541
FormatStringConverter(ASTContext *Context, const CallExpr *Call,
36-
unsigned FormatArgOffset, bool StrictMode,
42+
unsigned FormatArgOffset, Configuration Config,
3743
const LangOptions &LO);
3844

3945
bool canApply() const { return ConversionNotPossibleReason.empty(); }
@@ -45,6 +51,7 @@ class FormatStringConverter
4551

4652
private:
4753
ASTContext *Context;
54+
const Configuration Config;
4855
const bool CastMismatchedIntegerTypes;
4956
const Expr *const *Args;
5057
const unsigned NumArgs;

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ New checks
150150
Finds initializer lists for aggregate types that could be
151151
written as designated initializers instead.
152152

153+
- New :doc:`modernize-use-std-format
154+
<clang-tidy/checks/modernize/use-std-format>` check.
155+
156+
Converts calls to ``absl::StrFormat``, or other functions via
157+
configuration options, to C++20's ``std::format``, or another function
158+
via a configuration option, modifying the format string appropriately and
159+
removing now-unnecessary calls to ``std::string::c_str()`` and
160+
``std::string::data()``.
161+
153162
- New :doc:`readability-enum-initial-value
154163
<clang-tidy/checks/readability/enum-initial-value>` check.
155164

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ Clang-Tidy Checks
300300
:doc:`modernize-use-nullptr <modernize/use-nullptr>`, "Yes"
301301
:doc:`modernize-use-override <modernize/use-override>`, "Yes"
302302
:doc:`modernize-use-starts-ends-with <modernize/use-starts-ends-with>`, "Yes"
303+
:doc:`modernize-use-std-format <modernize/use-std-format>`, "Yes"
303304
:doc:`modernize-use-std-numbers <modernize/use-std-numbers>`, "Yes"
304305
:doc:`modernize-use-std-print <modernize/use-std-print>`, "Yes"
305306
:doc:`modernize-use-trailing-return-type <modernize/use-trailing-return-type>`, "Yes"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
.. title:: clang-tidy - modernize-use-std-format
2+
3+
modernize-use-std-format
4+
========================
5+
6+
Converts calls to ``absl::StrFormat``, or other functions via
7+
configuration options, to C++20's ``std::format``, or another function
8+
via a configuration option, modifying the format string appropriately and
9+
removing now-unnecessary calls to ``std::string::c_str()`` and
10+
``std::string::data()``.
11+
12+
For example, it turns lines like
13+
14+
.. code-block:: c++
15+
16+
return absl::StrFormat("The %s is %3d", description.c_str(), value);
17+
18+
into:
19+
20+
.. code-block:: c++
21+
22+
return std::format("The {} is {:3}", description, value);
23+
24+
The check uses the same format-string-conversion algorithm as
25+
`modernize-use-std-print <../modernize/use-std-print.html>`_ and its
26+
shortcomings are described in the documentation for that check.
27+
28+
Options
29+
-------
30+
31+
.. option:: StrictMode
32+
33+
When `true`, the check will add casts when converting from variadic
34+
functions and printing signed or unsigned integer types (including
35+
fixed-width integer types from ``<cstdint>``, ``ptrdiff_t``, ``size_t``
36+
and ``ssize_t``) as the opposite signedness to ensure that the output
37+
would matches that of a simple wrapper for ``std::sprintf`` that
38+
accepted a C-style variable argument list. For example, with
39+
`StrictMode` enabled,
40+
41+
.. code-block:: c++
42+
43+
extern std::string strprintf(const char *format, ...);
44+
int i = -42;
45+
unsigned int u = 0xffffffff;
46+
return strprintf("%d %u\n", i, u);
47+
48+
would be converted to
49+
50+
.. code-block:: c++
51+
52+
return std::format("{} {}\n", static_cast<unsigned int>(i), static_cast<int>(u));
53+
54+
to ensure that the output will continue to be the unsigned representation
55+
of -42 and the signed representation of 0xffffffff (often 4294967254
56+
and -1 respectively). When `false` (which is the default), these casts
57+
will not be added which may cause a change in the output. Note that this
58+
option makes no difference for the default value of
59+
`StrFormatLikeFunctions` since ``absl::StrFormat`` takes a function
60+
parameter pack and is not a variadic function.
61+
62+
.. option:: StrFormatLikeFunctions
63+
64+
A semicolon-separated list of (fully qualified) function names to
65+
replace, with the requirement that the first parameter contains the
66+
printf-style format string and the arguments to be formatted follow
67+
immediately afterwards. The default value for this option is
68+
`absl::StrFormat`.
69+
70+
.. option:: ReplacementFormatFunction
71+
72+
The function that will be used to replace the function set by the
73+
`StrFormatLikeFunctions` option rather than the default
74+
`std::format`. It is expected that the function provides an interface
75+
that is compatible with ``std::format``. A suitable candidate would be
76+
`fmt::format`.
77+
78+
.. option:: FormatHeader
79+
80+
The header that must be included for the declaration of
81+
`ReplacementFormatFunction` so that a ``#include`` directive can be added if
82+
required. If `ReplacementFormatFunction` is `std::format` then this option will
83+
default to ``<format>``, otherwise this option will default to nothing
84+
and no ``#include`` directive will be added.

0 commit comments

Comments
 (0)