blob: d943b7a1a2705f99589ceebd0292d54f171b536a [file] [log] [blame]
//===--- 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