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