123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- //===--- 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"
- #include <optional>
- using namespace clang::ast_matchers;
- namespace clang::tidy::google {
- static const llvm::StringRef RenameCaseToSuiteMessage =
- "Google Test APIs named with 'case' are deprecated; use equivalent APIs "
- "named with 'suite'";
- static std::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 std::nullopt;
- }
- 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);
- std::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);
- Finder->addMatcher(
- typeLoc(loc(usingType(hasUnderlyingType(
- typedefType(hasDeclaration(TestCaseTypeAlias))))),
- unless(hasAncestor(decl(isImplicit()))), LocationFilter)
- .bind("typeloc"),
- 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 clang::tidy::google
|