| 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 | M: ifStmt(hasCondition(InnerMatcher: expr().bind(ID: C)), hasThen(InnerMatcher: stmt().bind(ID: T)), |
| 42 | hasElse(InnerMatcher: stmt().bind(ID: E))), |
| 43 | Edits: change(Target: statement(ID: RootID), Replacement: cat(Parts: "if(!(" , Parts: node(ID: std::string(C)), Parts: ")) " , |
| 44 | Parts: statement(ID: std::string(E)), Parts: " else " , |
| 45 | Parts: statement(ID: std::string(T)))), |
| 46 | Metadata: cat(Parts: "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(M: returnStmt(), Edits: noopEdit(Anchor: node(ID: RootID)), Metadata: cat(Parts: "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(M: returnStmt(), Edits: edit(E: noReplacementEdit(Target: node(ID: RootID))), |
| 108 | Metadata: cat(Parts: "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(M: returnStmt(), |
| 128 | Edits: note(Anchor: node(ID: RootID), Note: cat(Parts: "some note" )), |
| 129 | Metadata: cat(Parts: "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(M: returnStmt(), |
| 148 | Edits: noopEdit(Anchor: node(ID: RootID)), |
| 149 | Metadata: cat(Parts: "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(M: integerLiteral(), Edits: change(Replacement: cat(Parts: "LIT" )), Metadata: cat(Parts: "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 | M: binaryOperator(hasOperatorName(Name: "+" ), hasRHS(InnerMatcher: expr().bind(ID: "r" ))), |
| 193 | Edits: change(Target: node(ID: "r" ), Replacement: cat(Parts: "RIGHT" )), Metadata: cat(Parts: "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(M: clang::ast_matchers::functionDecl(), |
| 220 | Edits: change(Replacement: cat(Parts: "void changed() {}" )), Metadata: cat(Parts: "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(LocalName: "Skip" , Default: "false" ) == "true" ) |
| 244 | return std::nullopt; |
| 245 | return makeRule(M: clang::ast_matchers::functionDecl(), |
| 246 | Edits: changeTo(Replacement: cat(Parts: "void nothing();" )), Metadata: cat(Parts: "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(M: callExpr(callee(InnerMatcher: functionDecl(hasName(Name: "f" )))), |
| 274 | Edits: change(Replacement: cat(Parts: "other()" )), Metadata: cat(Parts: "no message" )); |
| 275 | addInclude(Rule, Header: "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 | M: integerLiteral(), Edits: change(Replacement: cat(Parts: "5" )), Metadata: cat(Parts: "no message" )); |
| 324 | addInclude(Rule, Header: "bar.h" , Format: 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 = R"cc(#include "input.h" |
| 338 | |
| 339 | #include "bar.h" |
| 340 | int h(int x) { return 5; })cc" ; |
| 341 | |
| 342 | std::string = 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 = R"cc(#include "input.h" |
| 370 | |
| 371 | #include "bar.h" |
| 372 | int h(int x) { return 5; })cc" ; |
| 373 | |
| 374 | std::string = 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 | |