401 lines · cpp
1//===---- TransformerClangTidyCheckTest.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-exception6//7//===----------------------------------------------------------------------===//8 9#include "../clang-tidy/utils/TransformerClangTidyCheck.h"10#include "ClangTidyTest.h"11#include "clang/ASTMatchers/ASTMatchers.h"12#include "clang/Tooling/Transformer/RangeSelector.h"13#include "clang/Tooling/Transformer/RewriteRule.h"14#include "clang/Tooling/Transformer/Stencil.h"15#include "clang/Tooling/Transformer/Transformer.h"16#include "gmock/gmock.h"17#include "gtest/gtest.h"18#include <optional>19 20namespace clang {21namespace tidy {22namespace utils {23namespace {24using namespace ::clang::ast_matchers;25 26using transformer::cat;27using transformer::change;28using transformer::IncludeFormat;29using transformer::makeRule;30using transformer::node;31using transformer::noopEdit;32using transformer::note;33using transformer::RewriteRuleWith;34using transformer::RootID;35using transformer::statement;36 37// Invert the code of an if-statement, while maintaining its semantics.38RewriteRuleWith<std::string> invertIf() {39 StringRef C = "C", T = "T", E = "E";40 RewriteRuleWith<std::string> Rule = makeRule(41 ifStmt(hasCondition(expr().bind(C)), hasThen(stmt().bind(T)),42 hasElse(stmt().bind(E))),43 change(statement(RootID), cat("if(!(", node(std::string(C)), ")) ",44 statement(std::string(E)), " else ",45 statement(std::string(T)))),46 cat("negate condition and reverse `then` and `else` branches"));47 return Rule;48}49 50class IfInverterCheck : public TransformerClangTidyCheck {51public:52 IfInverterCheck(StringRef Name, ClangTidyContext *Context)53 : TransformerClangTidyCheck(invertIf(), Name, Context) {}54};55 56// Basic test of using a rewrite rule as a ClangTidy.57TEST(TransformerClangTidyCheckTest, Basic) {58 const std::string Input = R"cc(59 void log(const char* msg);60 void foo() {61 if (10 > 1.0)62 log("oh no!");63 else64 log("ok");65 }66 )cc";67 const std::string Expected = R"(68 void log(const char* msg);69 void foo() {70 if(!(10 > 1.0)) log("ok"); else log("oh no!");71 }72 )";73 EXPECT_EQ(Expected, test::runCheckOnCode<IfInverterCheck>(Input));74}75 76TEST(TransformerClangTidyCheckTest, DiagnosticsCorrectlyGenerated) {77 class DiagOnlyCheck : public TransformerClangTidyCheck {78 public:79 DiagOnlyCheck(StringRef Name, ClangTidyContext *Context)80 : TransformerClangTidyCheck(81 makeRule(returnStmt(), noopEdit(node(RootID)), cat("message")),82 Name, Context) {}83 };84 std::string Input = "int h() { return 5; }";85 std::vector<ClangTidyError> Errors;86 EXPECT_EQ(Input, test::runCheckOnCode<DiagOnlyCheck>(Input, &Errors));87 EXPECT_EQ(Errors.size(), 1U);88 EXPECT_EQ(Errors[0].Message.Message, "message");89 EXPECT_THAT(Errors[0].Message.Ranges, testing::IsEmpty());90 EXPECT_THAT(Errors[0].Notes, testing::IsEmpty());91 92 // The diagnostic is anchored to the match, "return 5".93 EXPECT_EQ(Errors[0].Message.FileOffset, 10U);94}95 96transformer::ASTEdit noReplacementEdit(transformer::RangeSelector Target) {97 transformer::ASTEdit E;98 E.TargetRange = std::move(Target);99 return E;100}101 102TEST(TransformerClangTidyCheckTest, EmptyReplacement) {103 class DiagOnlyCheck : public TransformerClangTidyCheck {104 public:105 DiagOnlyCheck(StringRef Name, ClangTidyContext *Context)106 : TransformerClangTidyCheck(107 makeRule(returnStmt(), edit(noReplacementEdit(node(RootID))),108 cat("message")),109 Name, Context) {}110 };111 std::string Input = "int h() { return 5; }";112 std::vector<ClangTidyError> Errors;113 EXPECT_EQ("int h() { }", test::runCheckOnCode<DiagOnlyCheck>(Input, &Errors));114 EXPECT_EQ(Errors.size(), 1U);115 EXPECT_EQ(Errors[0].Message.Message, "message");116 EXPECT_THAT(Errors[0].Message.Ranges, testing::IsEmpty());117 118 // The diagnostic is anchored to the match, "return 5".119 EXPECT_EQ(Errors[0].Message.FileOffset, 10U);120}121 122TEST(TransformerClangTidyCheckTest, NotesCorrectlyGenerated) {123 class DiagAndNoteCheck : public TransformerClangTidyCheck {124 public:125 DiagAndNoteCheck(StringRef Name, ClangTidyContext *Context)126 : TransformerClangTidyCheck(127 makeRule(returnStmt(),128 note(node(RootID), cat("some note")),129 cat("message")),130 Name, Context) {}131 };132 std::string Input = "int h() { return 5; }";133 std::vector<ClangTidyError> Errors;134 EXPECT_EQ(Input, test::runCheckOnCode<DiagAndNoteCheck>(Input, &Errors));135 EXPECT_EQ(Errors.size(), 1U);136 EXPECT_EQ(Errors[0].Notes.size(), 1U);137 EXPECT_EQ(Errors[0].Notes[0].Message, "some note");138 139 // The note is anchored to the match, "return 5".140 EXPECT_EQ(Errors[0].Notes[0].FileOffset, 10U);141}142 143TEST(TransformerClangTidyCheckTest, DiagnosticMessageEscaped) {144 class GiveDiagWithPercentSymbol : public TransformerClangTidyCheck {145 public:146 GiveDiagWithPercentSymbol(StringRef Name, ClangTidyContext *Context)147 : TransformerClangTidyCheck(makeRule(returnStmt(),148 noopEdit(node(RootID)),149 cat("bad code: x % y % z")),150 Name, Context) {}151 };152 std::string Input = "int somecode() { return 0; }";153 std::vector<ClangTidyError> Errors;154 EXPECT_EQ(Input,155 test::runCheckOnCode<GiveDiagWithPercentSymbol>(Input, &Errors));156 ASSERT_EQ(Errors.size(), 1U);157 // The message stored in this field shouldn't include escaped percent signs,158 // because the diagnostic printer should have _unescaped_ them when processing159 // the diagnostic. The only behavior observable/verifiable by the test is that160 // the presence of the '%' doesn't crash Clang.161 EXPECT_EQ(Errors[0].Message.Message, "bad code: x % y % z");162}163 164class IntLitCheck : public TransformerClangTidyCheck {165public:166 IntLitCheck(StringRef Name, ClangTidyContext *Context)167 : TransformerClangTidyCheck(168 makeRule(integerLiteral(), change(cat("LIT")), cat("no message")),169 Name, Context) {}170};171 172// Tests that two changes in a single macro expansion do not lead to conflicts173// in applying the changes.174TEST(TransformerClangTidyCheckTest, TwoChangesInOneMacroExpansion) {175 const std::string Input = R"cc(176#define PLUS(a,b) (a) + (b)177 int f() { return PLUS(3, 4); }178 )cc";179 const std::string Expected = R"cc(180#define PLUS(a,b) (a) + (b)181 int f() { return PLUS(LIT, LIT); }182 )cc";183 184 EXPECT_EQ(Expected, test::runCheckOnCode<IntLitCheck>(Input));185}186 187class BinOpCheck : public TransformerClangTidyCheck {188public:189 BinOpCheck(StringRef Name, ClangTidyContext *Context)190 : TransformerClangTidyCheck(191 makeRule(192 binaryOperator(hasOperatorName("+"), hasRHS(expr().bind("r"))),193 change(node("r"), cat("RIGHT")), cat("no message")),194 Name, Context) {}195};196 197// Tests case where the rule's match spans both source from the macro and its198// argument, while the change spans only the argument AND there are two such199// matches. We verify that both replacements succeed.200TEST(TransformerClangTidyCheckTest, TwoMatchesInMacroExpansion) {201 const std::string Input = R"cc(202#define M(a,b) (1 + a) * (1 + b)203 int f() { return M(3, 4); }204 )cc";205 const std::string Expected = R"cc(206#define M(a,b) (1 + a) * (1 + b)207 int f() { return M(RIGHT, RIGHT); }208 )cc";209 210 EXPECT_EQ(Expected, test::runCheckOnCode<BinOpCheck>(Input));211}212 213// A trivial rewrite-rule generator that requires Objective-C code.214std::optional<RewriteRuleWith<std::string>>215needsObjC(const LangOptions &LangOpts,216 const ClangTidyCheck::OptionsView &Options) {217 if (!LangOpts.ObjC)218 return std::nullopt;219 return makeRule(clang::ast_matchers::functionDecl(),220 change(cat("void changed() {}")), cat("no message"));221}222 223class NeedsObjCCheck : public TransformerClangTidyCheck {224public:225 NeedsObjCCheck(StringRef Name, ClangTidyContext *Context)226 : TransformerClangTidyCheck(needsObjC, Name, Context) {}227};228 229// Verify that the check only rewrites the code when the input is Objective-C.230TEST(TransformerClangTidyCheckTest, DisableByLang) {231 const std::string Input = "void log() {}";232 EXPECT_EQ(Input,233 test::runCheckOnCode<NeedsObjCCheck>(Input, nullptr, "input.cc"));234 235 EXPECT_EQ("void changed() {}",236 test::runCheckOnCode<NeedsObjCCheck>(Input, nullptr, "input.mm"));237}238 239// A trivial rewrite rule generator that checks config options.240std::optional<RewriteRuleWith<std::string>>241noSkip(const LangOptions &LangOpts,242 const ClangTidyCheck::OptionsView &Options) {243 if (Options.get("Skip", "false") == "true")244 return std::nullopt;245 return makeRule(clang::ast_matchers::functionDecl(),246 changeTo(cat("void nothing();")), cat("no message"));247}248 249class ConfigurableCheck : public TransformerClangTidyCheck {250public:251 ConfigurableCheck(StringRef Name, ClangTidyContext *Context)252 : TransformerClangTidyCheck(noSkip, Name, Context) {}253};254 255// Tests operation with config option "Skip" set to true and false.256TEST(TransformerClangTidyCheckTest, DisableByConfig) {257 const std::string Input = "void log(int);";258 const std::string Expected = "void nothing();";259 ClangTidyOptions Options;260 261 Options.CheckOptions["test-check-0.Skip"] = "true";262 EXPECT_EQ(Input, test::runCheckOnCode<ConfigurableCheck>(263 Input, nullptr, "input.cc", {}, Options));264 265 Options.CheckOptions["test-check-0.Skip"] = "false";266 EXPECT_EQ(Expected, test::runCheckOnCode<ConfigurableCheck>(267 Input, nullptr, "input.cc", {}, Options));268}269 270RewriteRuleWith<std::string> replaceCall(IncludeFormat Format) {271 using namespace ::clang::ast_matchers;272 RewriteRuleWith<std::string> Rule =273 makeRule(callExpr(callee(functionDecl(hasName("f")))),274 change(cat("other()")), cat("no message"));275 addInclude(Rule, "clang/OtherLib.h", Format);276 return Rule;277}278 279template <IncludeFormat Format>280class IncludeCheck : public TransformerClangTidyCheck {281public:282 IncludeCheck(StringRef Name, ClangTidyContext *Context)283 : TransformerClangTidyCheck(replaceCall(Format), Name, Context) {}284};285 286TEST(TransformerClangTidyCheckTest, AddIncludeQuoted) {287 288 std::string Input = R"cc(289 int f(int x);290 int h(int x) { return f(x); }291 )cc";292 std::string Expected = R"cc(#include "clang/OtherLib.h"293 294 295 int f(int x);296 int h(int x) { return other(); }297 )cc";298 299 EXPECT_EQ(Expected,300 test::runCheckOnCode<IncludeCheck<IncludeFormat::Quoted>>(Input));301}302 303TEST(TransformerClangTidyCheckTest, AddIncludeAngled) {304 std::string Input = R"cc(305 int f(int x);306 int h(int x) { return f(x); }307 )cc";308 std::string Expected = R"cc(#include <clang/OtherLib.h>309 310 311 int f(int x);312 int h(int x) { return other(); }313 )cc";314 315 EXPECT_EQ(Expected,316 test::runCheckOnCode<IncludeCheck<IncludeFormat::Angled>>(Input));317}318 319class IncludeOrderCheck : public TransformerClangTidyCheck {320 static RewriteRuleWith<std::string> rule() {321 using namespace ::clang::ast_matchers;322 RewriteRuleWith<std::string> Rule = transformer::makeRule(323 integerLiteral(), change(cat("5")), cat("no message"));324 addInclude(Rule, "bar.h", IncludeFormat::Quoted);325 return Rule;326 }327 328public:329 IncludeOrderCheck(StringRef Name, ClangTidyContext *Context)330 : TransformerClangTidyCheck(rule(), Name, Context) {}331};332 333TEST(TransformerClangTidyCheckTest, AddIncludeObeysSortStyleLocalOption) {334 std::string Input = R"cc(#include "input.h"335int h(int x) { return 3; })cc";336 337 std::string TreatsAsLibraryHeader = R"cc(#include "input.h"338 339#include "bar.h"340int h(int x) { return 5; })cc";341 342 std::string TreatsAsNormalHeader = R"cc(#include "bar.h"343#include "input.h"344int h(int x) { return 5; })cc";345 346 ClangTidyOptions Options;347 std::map<StringRef, StringRef> PathsToContent = {{"input.h", "\n"}};348 Options.CheckOptions["test-check-0.IncludeStyle"] = "llvm";349 EXPECT_EQ(TreatsAsLibraryHeader,350 test::runCheckOnCode<IncludeOrderCheck>(351 Input, nullptr, "inputTest.cpp", {}, Options, PathsToContent));352 EXPECT_EQ(TreatsAsNormalHeader,353 test::runCheckOnCode<IncludeOrderCheck>(354 Input, nullptr, "input_test.cpp", {}, Options, PathsToContent));355 356 Options.CheckOptions["test-check-0.IncludeStyle"] = "google";357 EXPECT_EQ(TreatsAsNormalHeader,358 test::runCheckOnCode<IncludeOrderCheck>(359 Input, nullptr, "inputTest.cc", {}, Options, PathsToContent));360 EXPECT_EQ(TreatsAsLibraryHeader,361 test::runCheckOnCode<IncludeOrderCheck>(362 Input, nullptr, "input_test.cc", {}, Options, PathsToContent));363}364 365TEST(TransformerClangTidyCheckTest, AddIncludeObeysSortStyleGlobalOption) {366 std::string Input = R"cc(#include "input.h"367int h(int x) { return 3; })cc";368 369 std::string TreatsAsLibraryHeader = R"cc(#include "input.h"370 371#include "bar.h"372int h(int x) { return 5; })cc";373 374 std::string TreatsAsNormalHeader = R"cc(#include "bar.h"375#include "input.h"376int h(int x) { return 5; })cc";377 378 ClangTidyOptions Options;379 std::map<StringRef, StringRef> PathsToContent = {{"input.h", "\n"}};380 Options.CheckOptions["IncludeStyle"] = "llvm";381 EXPECT_EQ(TreatsAsLibraryHeader,382 test::runCheckOnCode<IncludeOrderCheck>(383 Input, nullptr, "inputTest.cpp", {}, Options, PathsToContent));384 EXPECT_EQ(TreatsAsNormalHeader,385 test::runCheckOnCode<IncludeOrderCheck>(386 Input, nullptr, "input_test.cpp", {}, Options, PathsToContent));387 388 Options.CheckOptions["IncludeStyle"] = "google";389 EXPECT_EQ(TreatsAsNormalHeader,390 test::runCheckOnCode<IncludeOrderCheck>(391 Input, nullptr, "inputTest.cc", {}, Options, PathsToContent));392 EXPECT_EQ(TreatsAsLibraryHeader,393 test::runCheckOnCode<IncludeOrderCheck>(394 Input, nullptr, "input_test.cc", {}, Options, PathsToContent));395}396 397} // namespace398} // namespace utils399} // namespace tidy400} // namespace clang401