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
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 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
42namespace {
43
44class UpgradeGoogletestCasePPCallback : public PPCallbacks {
45public:
46 UpgradeGoogletestCasePPCallback(UpgradeGoogletestCaseCheck *Check,
47 Preprocessor *PP)
48 : Check(Check), PP(PP) {}
49
50 void MacroExpands(const Token &MacroNameTok, const MacroDefinition &MD,
51 SourceRange Range, const MacroArgs *) override {
52 macroUsed(MacroNameTok, MD, Loc: Range.getBegin(), Action: CheckAction::Rename);
53 }
54
55 void MacroUndefined(const Token &MacroNameTok, const MacroDefinition &MD,
56 const MacroDirective *Undef) override {
57 if (Undef != nullptr)
58 macroUsed(MacroNameTok, MD, Loc: Undef->getLocation(), Action: CheckAction::Warn);
59 }
60
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 SpellingLoc: MD->getMacroInfo()->getDefinitionLoc());
69 ReplacementFound = FileName.ends_with(Suffix: "gtest/gtest-typed-test.h") &&
70 PP->getSpelling(Tok: MacroNameTok) == "TYPED_TEST_SUITE";
71 }
72 }
73
74 void Defined(const Token &MacroNameTok, const MacroDefinition &MD,
75 SourceRange Range) override {
76 macroUsed(MacroNameTok, MD, Loc: Range.getBegin(), Action: CheckAction::Warn);
77 }
78
79 void Ifdef(SourceLocation Loc, const Token &MacroNameTok,
80 const MacroDefinition &MD) override {
81 macroUsed(MacroNameTok, MD, Loc, Action: CheckAction::Warn);
82 }
83
84 void Ifndef(SourceLocation Loc, const Token &MacroNameTok,
85 const MacroDefinition &MD) override {
86 macroUsed(MacroNameTok, MD, Loc, Action: CheckAction::Warn);
87 }
88
89private:
90 enum class CheckAction { Warn, Rename };
91
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(Tok: MacroNameTok);
98
99 std::optional<llvm::StringRef> Replacement = getNewMacroName(MacroName: Name);
100 if (!Replacement)
101 return;
102
103 llvm::StringRef FileName = PP->getSourceManager().getFilename(
104 SpellingLoc: MD.getMacroInfo()->getDefinitionLoc());
105 if (!FileName.ends_with(Suffix: "gtest/gtest-typed-test.h"))
106 return;
107
108 DiagnosticBuilder Diag = Check->diag(Loc, Description: RenameCaseToSuiteMessage);
109
110 if (Action == CheckAction::Rename)
111 Diag << FixItHint::CreateReplacement(
112 RemoveRange: CharSourceRange::getTokenRange(B: Loc, E: Loc), Code: *Replacement);
113 }
114
115 bool ReplacementFound = false;
116 UpgradeGoogletestCaseCheck *Check;
117 Preprocessor *PP;
118};
119
120} // namespace
121
122void UpgradeGoogletestCaseCheck::registerPPCallbacks(const SourceManager &,
123 Preprocessor *PP,
124 Preprocessor *) {
125 PP->addPPCallbacks(
126 C: std::make_unique<UpgradeGoogletestCasePPCallback>(args: this, args&: PP));
127}
128
129void UpgradeGoogletestCaseCheck::registerMatchers(MatchFinder *Finder) {
130 auto LocationFilter =
131 unless(isExpansionInFileMatching(RegExp: "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 InnerMatcher: cxxRecordDecl(isSameOrDerivedFrom(Base: cxxRecordDecl(
144 hasName(Name: "::testing::Test"),
145 hasMethod(InnerMatcher: hasName(Name: "SetUpTestSuite")))))
146 .bind(ID: "class"))),
147 cxxMethodDecl(
148 hasName(Name: "test_case_name"),
149 ofClass(
150 InnerMatcher: cxxRecordDecl(isSameOrDerivedFrom(Base: cxxRecordDecl(
151 hasName(Name: "::testing::TestInfo"),
152 hasMethod(InnerMatcher: hasName(Name: "test_suite_name")))))
153 .bind(ID: "class"))),
154 cxxMethodDecl(
155 hasAnyName("OnTestCaseStart", "OnTestCaseEnd"),
156 ofClass(InnerMatcher: cxxRecordDecl(
157 isSameOrDerivedFrom(Base: cxxRecordDecl(
158 hasName(Name: "::testing::TestEventListener"),
159 hasMethod(InnerMatcher: hasName(Name: "OnTestSuiteStart")))))
160 .bind(ID: "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(InnerMatcher: cxxRecordDecl(
166 isSameOrDerivedFrom(Base: cxxRecordDecl(
167 hasName(Name: "::testing::UnitTest"),
168 hasMethod(InnerMatcher: hasName(Name: "current_test_suite")))))
169 .bind(ID: "class")))))
170 .bind(ID: "method");
171
172 Finder->addMatcher(NodeMatch: expr(anyOf(callExpr(callee(InnerMatcher: Methods)).bind(ID: "call"),
173 declRefExpr(to(InnerMatcher: Methods)).bind(ID: "ref")),
174 LocationFilter),
175 Action: this);
176
177 Finder->addMatcher(
178 NodeMatch: usingDecl(hasAnyUsingShadowDecl(InnerMatcher: hasTargetDecl(InnerMatcher: Methods)), LocationFilter)
179 .bind(ID: "using"),
180 Action: this);
181
182 Finder->addMatcher(NodeMatch: cxxMethodDecl(Methods, LocationFilter), Action: 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(Name: "::testing::TestCase")).bind(ID: "test-case");
189 Finder->addMatcher(
190 NodeMatch: typeLoc(loc(InnerMatcher: qualType(typedefType(hasDeclaration(InnerMatcher: TestCaseTypeAlias)))),
191 unless(hasAncestor(decl(isImplicit()))), LocationFilter)
192 .bind(ID: "typeloc"),
193 Action: this);
194 Finder->addMatcher(
195 NodeMatch: usingDecl(hasAnyUsingShadowDecl(InnerMatcher: hasTargetDecl(InnerMatcher: TestCaseTypeAlias)))
196 .bind(ID: "using"),
197 Action: this);
198 Finder->addMatcher(
199 NodeMatch: typeLoc(loc(InnerMatcher: usingType(hasUnderlyingType(
200 typedefType(hasDeclaration(InnerMatcher: TestCaseTypeAlias))))),
201 unless(hasAncestor(decl(isImplicit()))), LocationFilter)
202 .bind(ID: "typeloc"),
203 Action: this);
204}
205
206static 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
228template <typename NodeType>
229static bool isInInstantiation(const NodeType &Node,
230 const MatchFinder::MatchResult &Result) {
231 return !match(isInTemplateInstantiation(), Node, *Result.Context).empty();
232}
233
234template <typename NodeType>
235static 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
242static bool
243derivedTypeHasReplacementMethod(const MatchFinder::MatchResult &Result,
244 llvm::StringRef ReplacementMethod) {
245 const auto *Class = Result.Nodes.getNodeAs<CXXRecordDecl>(ID: "class");
246 return !match(Matcher: cxxRecordDecl(
247 unless(isExpansionInFileMatching(
248 RegExp: "gtest/gtest(-typed-test)?\\.h$")),
249 hasMethod(InnerMatcher: cxxMethodDecl(hasName(Name: ReplacementMethod)))),
250 Node: *Class, Context&: *Result.Context)
251 .empty();
252}
253
254static CharSourceRange
255getAliasNameRange(const MatchFinder::MatchResult &Result) {
256 if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>(ID: "using")) {
257 return CharSourceRange::getTokenRange(
258 R: Using->getNameInfo().getSourceRange());
259 }
260 return CharSourceRange::getTokenRange(
261 R: Result.Nodes.getNodeAs<TypeLoc>(ID: "typeloc")->getSourceRange());
262}
263
264void UpgradeGoogletestCaseCheck::check(const MatchFinder::MatchResult &Result) {
265 llvm::StringRef ReplacementText;
266 CharSourceRange ReplacementRange;
267 if (const auto *Method = Result.Nodes.getNodeAs<CXXMethodDecl>(ID: "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>(ID: "call")) {
274 const auto *Callee = llvm::cast<MemberExpr>(Call->getCallee());
275 ReplacementRange = CharSourceRange::getTokenRange(Callee->getMemberLoc(),
276 Callee->getMemberLoc());
277 IsInInstantiation = isInInstantiation(Node: *Call, Result);
278 IsInTemplate = isInTemplate<Stmt>(*Call, Result);
279 } else if (const auto *Ref = Result.Nodes.getNodeAs<DeclRefExpr>(ID: "ref")) {
280 ReplacementRange =
281 CharSourceRange::getTokenRange(R: Ref->getNameInfo().getSourceRange());
282 IsInInstantiation = isInInstantiation(Node: *Ref, Result);
283 IsInTemplate = isInTemplate<Stmt>(*Ref, Result);
284 } else if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>(ID: "using")) {
285 ReplacementRange =
286 CharSourceRange::getTokenRange(R: Using->getNameInfo().getSourceRange());
287 IsInInstantiation = isInInstantiation(Node: *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(Node: *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, ReplacementMethod: ReplacementText);
303 }
304
305 if (IsInInstantiation) {
306 if (MatchedTemplateLocations.count(V: 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(Loc: ReplacementRange.getBegin(), Description: 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(V: ReplacementRange.getBegin());
321 }
322
323 if (!AddFix) {
324 diag(Loc: ReplacementRange.getBegin(), Description: 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(Loc: ReplacementRange.getBegin(), Description: RenameCaseToSuiteMessage);
340
341 ReplacementRange = Lexer::makeFileCharRange(
342 Range: ReplacementRange, SM: *Result.SourceManager, LangOpts: 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(RemoveRange: ReplacementRange, Code: ReplacementText);
349}
350
351} // namespace clang::tidy::google
352

source code of clang-tools-extra/clang-tidy/google/UpgradeGoogletestCaseCheck.cpp