Skip to content

Commit

Permalink
[clang-tidy] Add modernize-use-std-format check (llvm#90397)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mikecrowe authored May 13, 2024
1 parent 6993798 commit af79372
Show file tree
Hide file tree
Showing 13 changed files with 473 additions and 8 deletions.
1 change: 1 addition & 0 deletions clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ add_clang_library(clangTidyModernizeModule
UseNullptrCheck.cpp
UseOverrideCheck.cpp
UseStartsEndsWithCheck.cpp
UseStdFormatCheck.cpp
UseStdNumbersCheck.cpp
UseStdPrintCheck.cpp
UseTrailingReturnTypeCheck.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "UseNullptrCheck.h"
#include "UseOverrideCheck.h"
#include "UseStartsEndsWithCheck.h"
#include "UseStdFormatCheck.h"
#include "UseStdNumbersCheck.h"
#include "UseStdPrintCheck.h"
#include "UseTrailingReturnTypeCheck.h"
Expand Down Expand Up @@ -76,6 +77,7 @@ class ModernizeModule : public ClangTidyModule {
"modernize-use-designated-initializers");
CheckFactories.registerCheck<UseStartsEndsWithCheck>(
"modernize-use-starts-ends-with");
CheckFactories.registerCheck<UseStdFormatCheck>("modernize-use-std-format");
CheckFactories.registerCheck<UseStdNumbersCheck>(
"modernize-use-std-numbers");
CheckFactories.registerCheck<UseStdPrintCheck>("modernize-use-std-print");
Expand Down
107 changes: 107 additions & 0 deletions clang-tools-extra/clang-tidy/modernize/UseStdFormatCheck.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//===--- UseStdFormatCheck.cpp - clang-tidy -------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "UseStdFormatCheck.h"
#include "../utils/FormatStringConverter.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Lexer.h"
#include "clang/Tooling/FixIt.h"

using namespace clang::ast_matchers;

namespace clang::tidy::modernize {

namespace {
AST_MATCHER(StringLiteral, isOrdinary) { return Node.isOrdinary(); }
} // namespace

UseStdFormatCheck::UseStdFormatCheck(StringRef Name, ClangTidyContext *Context)
: ClangTidyCheck(Name, Context),
StrictMode(Options.getLocalOrGlobal("StrictMode", false)),
StrFormatLikeFunctions(utils::options::parseStringList(
Options.get("StrFormatLikeFunctions", ""))),
ReplacementFormatFunction(
Options.get("ReplacementFormatFunction", "std::format")),
IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
utils::IncludeSorter::IS_LLVM),
areDiagsSelfContained()),
MaybeHeaderToInclude(Options.get("FormatHeader")) {
if (StrFormatLikeFunctions.empty())
StrFormatLikeFunctions.push_back("absl::StrFormat");

if (!MaybeHeaderToInclude && ReplacementFormatFunction == "std::format")
MaybeHeaderToInclude = "<format>";
}

void UseStdFormatCheck::registerPPCallbacks(const SourceManager &SM,
Preprocessor *PP,
Preprocessor *ModuleExpanderPP) {
IncludeInserter.registerPreprocessor(PP);
}

void UseStdFormatCheck::registerMatchers(MatchFinder *Finder) {
Finder->addMatcher(
callExpr(argumentCountAtLeast(1),
hasArgument(0, stringLiteral(isOrdinary())),
callee(functionDecl(unless(cxxMethodDecl()),
matchers::matchesAnyListedName(
StrFormatLikeFunctions))
.bind("func_decl")))
.bind("strformat"),
this);
}

void UseStdFormatCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
using utils::options::serializeStringList;
Options.store(Opts, "StrictMode", StrictMode);
Options.store(Opts, "StrFormatLikeFunctions",
serializeStringList(StrFormatLikeFunctions));
Options.store(Opts, "ReplacementFormatFunction", ReplacementFormatFunction);
Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
if (MaybeHeaderToInclude)
Options.store(Opts, "FormatHeader", *MaybeHeaderToInclude);
}

