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