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
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 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
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 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
76TEST(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
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(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
122TEST(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
143TEST(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
164class IntLitCheck : public TransformerClangTidyCheck {
165public:
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.
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 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.
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(M: clang::ast_matchers::functionDecl(),
220 Edits: change(Replacement: cat(Parts: "void changed() {}")), Metadata: cat(Parts: "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(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
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", 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
270RewriteRuleWith<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
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 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
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, 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
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, 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

source code of clang-tools-extra/unittests/clang-tidy/TransformerClangTidyCheckTest.cpp