diff --git a/clang-tools-extra/clang-tidy/utils/TransformerClangTidyCheck.cpp b/clang-tools-extra/clang-tidy/utils/TransformerClangTidyCheck.cpp --- a/clang-tools-extra/clang-tidy/utils/TransformerClangTidyCheck.cpp +++ b/clang-tools-extra/clang-tidy/utils/TransformerClangTidyCheck.cpp @@ -27,6 +27,33 @@ " explicitly provide an empty explanation if none is desired"); } +// If a string unintentionally containing '%' is passed as a diagnostic, Clang +// will claim the string is ill-formed and assert-fail. This function escapes +// such strings so they can be safely used in diagnostics. +std::string escapeForDiagnostic(std::string ToEscape) { + // Optimize for the common case that the string does not contain `%` at the + // cost of an extra scan over the string in the slow case. + auto Pos = ToEscape.find('%'); + if (Pos == ToEscape.npos) + return ToEscape; + + std::string Result; + Result.reserve(ToEscape.size()); + // Convert position to a count. + ++Pos; + Result.append(ToEscape, 0, Pos); + Result += '%'; + + for (auto N = ToEscape.size(); Pos < N; ++Pos) { + const char C = ToEscape.at(Pos); + Result += C; + if (C == '%') + Result += '%'; + } + + return Result; +} + TransformerClangTidyCheck::TransformerClangTidyCheck(StringRef Name, ClangTidyContext *Context) : ClangTidyCheck(Name, Context), @@ -99,7 +126,8 @@ } // Associate the diagnostic with the location of the first change. - DiagnosticBuilder Diag = diag((*Edits)[0].Range.getBegin(), *Explanation); + DiagnosticBuilder Diag = + diag((*Edits)[0].Range.getBegin(), escapeForDiagnostic(*Explanation)); for (const auto &T : *Edits) switch (T.Kind) { case transformer::EditKind::Range: diff --git a/clang-tools-extra/unittests/clang-tidy/TransformerClangTidyCheckTest.cpp b/clang-tools-extra/unittests/clang-tidy/TransformerClangTidyCheckTest.cpp --- a/clang-tools-extra/unittests/clang-tidy/TransformerClangTidyCheckTest.cpp +++ b/clang-tools-extra/unittests/clang-tidy/TransformerClangTidyCheckTest.cpp @@ -90,6 +90,25 @@ EXPECT_EQ(Errors[0].Message.FileOffset, 10U); } +TEST(TransformerClangTidyCheckTest, DiagnosticMessageEscaped) { + class GiveDiagWithPercentSymbol : public TransformerClangTidyCheck { + public: + GiveDiagWithPercentSymbol(StringRef Name, ClangTidyContext *Context) + : TransformerClangTidyCheck(makeRule(returnStmt(), + noopEdit(node(RootID)), + cat("bad code: x % y % z")), + Name, Context) {} + }; + std::string Input = "int somecode() { return 0; }"; + std::vector Errors; + EXPECT_EQ(Input, + test::runCheckOnCode(Input, &Errors)); + ASSERT_EQ(Errors.size(), 1U); + // This should match the input; this test only verifies that the presence of + // '%' doesn't crash Clang. + EXPECT_EQ(Errors[0].Message.Message, "bad code: x % y % z"); +} + class IntLitCheck : public TransformerClangTidyCheck { public: IntLitCheck(StringRef Name, ClangTidyContext *Context)