void UseStdFormatCheck::check(const MatchFinder::MatchResult &Result) {
const unsigned FormatArgOffset = 0;
const auto *OldFunction = Result.Nodes.getNodeAs<FunctionDecl>("func_decl");
const auto *StrFormat = Result.Nodes.getNodeAs<CallExpr>("strformat");

utils::FormatStringConverter::Configuration ConverterConfig;
ConverterConfig.StrictMode = StrictMode;
utils::FormatStringConverter Converter(Result.Context, StrFormat,
FormatArgOffset, ConverterConfig,
getLangOpts());
const Expr *StrFormatCall = StrFormat->getCallee();
if (!Converter.canApply()) {
diag(StrFormat->getBeginLoc(),
"unable to use '%0' instead of %1 because %2")
<< StrFormatCall->getSourceRange() << ReplacementFormatFunction
<< OldFunction->getIdentifier()
<< Converter.conversionNotPossibleReason();
return;
}

DiagnosticBuilder Diag =
diag(StrFormatCall->getBeginLoc(), "use '%0' instead of %1")
<< ReplacementFormatFunction << OldFunction->getIdentifier();
Diag << FixItHint::CreateReplacement(
CharSourceRange::getTokenRange(StrFormatCall->getSourceRange()),
ReplacementFormatFunction);
Converter.applyFixes(Diag, *Result.SourceManager);

if (MaybeHeaderToInclude)
Diag << IncludeInserter.createIncludeInsertion(
Result.Context->getSourceManager().getFileID(
StrFormatCall->getBeginLoc()),
*MaybeHeaderToInclude);
}

} // namespace clang::tidy::modernize
51 changes: 51 additions & 0 deletions clang-tools-extra/clang-tidy/modernize/UseStdFormatCheck.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//===--- UseStdFormatCheck.h - clang-tidy -----------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTDFORMATCHECK_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTDFORMATCHECK_H

#include "../ClangTidyCheck.h"
#include "../utils/IncludeInserter.h"

namespace clang::tidy::modernize {

/// Converts calls to absl::StrFormat, or other functions via configuration
/// options, to C++20's std::format, or another function via a configuration
/// option, modifying the format string appropriately and removing
/// now-unnecessary calls to std::string::c_str() and std::string::data().
///
/// For the user-facing documentation see:
/// http://clang.llvm.org/extra/clang-tidy/checks/modernize/use-std-format.html
class UseStdFormatCheck : public ClangTidyCheck {
public:
UseStdFormatCheck(StringRef Name, ClangTidyContext *Context);
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
if (ReplacementFormatFunction == "std::format")
return LangOpts.CPlusPlus20;
return LangOpts.CPlusPlus;
}
void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
Preprocessor *ModuleExpanderPP) override;
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
std::optional<TraversalKind> getCheckTraversalKind() const override {
return TK_IgnoreUnlessSpelledInSource;
}

private:
bool StrictMode;
std::vector<StringRef> StrFormatLikeFunctions;
StringRef ReplacementFormatFunction;
utils::IncludeInserter IncludeInserter;
std::optional<StringRef> MaybeHeaderToInclude;
};

} // namespace clang::tidy::modernize

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTDFORMATCHECK_H
5 changes: 4 additions & 1 deletion clang-tools-extra/clang-tidy/modernize/UseStdPrintCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,11 @@ void UseStdPrintCheck::check(const MatchFinder::MatchResult &Result) {
FormatArgOffset = 1;
}

