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-exception 6 // 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 20 namespace clang { 21 namespace tidy { 22 namespace utils { 23 namespace { 24 using namespace ::clang::ast_matchers; 25 26 using transformer::cat; 27 using transformer::change; 28 using transformer::IncludeFormat; 29 using transformer::makeRule; 30 using transformer::node; 31 using transformer::noopEdit; 32 using transformer::note; 33 using transformer::RewriteRuleWith; 34 using transformer::RootID; 35 using transformer::statement; 36 37 // Invert the code of an if-statement, while maintaining its semantics. 38 RewriteRuleWith<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 50 class IfInverterCheck : public TransformerClangTidyCheck { 51 public: 52 IfInverterCheck(StringRef Name, ClangTidyContext *Context) 53 : TransformerClangTidyCheck(invertIf(), Name, Context) {} 54 }; 55 56 // Basic test of using a rewrite rule as a ClangTidy. 57 TEST(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 else 64 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 76 TEST(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 96 transformer::ASTEdit noReplacementEdit(transformer::RangeSelector Target) { 97 transformer::ASTEdit E; 98 E.TargetRange = std::move(Target); 99 return E; 100 } 101 102 TEST(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 122 TEST(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 143 TEST(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 processing 159 // the diagnostic. The only behavior observable/verifiable by the test is that 160 // the presence of the '%' doesn't crash Clang. 161 EXPECT_EQ(Errors[0].Message.Message, "bad code: x % y % z"); 162 } 163 164 class IntLitCheck : public TransformerClangTidyCheck { 165 public: 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 conflicts 173 // in applying the changes. 174 TEST(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 187 class BinOpCheck : public TransformerClangTidyCheck { 188 public: 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 its 198 // argument, while the change spans only the argument AND there are two such 199 // matches. We verify that both replacements succeed. 200 TEST(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. 214 std::optional<RewriteRuleWith<std::string>> 215 needsObjC(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 223 class NeedsObjCCheck : public TransformerClangTidyCheck { 224 public: 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. 230 TEST(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. 240 std::optional<RewriteRuleWith<std::string>> 241 noSkip(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 249 class ConfigurableCheck : public TransformerClangTidyCheck { 250 public: 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. 256 TEST(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 270 RewriteRuleWith<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 279 template <IncludeFormat Format> 280 class IncludeCheck : public TransformerClangTidyCheck { 281 public: 282 IncludeCheck(StringRef Name, ClangTidyContext *Context) 283 : TransformerClangTidyCheck(replaceCall(Format), Name, Context) {} 284 }; 285 286 TEST(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 303 TEST(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 319 class 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 328 public: 329 IncludeOrderCheck(StringRef Name, ClangTidyContext *Context) 330 : TransformerClangTidyCheck(rule(), Name, Context) {} 331 }; 332 333 TEST(TransformerClangTidyCheckTest, AddIncludeObeysSortStyleLocalOption) { 334 std::string Input = R"cc(#include "input.h" 335 int h(int x) { return 3; })cc"; 336 337 std::string TreatsAsLibraryHeader = R"cc(#include "input.h" 338 339 #include "bar.h" 340 int h(int x) { return 5; })cc"; 341 342 std::string TreatsAsNormalHeader = R"cc(#include "bar.h" 343 #include "input.h" 344 int 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 365 TEST(TransformerClangTidyCheckTest, AddIncludeObeysSortStyleGlobalOption) { 366 std::string Input = R"cc(#include "input.h" 367 int h(int x) { return 3; })cc"; 368 369 std::string TreatsAsLibraryHeader = R"cc(#include "input.h" 370 371 #include "bar.h" 372 int h(int x) { return 5; })cc"; 373 374 std::string TreatsAsNormalHeader = R"cc(#include "bar.h" 375 #include "input.h" 376 int 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 } // namespace 398 } // namespace utils 399 } // namespace tidy 400 } // namespace clang 401