| 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 | |