utils::FormatStringConverter::Configuration ConverterConfig;
ConverterConfig.StrictMode = StrictMode;
ConverterConfig.AllowTrailingNewlineRemoval = true;
utils::FormatStringConverter Converter(
Result.Context, Printf, FormatArgOffset, StrictMode, getLangOpts());
Result.Context, Printf, FormatArgOffset, ConverterConfig, getLangOpts());
const Expr *PrintfCall = Printf->getCallee();
const StringRef ReplacementFunction = Converter.usePrintNewlineFunction()
? ReplacementPrintlnFunction
Expand Down
16 changes: 10 additions & 6 deletions clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,11 @@ static bool castMismatchedIntegerTypes(const CallExpr *Call, bool StrictMode) {
FormatStringConverter::FormatStringConverter(ASTContext *ContextIn,
const CallExpr *Call,
unsigned FormatArgOffset,
bool StrictMode,
const Configuration ConfigIn,
const LangOptions &LO)
: Context(ContextIn),
CastMismatchedIntegerTypes(castMismatchedIntegerTypes(Call, StrictMode)),
: Context(ContextIn), Config(ConfigIn),
CastMismatchedIntegerTypes(
castMismatchedIntegerTypes(Call, ConfigIn.StrictMode)),
Args(Call->getArgs()), NumArgs(Call->getNumArgs()),
ArgsOffset(FormatArgOffset + 1), LangOpts(LO) {
assert(ArgsOffset <= NumArgs);
Expand Down Expand Up @@ -627,9 +628,12 @@ void FormatStringConverter::finalizeFormatText() {

// It's clearer to convert printf("Hello\r\n"); to std::print("Hello\r\n")
// than to std::println("Hello\r");
if (StringRef(StandardFormatString).ends_with("\\n") &&
!StringRef(StandardFormatString).ends_with("\\\\n") &&
!StringRef(StandardFormatString).ends_with("\\r\\n")) {
// Use StringRef until C++20 std::string::ends_with() is available.
const auto StandardFormatStringRef = StringRef(StandardFormatString);
if (Config.AllowTrailingNewlineRemoval &&
StandardFormatStringRef.ends_with("\\n") &&
!StandardFormatStringRef.ends_with("\\\\n") &&
!StandardFormatStringRef.ends_with("\\r\\n")) {
UsePrintNewlineFunction = true;
FormatStringNeededRewriting = true;
StandardFormatString.erase(StandardFormatString.end() - 2,
Expand Down
9 changes: 8 additions & 1 deletion clang-tools-extra/clang-tidy/utils/FormatStringConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ class FormatStringConverter
public:
using ConversionSpecifier = clang::analyze_format_string::ConversionSpecifier;
using PrintfSpecifier = analyze_printf::PrintfSpecifier;

struct Configuration {
bool StrictMode = false;
bool AllowTrailingNewlineRemoval = false;
};

FormatStringConverter(ASTContext *Context, const CallExpr *Call,
unsigned FormatArgOffset, bool StrictMode,
unsigned FormatArgOffset, Configuration Config,
const LangOptions &LO);

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

private:
ASTContext *Context;
const Configuration Config;
const bool CastMismatchedIntegerTypes;
const Expr *const *Args;
const unsigned NumArgs;
Expand Down
9 changes: 9 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ New checks
Finds initializer lists for aggregate types that could be
written as designated initializers instead.

- New :doc:`modernize-use-std-format
<clang-tidy/checks/modernize/use-std-format>` check.

Converts calls to ``absl::StrFormat``, or other functions via
configuration options, to C++20's ``std::format``, or another function
via a configuration option, modifying the format string appropriately and
removing now-unnecessary calls to ``std::string::c_str()`` and
``std::string::data()``.

- New :doc:`readability-enum-initial-value
<clang-tidy/checks/readability/enum-initial-value>` check.

Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/docs/clang-tidy/checks/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ Clang-Tidy Checks
:doc:`modernize-use-nullptr <modernize/use-nullptr>`, "Yes"
:doc:`modernize-use-override <modernize/use-override>`, "Yes"
:doc:`modernize-use-starts-ends-with <modernize/use-starts-ends-with>`, "Yes"
:doc:`modernize-use-std-format <modernize/use-std-format>`, "Yes"
:doc:`modernize-use-std-numbers <modernize/use-std-numbers>`, "Yes"
:doc:`modernize-use-std-print <modernize/use-std-print>`, "Yes"
:doc:`modernize-use-trailing-return-type <modernize/use-trailing-return-type>`, "Yes"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.. title:: clang-tidy - modernize-use-std-format

modernize-use-std-format
========================

Converts calls to ``absl::StrFormat``, or other functions via
configuration options, to C++20's ``std::format``, or another function
via a configuration option, modifying the format string appropriately and
removing now-unnecessary calls to ``std::string::c_str()`` and
``std::string::data()``.

For example, it turns lines like

.. code-block:: c++

return absl::StrFormat("The %s is %3d", description.c_str(), value);

into:

.. code-block:: c++

return std::format("The {} is {:3}", description, value);

The check uses the same format-string-conversion algorithm as
`modernize-use-std-print <../modernize/use-std-print.html>`_ and its
shortcomings are described in the documentation for that check.

Options
-------

.. option:: StrictMode

When `true`, the check will add casts when converting from variadic
functions and printing signed or unsigned integer types (including
fixed-width integer types from ``<cstdint>``, ``ptrdiff_t``, ``size_t``
and ``ssize_t``) as the opposite signedness to ensure that the output
would matches that of a simple wrapper for ``std::sprintf`` that
accepted a C-style variable argument list. For example, with
`StrictMode` enabled,

.. code-block:: c++

extern std::string strprintf(const char *format, ...);
int i = -42;
unsigned int u = 0xffffffff;
return strprintf("%d %u\n", i, u);
would be converted to

.. code-block:: c++

return std::format("{} {}\n", static_cast<unsigned int>(i), static_cast<int>(u));

to ensure that the output will continue to be the unsigned representation
of -42 and the signed representation of 0xffffffff (often 4294967254
and -1 respectively). When `false` (which is the default), these casts
will not be added which may cause a change in the output. Note that this
option makes no difference for the default value of
`StrFormatLikeFunctions` since ``absl::StrFormat`` takes a function
parameter pack and is not a variadic function.

.. option:: StrFormatLikeFunctions

A semicolon-separated list of (fully qualified) function names to
replace, with the requirement that the first parameter contains the
printf-style format string and the arguments to be formatted follow
immediately afterwards. The default value for this option is
`absl::StrFormat`.

.. option:: ReplacementFormatFunction

The function that will be used to replace the function set by the
`StrFormatLikeFunctions` option rather than the default
`std::format`. It is expected that the function provides an interface
that is compatible with ``std::format``. A suitable candidate would be
`fmt::format`.

.. option:: FormatHeader

The header that must be included for the declaration of
`ReplacementFormatFunction` so that a ``#include`` directive can be added if
required. If `ReplacementFormatFunction` is `std::format` then this option will
default to ``<format>``, otherwise this option will default to nothing
and no ``#include`` directive will be added.
Loading

0 comments on commit af79372

Please sign in to comment.