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