1 //===--- UpgradeGoogletestCaseCheck.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 "UpgradeGoogletestCaseCheck.h"
10 #include "clang/AST/ASTContext.h"
11 #include "clang/ASTMatchers/ASTMatchFinder.h"
12 #include "clang/Lex/PPCallbacks.h"
13 #include "clang/Lex/Preprocessor.h"
14
15 using namespace clang::ast_matchers;
16
17 namespace clang {
18 namespace tidy {
19 namespace google {
20
21 static const llvm::StringRef RenameCaseToSuiteMessage =
22 "Google Test APIs named with 'case' are deprecated; use equivalent APIs "
23 "named with 'suite'";
24
25 static llvm::Optional<llvm::StringRef>
getNewMacroName(llvm::StringRef MacroName)26 getNewMacroName(llvm::StringRef MacroName) {
27 std::pair<llvm::StringRef, llvm::StringRef> ReplacementMap[] = {
28 {"TYPED_TEST_CASE", "TYPED_TEST_SUITE"},
29 {"TYPED_TEST_CASE_P", "TYPED_TEST_SUITE_P"},
30 {"REGISTER_TYPED_TEST_CASE_P", "REGISTER_TYPED_TEST_SUITE_P"},
31 {"INSTANTIATE_TYPED_TEST_CASE_P", "INSTANTIATE_TYPED_TEST_SUITE_P"},
32 {"INSTANTIATE_TEST_CASE_P", "INSTANTIATE_TEST_SUITE_P"},
33 };
34
35 for (auto &Mapping : ReplacementMap) {
36 if (MacroName == Mapping.first)
37 return Mapping.second;
38 }
39
40 return llvm::None;
41 }
42
43 namespace {
44
45 class UpgradeGoogletestCasePPCallback : public PPCallbacks {
46 public:
UpgradeGoogletestCasePPCallback(UpgradeGoogletestCaseCheck * Check,Preprocessor * PP)47 UpgradeGoogletestCasePPCallback(UpgradeGoogletestCaseCheck *Check,
48 Preprocessor *PP)
49 : ReplacementFound(false), Check(Check), PP(PP) {}
50
MacroExpands(const Token & MacroNameTok,const MacroDefinition & MD,SourceRange Range,const MacroArgs *)51 void MacroExpands(const Token &MacroNameTok, const MacroDefinition &MD,
52 SourceRange Range, const MacroArgs *) override {
53 macroUsed(MacroNameTok, MD, Range.getBegin(), CheckAction::Rename);
54 }
55
MacroUndefined(const Token & MacroNameTok,const MacroDefinition & MD,const MacroDirective * Undef)56 void MacroUndefined(const Token &MacroNameTok, const MacroDefinition &MD,
57 const MacroDirective *Undef) override {
58 if (Undef != nullptr)
59 macroUsed(MacroNameTok, MD, Undef->getLocation(), CheckAction::Warn);
60 }
61
MacroDefined(const Token & MacroNameTok,const MacroDirective * MD)62 void MacroDefined(const Token &MacroNameTok,
63 const MacroDirective *MD) override {
64 if (!ReplacementFound && MD != nullptr) {
65 // We check if the newly defined macro is one of the target replacements.
66 // This ensures that the check creates warnings only if it is including a
67 // recent enough version of Google Test.
68 llvm::StringRef FileName = PP->getSourceManager().getFilename(
69 MD->getMacroInfo()->getDefinitionLoc());
70 ReplacementFound = FileName.endswith("gtest/gtest-typed-test.h") &&
71 PP->getSpelling(MacroNameTok) == "TYPED_TEST_SUITE";
72 }
73 }
74
Defined(const Token & MacroNameTok,const MacroDefinition & MD,SourceRange Range)75 void Defined(const Token &MacroNameTok, const MacroDefinition &MD,
76 SourceRange Range) override {
77 macroUsed(MacroNameTok, MD, Range.getBegin(), CheckAction::Warn);
78 }
79
Ifdef(SourceLocation Loc,const Token & MacroNameTok,const MacroDefinition & MD)80 void Ifdef(SourceLocation Loc, const Token &MacroNameTok,
81 const MacroDefinition &MD) override {
82 macroUsed(MacroNameTok, MD, Loc, CheckAction::Warn);
83 }
84
Ifndef(SourceLocation Loc,const Token & MacroNameTok,const MacroDefinition & MD)85 void Ifndef(SourceLocation Loc, const Token &MacroNameTok,
86 const MacroDefinition &MD) override {
87 macroUsed(MacroNameTok, MD, Loc, CheckAction::Warn);
88 }
89
90 private:
91 enum class CheckAction { Warn, Rename };
92
macroUsed(const clang::Token & MacroNameTok,const MacroDefinition & MD,SourceLocation Loc,CheckAction Action)93 void macroUsed(const clang::Token &MacroNameTok, const MacroDefinition &MD,
94 SourceLocation Loc, CheckAction Action) {
95 if (!ReplacementFound)
96 return;
97
98 std::string Name = PP->getSpelling(MacroNameTok);
99
100 llvm::Optional<llvm::StringRef> Replacement = getNewMacroName(Name);
101 if (!Replacement)
102 return;
103
104 llvm::StringRef FileName = PP->getSourceManager().getFilename(
105 MD.getMacroInfo()->getDefinitionLoc());
106 if (!FileName.endswith("gtest/gtest-typed-test.h"))
107 return;
108
109 DiagnosticBuilder Diag = Check->diag(Loc, RenameCaseToSuiteMessage);
110
111 if (Action == CheckAction::Rename)
112 Diag << FixItHint::CreateReplacement(
113 CharSourceRange::getTokenRange(Loc, Loc), *Replacement);
114 }
115
116 bool ReplacementFound;
117 UpgradeGoogletestCaseCheck *Check;
118 Preprocessor *PP;
119 };
120
121 } // namespace
122
registerPPCallbacks(const SourceManager &,Preprocessor * PP,Preprocessor *)123 void UpgradeGoogletestCaseCheck::registerPPCallbacks(const SourceManager &,
124 Preprocessor *PP,
125 Preprocessor *) {
126 PP->addPPCallbacks(
127 std::make_unique<UpgradeGoogletestCasePPCallback>(this, PP));
128 }
129
registerMatchers(MatchFinder * Finder)130 void UpgradeGoogletestCaseCheck::registerMatchers(MatchFinder *Finder) {
131 auto LocationFilter =
132 unless(isExpansionInFileMatching("gtest/gtest(-typed-test)?\\.h$"));
133
134 // Matchers for the member functions that are being renamed. In each matched
135 // Google Test class, we check for the existence of one new method name. This
136 // makes sure the check gives warnings only if the included version of Google
137 // Test is recent enough.
138 auto Methods =
139 cxxMethodDecl(
140 anyOf(
141 cxxMethodDecl(
142 hasAnyName("SetUpTestCase", "TearDownTestCase"),
143 ofClass(
144 cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl(
145 hasName("::testing::Test"),
146 hasMethod(hasName("SetUpTestSuite")))))
147 .bind("class"))),
148 cxxMethodDecl(
149 hasName("test_case_name"),
150 ofClass(
151 cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl(
152 hasName("::testing::TestInfo"),
153 hasMethod(hasName("test_suite_name")))))
154 .bind("class"))),
155 cxxMethodDecl(
156 hasAnyName("OnTestCaseStart", "OnTestCaseEnd"),
157 ofClass(cxxRecordDecl(
158 isSameOrDerivedFrom(cxxRecordDecl(
159 hasName("::testing::TestEventListener"),
160 hasMethod(hasName("OnTestSuiteStart")))))
161 .bind("class"))),
162 cxxMethodDecl(
163 hasAnyName("current_test_case", "successful_test_case_count",
164 "failed_test_case_count", "total_test_case_count",
165 "test_case_to_run_count", "GetTestCase"),
166 ofClass(cxxRecordDecl(
167 isSameOrDerivedFrom(cxxRecordDecl(
168 hasName("::testing::UnitTest"),
169 hasMethod(hasName("current_test_suite")))))
170 .bind("class")))))
171 .bind("method");
172
173 Finder->addMatcher(expr(anyOf(callExpr(callee(Methods)).bind("call"),
174 declRefExpr(to(Methods)).bind("ref")),
175 LocationFilter),
176 this);
177
178 Finder->addMatcher(
179 usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(Methods)), LocationFilter)
180 .bind("using"),
181 this);
182
183 Finder->addMatcher(cxxMethodDecl(Methods, LocationFilter), this);
184
185 // Matchers for `TestCase` -> `TestSuite`. The fact that `TestCase` is an
186 // alias and not a class declaration ensures we only match with a recent
187 // enough version of Google Test.
188 auto TestCaseTypeAlias =
189 typeAliasDecl(hasName("::testing::TestCase")).bind("test-case");
190 Finder->addMatcher(
191 typeLoc(loc(qualType(typedefType(hasDeclaration(TestCaseTypeAlias)))),
192 unless(hasAncestor(decl(isImplicit()))), LocationFilter)
193 .bind("typeloc"),
194 this);
195 Finder->addMatcher(
196 usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(TestCaseTypeAlias)))
197 .bind("using"),
198 this);
199 }
200
getNewMethodName(llvm::StringRef CurrentName)201 static llvm::StringRef getNewMethodName(llvm::StringRef CurrentName) {
202 std::pair<llvm::StringRef, llvm::StringRef> ReplacementMap[] = {
203 {"SetUpTestCase", "SetUpTestSuite"},
204 {"TearDownTestCase", "TearDownTestSuite"},
205 {"test_case_name", "test_suite_name"},
206 {"OnTestCaseStart", "OnTestSuiteStart"},
207 {"OnTestCaseEnd", "OnTestSuiteEnd"},
208 {"current_test_case", "current_test_suite"},
209 {"successful_test_case_count", "successful_test_suite_count"},
210 {"failed_test_case_count", "failed_test_suite_count"},
211 {"total_test_case_count", "total_test_suite_count"},
212 {"test_case_to_run_count", "test_suite_to_run_count"},
213 {"GetTestCase", "GetTestSuite"}};
214
215 for (auto &Mapping : ReplacementMap) {
216 if (CurrentName == Mapping.first)
217 return Mapping.second;
218 }
219
220 llvm_unreachable("Unexpected function name");
221 }
222
223 template <typename NodeType>
isInInstantiation(const NodeType & Node,const MatchFinder::MatchResult & Result)224 static bool isInInstantiation(const NodeType &Node,
225 const MatchFinder::MatchResult &Result) {
226 return !match(isInTemplateInstantiation(), Node, *Result.Context).empty();
227 }
228
229 template <typename NodeType>
isInTemplate(const NodeType & Node,const MatchFinder::MatchResult & Result)230 static bool isInTemplate(const NodeType &Node,
231 const MatchFinder::MatchResult &Result) {
232 internal::Matcher<NodeType> IsInsideTemplate =
233 hasAncestor(decl(anyOf(classTemplateDecl(), functionTemplateDecl())));
234 return !match(IsInsideTemplate, Node, *Result.Context).empty();
235 }
236
237 static bool
derivedTypeHasReplacementMethod(const MatchFinder::MatchResult & Result,llvm::StringRef ReplacementMethod)238 derivedTypeHasReplacementMethod(const MatchFinder::MatchResult &Result,
239 llvm::StringRef ReplacementMethod) {
240 const auto *Class = Result.Nodes.getNodeAs<CXXRecordDecl>("class");
241 return !match(cxxRecordDecl(
242 unless(isExpansionInFileMatching(
243 "gtest/gtest(-typed-test)?\\.h$")),
244 hasMethod(cxxMethodDecl(hasName(ReplacementMethod)))),
245 *Class, *Result.Context)
246 .empty();
247 }
248
249 static CharSourceRange
getAliasNameRange(const MatchFinder::MatchResult & Result)250 getAliasNameRange(const MatchFinder::MatchResult &Result) {
251 if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>("using")) {
252 return CharSourceRange::getTokenRange(
253 Using->getNameInfo().getSourceRange());
254 }
255 return CharSourceRange::getTokenRange(
256 Result.Nodes.getNodeAs<TypeLoc>("typeloc")->getSourceRange());
257 }
258
check(const MatchFinder::MatchResult & Result)259 void UpgradeGoogletestCaseCheck::check(const MatchFinder::MatchResult &Result) {
260 llvm::StringRef ReplacementText;
261 CharSourceRange ReplacementRange;
262 if (const auto *Method = Result.Nodes.getNodeAs<CXXMethodDecl>("method")) {
263 ReplacementText = getNewMethodName(Method->getName());
264
265 bool IsInInstantiation;
266 bool IsInTemplate;
267 bool AddFix = true;
268 if (const auto *Call = Result.Nodes.getNodeAs<CXXMemberCallExpr>("call")) {
269 const auto *Callee = llvm::cast<MemberExpr>(Call->getCallee());
270 ReplacementRange = CharSourceRange::getTokenRange(Callee->getMemberLoc(),
271 Callee->getMemberLoc());
272 IsInInstantiation = isInInstantiation(*Call, Result);
273 IsInTemplate = isInTemplate<Stmt>(*Call, Result);
274 } else if (const auto *Ref = Result.Nodes.getNodeAs<DeclRefExpr>("ref")) {
275 ReplacementRange =
276 CharSourceRange::getTokenRange(Ref->getNameInfo().getSourceRange());
277 IsInInstantiation = isInInstantiation(*Ref, Result);
278 IsInTemplate = isInTemplate<Stmt>(*Ref, Result);
279 } else if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>("using")) {
280 ReplacementRange =
281 CharSourceRange::getTokenRange(Using->getNameInfo().getSourceRange());
282 IsInInstantiation = isInInstantiation(*Using, Result);
283 IsInTemplate = isInTemplate<Decl>(*Using, Result);
284 } else {
285 // This branch means we have matched a function declaration / definition
286 // either for a function from googletest or for a function in a derived
287 // class.
288
289 ReplacementRange = CharSourceRange::getTokenRange(
290 Method->getNameInfo().getSourceRange());
291 IsInInstantiation = isInInstantiation(*Method, Result);
292 IsInTemplate = isInTemplate<Decl>(*Method, Result);
293
294 // If the type of the matched method is strictly derived from a googletest
295 // type and has both the old and new member function names, then we cannot
296 // safely rename (or delete) the old name version.
297 AddFix = !derivedTypeHasReplacementMethod(Result, ReplacementText);
298 }
299
300 if (IsInInstantiation) {
301 if (MatchedTemplateLocations.count(ReplacementRange.getBegin()) == 0) {
302 // For each location matched in a template instantiation, we check if
303 // the location can also be found in `MatchedTemplateLocations`. If it
304 // is not found, that means the expression did not create a match
305 // without the instantiation and depends on template parameters. A
306 // manual fix is probably required so we provide only a warning.
307 diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage);
308 }
309 return;
310 }
311
312 if (IsInTemplate) {
313 // We gather source locations from template matches not in template
314 // instantiations for future matches.
315 MatchedTemplateLocations.insert(ReplacementRange.getBegin());
316 }
317
318 if (!AddFix) {
319 diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage);
320 return;
321 }
322 } else {
323 // This is a match for `TestCase` to `TestSuite` refactoring.
324 assert(Result.Nodes.getNodeAs<TypeAliasDecl>("test-case") != nullptr);
325 ReplacementText = "TestSuite";
326 ReplacementRange = getAliasNameRange(Result);
327
328 // We do not need to keep track of template instantiations for this branch,
329 // because we are matching a `TypeLoc` for the alias declaration. Templates
330 // will only be instantiated with the true type name, `TestSuite`.
331 }
332
333 DiagnosticBuilder Diag =
334 diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage);
335
336 ReplacementRange = Lexer::makeFileCharRange(
337 ReplacementRange, *Result.SourceManager, Result.Context->getLangOpts());
338 if (ReplacementRange.isInvalid())
339 // An invalid source range likely means we are inside a macro body. A manual
340 // fix is likely needed so we do not create a fix-it hint.
341 return;
342
343 Diag << FixItHint::CreateReplacement(ReplacementRange, ReplacementText);
344 }
345
346 } // namespace google
347 } // namespace tidy
348 } // namespace clang
349