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" , std::nullopt, Options)); |
264 | |
265 | Options.CheckOptions["test-check-0.Skip" ] = "false" ; |
266 | EXPECT_EQ(Expected, test::runCheckOnCode<ConfigurableCheck>( |
267 | Input, nullptr, "input.cc" , std::nullopt, 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, test::runCheckOnCode<IncludeOrderCheck>( |
350 | Input, nullptr, "inputTest.cpp" , |
351 | std::nullopt, Options, PathsToContent)); |
352 | EXPECT_EQ(TreatsAsNormalHeader, test::runCheckOnCode<IncludeOrderCheck>( |
353 | Input, nullptr, "input_test.cpp" , |
354 | std::nullopt, Options, PathsToContent)); |
355 | |
356 | Options.CheckOptions["test-check-0.IncludeStyle" ] = "google" ; |
357 | EXPECT_EQ(TreatsAsNormalHeader, test::runCheckOnCode<IncludeOrderCheck>( |
358 | Input, nullptr, "inputTest.cc" , |
359 | std::nullopt, Options, PathsToContent)); |
360 | EXPECT_EQ(TreatsAsLibraryHeader, test::runCheckOnCode<IncludeOrderCheck>( |
361 | Input, nullptr, "input_test.cc" , |
362 | std::nullopt, 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, test::runCheckOnCode<IncludeOrderCheck>( |
382 | Input, nullptr, "inputTest.cpp" , |
383 | std::nullopt, Options, PathsToContent)); |
384 | EXPECT_EQ(TreatsAsNormalHeader, test::runCheckOnCode<IncludeOrderCheck>( |
385 | Input, nullptr, "input_test.cpp" , |
386 | std::nullopt, Options, PathsToContent)); |
387 | |
388 | Options.CheckOptions["IncludeStyle" ] = "google" ; |
389 | EXPECT_EQ(TreatsAsNormalHeader, test::runCheckOnCode<IncludeOrderCheck>( |
390 | Input, nullptr, "inputTest.cc" , |
391 | std::nullopt, Options, PathsToContent)); |
392 | EXPECT_EQ(TreatsAsLibraryHeader, test::runCheckOnCode<IncludeOrderCheck>( |
393 | Input, nullptr, "input_test.cc" , |
394 | std::nullopt, Options, PathsToContent)); |
395 | } |
396 | |
397 | } // namespace |
398 | } // namespace utils |
399 | } // namespace tidy |
400 | } // namespace clang |
401 | |