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> |
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: |
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 | |
89 | private: |
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 | |
122 | void UpgradeGoogletestCaseCheck::registerPPCallbacks(const SourceManager &, |
123 | Preprocessor *PP, |
124 | Preprocessor *) { |
125 | PP->addPPCallbacks( |
126 | C: std::make_unique<UpgradeGoogletestCasePPCallback>(args: this, args&: PP)); |
127 | } |
128 | |
129 | void 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 | |
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> |
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> |
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 |
243 | derivedTypeHasReplacementMethod(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 | |
254 | static CharSourceRange |
255 | getAliasNameRange(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 | |
264 | void 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 | |