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