brintos

brintos / llvm-project-archived public Read only

0
0
Text · 14.3 KiB · 87fd046 Raw
357 lines · cpp
1//===----------------------------------------------------------------------===//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-exception6//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, Range.getBegin(), 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, Undef->getLocation(), 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 a66      // recent enough version of Google Test.67      const llvm::StringRef FileName = PP->getSourceManager().getFilename(68          MD->getMacroInfo()->getDefinitionLoc());69      ReplacementFound = FileName.ends_with("gtest/gtest-typed-test.h") &&70                         PP->getSpelling(MacroNameTok) == "TYPED_TEST_SUITE";71    }72  }73 74  void Defined(const Token &MacroNameTok, const MacroDefinition &MD,75               SourceRange Range) override {76    macroUsed(MacroNameTok, MD, Range.getBegin(), CheckAction::Warn);77  }78 79  void Ifdef(SourceLocation Loc, const Token &MacroNameTok,80             const MacroDefinition &MD) override {81    macroUsed(MacroNameTok, MD, Loc, CheckAction::Warn);82  }83 84  void Ifndef(SourceLocation Loc, const Token &MacroNameTok,85              const MacroDefinition &MD) override {86    macroUsed(MacroNameTok, MD, Loc, 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    const std::string Name = PP->getSpelling(MacroNameTok);98 99    std::optional<llvm::StringRef> Replacement = getNewMacroName(Name);100    if (!Replacement)101      return;102 103    const llvm::StringRef FileName = PP->getSourceManager().getFilename(104        MD.getMacroInfo()->getDefinitionLoc());105    if (!FileName.ends_with("gtest/gtest-typed-test.h"))106      return;107 108    const DiagnosticBuilder Diag = Check->diag(Loc, RenameCaseToSuiteMessage);109 110    if (Action == CheckAction::Rename)111      Diag << FixItHint::CreateReplacement(112          CharSourceRange::getTokenRange(Loc, Loc), *Replacement);113  }114 115  bool ReplacementFound = false;116  UpgradeGoogletestCaseCheck *Check;117  Preprocessor *PP;118};119 120} // namespace121 122void UpgradeGoogletestCaseCheck::registerPPCallbacks(const SourceManager &,123                                                     Preprocessor *PP,124                                                     Preprocessor *) {125  PP->addPPCallbacks(126      std::make_unique<UpgradeGoogletestCasePPCallback>(this, PP));127}128 129void UpgradeGoogletestCaseCheck::registerMatchers(MatchFinder *Finder) {130  auto LocationFilter =131      unless(isExpansionInFileMatching("gtest/gtest(-typed-test)?\\.h$"));132 133  // Matchers for the member functions that are being renamed. In each matched134  // Google Test class, we check for the existence of one new method name. This135  // makes sure the check gives warnings only if the included version of Google136  // Test is recent enough.137  auto Methods =138      cxxMethodDecl(139          anyOf(140              cxxMethodDecl(141                  hasAnyName("SetUpTestCase", "TearDownTestCase"),142                  ofClass(143                      cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl(144                                        hasName("::testing::Test"),145                                        hasMethod(hasName("SetUpTestSuite")))))146                          .bind("class"))),147              cxxMethodDecl(148                  hasName("test_case_name"),149                  ofClass(150                      cxxRecordDecl(isSameOrDerivedFrom(cxxRecordDecl(151                                        hasName("::testing::TestInfo"),152                                        hasMethod(hasName("test_suite_name")))))153                          .bind("class"))),154              cxxMethodDecl(155                  hasAnyName("OnTestCaseStart", "OnTestCaseEnd"),156                  ofClass(cxxRecordDecl(157                              isSameOrDerivedFrom(cxxRecordDecl(158                                  hasName("::testing::TestEventListener"),159                                  hasMethod(hasName("OnTestSuiteStart")))))160                              .bind("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(cxxRecordDecl(166                              isSameOrDerivedFrom(cxxRecordDecl(167                                  hasName("::testing::UnitTest"),168                                  hasMethod(hasName("current_test_suite")))))169                              .bind("class")))))170          .bind("method");171 172  Finder->addMatcher(expr(anyOf(callExpr(callee(Methods)).bind("call"),173                                declRefExpr(to(Methods)).bind("ref")),174                          LocationFilter),175                     this);176 177  Finder->addMatcher(178      usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(Methods)), LocationFilter)179          .bind("using"),180      this);181 182  Finder->addMatcher(cxxMethodDecl(Methods, LocationFilter), this);183 184  // Matchers for `TestCase` -> `TestSuite`. The fact that `TestCase` is an185  // alias and not a class declaration ensures we only match with a recent186  // enough version of Google Test.187  auto TestCaseTypeAlias =188      typeAliasDecl(hasName("::testing::TestCase")).bind("test-case");189  Finder->addMatcher(190      typeLoc(loc(qualType(typedefType(hasDeclaration(TestCaseTypeAlias)))),191              unless(hasAncestor(decl(isImplicit()))), LocationFilter)192          .bind("typeloc"),193      this);194  Finder->addMatcher(195      usingDecl(hasAnyUsingShadowDecl(hasTargetDecl(TestCaseTypeAlias)))196          .bind("using"),197      this);198  Finder->addMatcher(199      typeLoc(loc(usingType(hasUnderlyingType(200                  typedefType(hasDeclaration(TestCaseTypeAlias))))),201              unless(hasAncestor(decl(isImplicit()))), LocationFilter)202          .bind("typeloc"),203      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  const internal::Matcher<NodeType> IsInsideTemplate =238      hasAncestor(decl(anyOf(classTemplateDecl(), functionTemplateDecl())));239  return !match(IsInsideTemplate, Node, *Result.Context).empty();240}241 242static bool243derivedTypeHasReplacementMethod(const MatchFinder::MatchResult &Result,244                                llvm::StringRef ReplacementMethod) {245  const auto *Class = Result.Nodes.getNodeAs<CXXRecordDecl>("class");246  return !match(cxxRecordDecl(247                    unless(isExpansionInFileMatching(248                        "gtest/gtest(-typed-test)?\\.h$")),249                    hasMethod(cxxMethodDecl(hasName(ReplacementMethod)))),250                *Class, *Result.Context)251              .empty();252}253 254static CharSourceRange255getAliasNameRange(const MatchFinder::MatchResult &Result) {256  if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>("using")) {257    return CharSourceRange::getTokenRange(258        Using->getNameInfo().getSourceRange());259  }260  TypeLoc TL = *Result.Nodes.getNodeAs<TypeLoc>("typeloc");261  if (auto QTL = TL.getAs<QualifiedTypeLoc>())262    TL = QTL.getUnqualifiedLoc();263 264  if (auto TTL = TL.getAs<TypedefTypeLoc>())265    return CharSourceRange::getTokenRange(TTL.getNameLoc());266  return CharSourceRange::getTokenRange(TL.castAs<UsingTypeLoc>().getNameLoc());267}268 269void UpgradeGoogletestCaseCheck::check(const MatchFinder::MatchResult &Result) {270  llvm::StringRef ReplacementText;271  CharSourceRange ReplacementRange;272  if (const auto *Method = Result.Nodes.getNodeAs<CXXMethodDecl>("method")) {273    ReplacementText = getNewMethodName(Method->getName());274 275    bool IsInInstantiation = false;276    bool IsInTemplate = false;277    bool AddFix = true;278    if (const auto *Call = Result.Nodes.getNodeAs<CXXMemberCallExpr>("call")) {279      const auto *Callee = llvm::cast<MemberExpr>(Call->getCallee());280      ReplacementRange = CharSourceRange::getTokenRange(Callee->getMemberLoc(),281                                                        Callee->getMemberLoc());282      IsInInstantiation = isInInstantiation(*Call, Result);283      IsInTemplate = isInTemplate<Stmt>(*Call, Result);284    } else if (const auto *Ref = Result.Nodes.getNodeAs<DeclRefExpr>("ref")) {285      ReplacementRange =286          CharSourceRange::getTokenRange(Ref->getNameInfo().getSourceRange());287      IsInInstantiation = isInInstantiation(*Ref, Result);288      IsInTemplate = isInTemplate<Stmt>(*Ref, Result);289    } else if (const auto *Using = Result.Nodes.getNodeAs<UsingDecl>("using")) {290      ReplacementRange =291          CharSourceRange::getTokenRange(Using->getNameInfo().getSourceRange());292      IsInInstantiation = isInInstantiation(*Using, Result);293      IsInTemplate = isInTemplate<Decl>(*Using, Result);294    } else {295      // This branch means we have matched a function declaration / definition296      // either for a function from googletest or for a function in a derived297      // class.298 299      ReplacementRange = CharSourceRange::getTokenRange(300          Method->getNameInfo().getSourceRange());301      IsInInstantiation = isInInstantiation(*Method, Result);302      IsInTemplate = isInTemplate<Decl>(*Method, Result);303 304      // If the type of the matched method is strictly derived from a googletest305      // type and has both the old and new member function names, then we cannot306      // safely rename (or delete) the old name version.307      AddFix = !derivedTypeHasReplacementMethod(Result, ReplacementText);308    }309 310    if (IsInInstantiation) {311      if (!MatchedTemplateLocations.contains(ReplacementRange.getBegin())) {312        // For each location matched in a template instantiation, we check if313        // the location can also be found in `MatchedTemplateLocations`. If it314        // is not found, that means the expression did not create a match315        // without the instantiation and depends on template parameters. A316        // manual fix is probably required so we provide only a warning.317        diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage);318      }319      return;320    }321 322    if (IsInTemplate) {323      // We gather source locations from template matches not in template324      // instantiations for future matches.325      MatchedTemplateLocations.insert(ReplacementRange.getBegin());326    }327 328    if (!AddFix) {329      diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage);330      return;331    }332  } else {333    // This is a match for `TestCase` to `TestSuite` refactoring.334    assert(Result.Nodes.getNodeAs<TypeAliasDecl>("test-case") != nullptr);335    ReplacementText = "TestSuite";336    ReplacementRange = getAliasNameRange(Result);337 338    // We do not need to keep track of template instantiations for this branch,339    // because we are matching a `TypeLoc` for the alias declaration. Templates340    // will only be instantiated with the true type name, `TestSuite`.341  }342 343  const DiagnosticBuilder Diag =344      diag(ReplacementRange.getBegin(), RenameCaseToSuiteMessage);345 346  ReplacementRange = Lexer::makeFileCharRange(347      ReplacementRange, *Result.SourceManager, Result.Context->getLangOpts());348  if (ReplacementRange.isInvalid())349    // An invalid source range likely means we are inside a macro body. A manual350    // fix is likely needed so we do not create a fix-it hint.351    return;352 353  Diag << FixItHint::CreateReplacement(ReplacementRange, ReplacementText);354}355 356} // namespace clang::tidy::google357