1 //===--- UpgradeGoogletestCaseCheck.cpp - clang-tidy ----------------------===//
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
7 //===----------------------------------------------------------------------===//
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"
16 using namespace clang::ast_matchers
;
18 namespace clang::tidy::google
{
20 static const llvm::StringRef RenameCaseToSuiteMessage
=
21 "Google Test APIs named with 'case' are deprecated; use equivalent APIs "
24 static std::optional
<llvm::StringRef
>
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"},
34 for (auto &Mapping
: ReplacementMap
) {
35 if (MacroName
== Mapping
.first
)
36 return Mapping
.second
;
44 class UpgradeGoogletestCasePPCallback
: public PPCallbacks
{
46 UpgradeGoogletestCasePPCallback(UpgradeGoogletestCaseCheck
*Check
,
48 : Check(Check
), PP(PP
) {}
50 void MacroExpands(const Token
&MacroNameTok
, const MacroDefinition
&MD
,
51 SourceRange Range
, const MacroArgs
*) override
{
52 macroUsed(MacroNameTok
, MD
, Range
.getBegin(), CheckAction::Rename
);
55 void MacroUndefined(const Token
&MacroNameTok
, const MacroDefinition
&MD
,
56 const MacroDirective
*Undef
) override
{
58 macroUsed(MacroNameTok
, MD
, Undef
->getLocation(), CheckAction::Warn
);
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
.endswith("gtest/gtest-typed-test.h") &&
70 PP
->getSpelling(MacroNameTok
) == "TYPED_TEST_SUITE";
74 void Defined(const Token
&MacroNameTok
, const MacroDefinition
&MD
,
75 SourceRange Range
) override
{
76 macroUsed(MacroNameTok
, MD
, Range
.getBegin(), CheckAction::Warn
);
79 void Ifdef(SourceLocation Loc
, const Token
&MacroNameTok
,
80 const MacroDefinition
&MD
) override
{
81 macroUsed(MacroNameTok
, MD
, Loc
, CheckAction::Warn
);
84 void Ifndef(SourceLocation Loc
, const Token
&MacroNameTok
,
85 const MacroDefinition
&MD
) override
{
86 macroUsed(MacroNameTok
, MD
, Loc
, CheckAction::Warn
);
90 enum class CheckAction
{ Warn
, Rename
};
92 void macroUsed(const clang::Token
&MacroNameTok
, const MacroDefinition
&MD
,
93 SourceLocation Loc
, CheckAction Action
) {
94 if (!ReplacementFound
)
97 std::string Name
= PP
->getSpelling(MacroNameTok
);
99 std::optional
<llvm::StringRef
> Replacement
= getNewMacroName(Name
);
103 llvm::StringRef FileName
= PP
->getSourceManager().getFilename(
104 MD
.getMacroInfo()->getDefinitionLoc());
105 if (!FileName
.endswith("gtest/gtest-typed-test.h"))
108 DiagnosticBuilder Diag
= Check
->diag(Loc
, RenameCaseToSuiteMessage
);
110 if (Action
== CheckAction::Rename
)
111 Diag
<< FixItHint::CreateReplacement(
112 CharSourceRange::getTokenRange(Loc
, Loc
), *Replacement
);
115 bool ReplacementFound
= false;
116 UpgradeGoogletestCaseCheck
*Check
;
122 void UpgradeGoogletestCaseCheck::registerPPCallbacks(const SourceManager
&,
126 std::make_unique
<UpgradeGoogletestCasePPCallback
>(this, PP
));
129 void UpgradeGoogletestCaseCheck::registerMatchers(MatchFinder
*Finder
) {
130 auto LocationFilter
=
131 unless(isExpansionInFileMatching("gtest/gtest(-typed-test)?\\.h$"));
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.
141 hasAnyName("SetUpTestCase", "TearDownTestCase"),
143 cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl(
144 hasName("::testing::Test"),
145 hasMethod(hasName("SetUpTestSuite")))))
148 hasName("test_case_name"),
150 cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl(
151 hasName("::testing::TestInfo"),
152 hasMethod(hasName("test_suite_name")))))
155 hasAnyName("OnTestCaseStart", "OnTestCaseEnd"),
156 ofClass(cxxRecordDecl(
157 isSameOrDerivedFrom(cxxRecordDecl(
158 hasName("::testing::TestEventListener"),
159 hasMethod(hasName("OnTestSuiteStart")))))
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")))))
172 Finder
->addMatcher(expr(anyOf(callExpr(callee(Methods
)).bind("call"),
173 declRefExpr(to(Methods
)).bind("ref")),
178 usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(Methods
)), LocationFilter
)
182 Finder
->addMatcher(cxxMethodDecl(Methods
, LocationFilter
), this);
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");
190 typeLoc(loc(qualType(typedefType(hasDeclaration(TestCaseTypeAlias
)))),
191 unless(hasAncestor(decl(isImplicit()))), LocationFilter
)
195 usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(TestCaseTypeAlias
)))
199 typeLoc(loc(usingType(hasUnderlyingType(
200 typedefType(hasDeclaration(TestCaseTypeAlias
))))),
201 unless(hasAncestor(decl(isImplicit()))), LocationFilter
)
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"}};
220 for (auto &Mapping
: ReplacementMap
) {
221 if (CurrentName
== Mapping
.first
)
222 return Mapping
.second
;
225 llvm_unreachable("Unexpected function name");
228 template <typename NodeType
>
229 static bool isInInstantiation(const NodeType
&Node
,
230 const MatchFinder::MatchResult
&Result
) {
231 return !match(isInTemplateInstantiation(), Node
, *Result
.Context
).empty();
234 template <typename NodeType
>
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();
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
)
254 static CharSourceRange
255 getAliasNameRange(const MatchFinder::MatchResult
&Result
) {
256 if (const auto *Using
= Result
.Nodes
.getNodeAs
<UsingDecl
>("using")) {
257 return CharSourceRange::getTokenRange(
258 Using
->getNameInfo().getSourceRange());
260 return CharSourceRange::getTokenRange(
261 Result
.Nodes
.getNodeAs
<TypeLoc
>("typeloc")->getSourceRange());
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());
270 bool IsInInstantiation
= false;
271 bool IsInTemplate
= false;
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")) {
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")) {
286 CharSourceRange::getTokenRange(Using
->getNameInfo().getSourceRange());
287 IsInInstantiation
= isInInstantiation(*Using
, Result
);
288 IsInTemplate
= isInTemplate
<Decl
>(*Using
, Result
);
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
294 ReplacementRange
= CharSourceRange::getTokenRange(
295 Method
->getNameInfo().getSourceRange());
296 IsInInstantiation
= isInInstantiation(*Method
, Result
);
297 IsInTemplate
= isInTemplate
<Decl
>(*Method
, Result
);
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
);
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
);
318 // We gather source locations from template matches not in template
319 // instantiations for future matches.
320 MatchedTemplateLocations
.insert(ReplacementRange
.getBegin());
324 diag(ReplacementRange
.getBegin(), RenameCaseToSuiteMessage
);
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
);
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`.
338 DiagnosticBuilder Diag
=
339 diag(ReplacementRange
.getBegin(), RenameCaseToSuiteMessage
);
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.
348 Diag
<< FixItHint::CreateReplacement(ReplacementRange
, ReplacementText
);
351 } // namespace clang::tidy::google