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