| //===--- UpgradeGoogletestCaseCheck.cpp - clang-tidy ----------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "UpgradeGoogletestCaseCheck.h" |
| #include "clang/AST/ASTContext.h" |
| #include "clang/ASTMatchers/ASTMatchFinder.h" |
| #include "clang/Lex/PPCallbacks.h" |
| #include "clang/Lex/Preprocessor.h" |
| |
| using namespace clang::ast_matchers; |
| |
| namespace clang { |
| namespace tidy { |
| namespace google { |
| |
| static const llvm::StringRef RenameCaseToSuiteMessage = |
| "Google Test APIs named with 'case' are deprecated; use equivalent APIs " |
| "named with 'suite'"; |
| |
| static llvm::Optional<llvm::StringRef> |
| getNewMacroName(llvm::StringRef MacroName) { |
| std::pair<llvm::StringRef, llvm::StringRef> ReplacementMap[] = { |
| {"TYPED_TEST_CASE", "TYPED_TEST_SUITE"}, |
| {"TYPED_TEST_CASE_P", "TYPED_TEST_SUITE_P"}, |
| {"REGISTER_TYPED_TEST_CASE_P", "REGISTER_TYPED_TEST_SUITE_P"}, |
| {"INSTANTIATE_TYPED_TEST_CASE_P", "INSTANTIATE_TYPED_TEST_SUITE_P"}, |
| {"INSTANTIATE_TEST_CASE_P", "INSTANTIATE_TEST_SUITE_P"}, |
| }; |
| |
| for (auto &Mapping : ReplacementMap) { |
| if (MacroName == Mapping.first) |
| return Mapping.second; |
| } |
| |
| return llvm::None; |
| } |
| |
| namespace { |
| |
| class UpgradeGoogletestCasePPCallback : public PPCallbacks { |
| public: |
| UpgradeGoogletestCasePPCallback(UpgradeGoogletestCaseCheck *Check, |
| Preprocessor *PP) |
| : ReplacementFound(false), Check(Check), PP(PP) {} |
| |
| void MacroExpands(const Token &MacroNameTok, const MacroDefinition &MD, |
| SourceRange Range, const MacroArgs *) override { |
| macroUsed(MacroNameTok, MD, Range.getBegin(), CheckAction::Rename); |
| } |
| |
| void MacroUndefined(const Token &MacroNameTok, const MacroDefinition &MD, |
| const MacroDirective *Undef) override { |
| if (Undef != nullptr) |
| macroUsed(MacroNameTok, MD, Undef->getLocation(), CheckAction::Warn); |
| } |
| |
| void MacroDefined(const Token &MacroNameTok, |
| const MacroDirective *MD) override { |
| if (!ReplacementFound && MD != nullptr) { |
| // We check if the newly defined macro is one of the target replacements. |
| // This ensures that the check creates warnings only if it is including a |
| // recent enough version of Google Test. |
| llvm::StringRef FileName = PP->getSourceManager().getFilename( |
| MD->getMacroInfo()->getDefinitionLoc()); |
| ReplacementFound = FileName.endswith("gtest/gtest-typed-test.h") && |
| PP->getSpelling(MacroNameTok) == "TYPED_TEST_SUITE"; |
| } |
| } |
| |
| void Defined(const Token &MacroNameTok, const MacroDefinition &MD, |
| SourceRange Range) override { |
| macroUsed(MacroNameTok, MD, Range.getBegin(), CheckAction::Warn); |
| } |
| |
| void Ifdef(SourceLocation Loc, const Token &MacroNameTok, |
| const MacroDefinition &MD) override { |
| macroUsed(MacroNameTok, MD, Loc, CheckAction::Warn); |
| } |
| |
| void Ifndef(SourceLocation Loc, const Token &MacroNameTok, |
| const MacroDefinition &MD) override { |
| macroUsed(MacroNameTok, MD, Loc, CheckAction::Warn); |
| } |
| |
| private: |
| enum class CheckAction { Warn, Rename }; |
| |
| void macroUsed(const clang::Token &MacroNameTok, const MacroDefinition &MD, |
| SourceLocation Loc, CheckAction Action) { |
| if (!ReplacementFound) |
| return; |
| |
| std::string Name = PP->getSpelling(MacroNameTok); |
| |
| llvm::Optional<llvm::StringRef> Replacement = getNewMacroName(Name); |
| if (!Replacement) |
| return; |
| |
| llvm::StringRef FileName = PP->getSourceManager().getFilename( |
| MD.getMacroInfo()->getDefinitionLoc()); |
| if (!FileName.endswith("gtest/gtest-typed-test.h")) |
| return; |
| |
| DiagnosticBuilder Diag = Check->diag(Loc, RenameCaseToSuiteMessage); |
| |
| if (Action == CheckAction::Rename) |
| Diag << FixItHint::CreateReplacement( |
| CharSourceRange::getTokenRange(Loc, Loc), *Replacement); |
| } |
| |
| bool ReplacementFound; |
| UpgradeGoogletestCaseCheck *Check; |
| Preprocessor *PP; |
| }; |
| |
| } // namespace |
| |
| void UpgradeGoogletestCaseCheck::registerPPCallbacks(const SourceManager &, |
| Preprocessor *PP, |
| Preprocessor *) { |
| PP->addPPCallbacks( |
| std::make_unique<UpgradeGoogletestCasePPCallback>(this, PP)); |
| } |
| |
| void UpgradeGoogletestCaseCheck::registerMatchers(MatchFinder *Finder) { |
| auto LocationFilter = |
| unless(isExpansionInFileMatching("gtest/gtest(-typed-test)?\\.h$")); |
| |
| // Matchers for the member functions that are being renamed. In each matched |
| // Google Test class, we check for the existence of one new method name. This |
| // makes sure the check gives warnings only if the included version of Google |
| // Test is recent enough. |
| auto Methods = |
| cxxMethodDecl( |
| anyOf( |
| cxxMethodDecl( |
| hasAnyName("SetUpTestCase", "TearDownTestCase"), |
| ofClass( |
| cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl( |
| hasName("::testing::Test"), |
| hasMethod(hasName("SetUpTestSuite"))))) |
| .bind("class"))), |
| cxxMethodDecl( |
| hasName("test_case_name"), |
| ofClass( |
| cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl( |
| hasName("::testing::TestInfo"), |
| hasMethod(hasName("test_suite_name"))))) |
| .bind("class"))), |
| cxxMethodDecl( |
| hasAnyName("OnTestCaseStart", "OnTestCaseEnd"), |
| ofClass(cxxRecordDecl( |
| isSameOrDerivedFrom(cxxRecordDecl( |
| hasName("::testing::TestEventListener"), |
| hasMethod(hasName("OnTestSuiteStart"))))) |
| .bind("class"))), |
| cxxMethodDecl( |
| hasAnyName("current_test_case", "successful_test_case_count", |
| "failed_test_case_count", "total_test_case_count", |
| "test_case_to_run_count", "GetTestCase"), |
| ofClass(cxxRecordDecl( |
| isSameOrDerivedFrom(cxxRecordDecl( |
| hasName("::testing::UnitTest"), |
| hasMethod(hasName("current_test_suite"))))) |
| .bind("class"))))) |
| .bind("method"); |
| |
| Finder->addMatcher(expr(anyOf(callExpr(callee(Methods)).bind("call"), |
| declRefExpr(to(Methods)).bind("ref")), |
| LocationFilter), |
| this); |
| |
| Finder->addMatcher( |
| usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(Methods)), LocationFilter) |
| .bind("using"), |
| this); |
| |
| Finder->addMatcher(cxxMethodDecl(Methods, LocationFilter), this); |
| |
| // Matchers for `TestCase` -> `TestSuite`. The fact that `TestCase` is an |
| // alias and not a class declaration ensures we only match with a recent |
| // enough version of Google Test. |
| auto TestCaseTypeAlias = |
| typeAliasDecl(hasName("::testing::TestCase")).bind("test-case"); |
| Finder->addMatcher( |
| typeLoc(loc(qualType(typedefType(hasDeclaration(TestCaseTypeAlias)))), |
| unless(hasAncestor(decl(isImplicit()))), LocationFilter) |
| .bind("typeloc"), |
| this); |
| Finder->addMatcher( |
| usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(TestCaseTypeAlias))) |
| .bind("using"), |
| this); |
| } |
| |
| static llvm::StringRef getNewMethodName(llvm::StringRef CurrentName) { |
| std::pair<llvm::StringRef, llvm::StringRef> ReplacementMap[] = { |
| {"SetUpTestCase", "SetUpTestSuite"}, |
| {"TearDownTestCase", "TearDownTestSuite"}, |
| {"test_case_name", "test_suite_name"}, |
| {"OnTestCaseStart", "OnTestSuiteStart"}, |
| {"OnTestCaseEnd", "OnTestSuiteEnd"}, |
| {"current_test_case", "current_test_suite"}, |
| {"successful_test_case_count", "successful_test_suite_count"}, |
| {"failed_test_case_count", "failed_test_suite_count"}, |
| {"total_test_case_count", "total_test_suite_count"}, |
| {"test_case_to_run_count", "test_suite_to_run_count"}, |
| {"GetTestCase", "GetTestSuite"}}; |
| |
| for (auto &Mapping : ReplacementMap) { |
| if (CurrentName == Mapping.first) |
| return Mapping.second; |
| } |
| |
| llvm_unreachable("Unexpected function name"); |
| } |
| |
| template <typename NodeType> |
| static bool isInInstantiation(const NodeType &Node, |
| const MatchFinder::MatchResult &Result) { |
| return !match(isInTemplateInstantiation(), Node, *Result.Context).empty(); |
| } |
| |
| template <typename NodeType> |
| static bool isInTemplate(const NodeType &Node, |
| const MatchFinder::MatchResult &Result) { |
| internal::Matcher<NodeType> IsInsideTemplate = |
| hasAncestor(decl(anyOf(classTemplateDecl(), functionTemplateDecl()))); |
| return !match(IsInsideTemplate, Node, *Result.Context).empty(); |
| } |
| |
| static bool |
| derivedTypeHasReplacementMethod(const MatchFinder::MatchResult &Result, |
| llvm::StringRef ReplacementMethod) { |
| const auto *Class = Result.Nodes.getNodeAs<CXXRecordDecl>("class"); |
| return !match(cxxRecordDecl( |
| unless(isExpansionInFileMatching( |
| "gtest/gtest(-typed-test)?\\.h$")), |
| hasMethod(cxxMethodDecl(hasName(ReplacementMethod)))), |
| *Class, *Result.Context) |
| .empty(); |
| } |
| |
| static CharSourceRange |
| getAliasNameRange(const MatchFinder::MatchResult &Result) { |
| if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>("using")) { |
| return CharSourceRange::getTokenRange( |
| Using->getNameInfo().getSourceRange()); |
| } |
| return CharSourceRange::getTokenRange( |
| Result.Nodes.getNodeAs<TypeLoc>("typeloc")->getSourceRange()); |
| } |
| |
| void UpgradeGoogletestCaseCheck::check(const MatchFinder::MatchResult &Result) { |
| llvm::StringRef ReplacementText; |
| CharSourceRange ReplacementRange; |
| if (const auto *Method = Result.Nodes.getNodeAs<CXXMethodDecl>("method")) { |
| ReplacementText = getNewMethodName(Method->getName()); |
| |
| bool IsInInstantiation; |
| bool IsInTemplate; |
| bool AddFix = true; |
| if (const auto *Call = Result.Nodes.getNodeAs<CXXMemberCallExpr>("call")) { |
| const auto *Callee = llvm::cast<MemberExpr>(Call->getCallee()); |
| ReplacementRange = CharSourceRange::getTokenRange(Callee->getMemberLoc(), |
| Callee->getMemberLoc()); |
| IsInInstantiation = isInInstantiation(*Call, Result); |
| IsInTemplate = isInTemplate<Stmt>(*Call, Result); |
| } else if (const auto *Ref = Result.Nodes.getNodeAs<DeclRefExpr>("ref")) { |
| ReplacementRange = |
| CharSourceRange::getTokenRange(Ref->getNameInfo().getSourceRange()); |
| IsInInstantiation = isInInstantiation(*Ref, Result); |
| IsInTemplate = isInTemplate<Stmt>(*Ref, Result); |
| } else if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>("using")) { |
| ReplacementRange = |
| CharSourceRange::getTokenRange(Using->getNameInfo().getSourceRange()); |
| IsInInstantiation = isInInstantiation(*Using, Result); |
| IsInTemplate = isInTemplate<Decl>(*Using, Result); |
| } else { |
| // This branch means we have matched a function declaration / definition |
| // either for a function from googletest or for a function in a derived |
| // class. |
| |
| ReplacementRange = CharSourceRange::getTokenRange( |
| Method->getNameInfo().getSourceRange()); |
| IsInInstantiation = isInInstantiation(*Method, Result); |
| IsInTemplate = isInTemplate<Decl>(*Method, Result); |
| |
| // If the type of the matched method is strictly derived from a googletest |
| // type and has both the old and new member function names, then we cannot |
| // safely rename (or delete) the old name version. |
| AddFix = !derivedTypeHasReplacementMethod(Result, ReplacementText); |
| } |
| |
| if (IsInInstantiation) { |
| if (MatchedTemplateLocations.count(ReplacementRange.getBegin()) == 0) { |
| // For each location matched in a template instantiation, we check if |
| // the location can also be found in `MatchedTemplateLocations`. If it |
| // is not found, that means the expression did not create a match |
| // without the instantiation and depends on template parameters. A |
| // manual fix is probably required so we provide only a warning. |
| diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage); |
| } |
| return; |
| } |
| |
| if (IsInTemplate) { |
| // We gather source locations from template matches not in template |
| // instantiations for future matches. |
| MatchedTemplateLocations.insert(ReplacementRange.getBegin()); |
| } |
| |
| if (!AddFix) { |
| diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage); |
| return; |
| } |
| } else { |
| // This is a match for `TestCase` to `TestSuite` refactoring. |
| assert(Result.Nodes.getNodeAs<TypeAliasDecl>("test-case") != nullptr); |
| ReplacementText = "TestSuite"; |
| ReplacementRange = getAliasNameRange(Result); |
| |
| // We do not need to keep track of template instantiations for this branch, |
| // because we are matching a `TypeLoc` for the alias declaration. Templates |
| // will only be instantiated with the true type name, `TestSuite`. |
| } |
| |
| DiagnosticBuilder Diag = |
| diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage); |
| |
| ReplacementRange = Lexer::makeFileCharRange( |
| ReplacementRange, *Result.SourceManager, Result.Context->getLangOpts()); |
| if (ReplacementRange.isInvalid()) |
| // An invalid source range likely means we are inside a macro body. A manual |
| // fix is likely needed so we do not create a fix-it hint. |
| return; |
| |
| Diag << FixItHint::CreateReplacement(ReplacementRange, ReplacementText); |
| } |
| |
| } // namespace google |
| } // namespace tidy |
| } // namespace clang |