xref: /llvm-project/clang-tools-extra/unittests/clang-tidy/TransformerClangTidyCheckTest.cpp (revision fc5de0af33b7fb9b9f745e70dcd351f2de4211cb)
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