forked from OSchip/llvm-project
355 lines
14 KiB
C++
355 lines
14 KiB
C++
//===--- 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"
|
|
|
|
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 *) {
|
|
if (!getLangOpts().CPlusPlus)
|
|
return;
|
|
|
|
PP->addPPCallbacks(
|
|
llvm::make_unique<UpgradeGoogletestCasePPCallback>(this, PP));
|
|
}
|
|
|
|
void UpgradeGoogletestCaseCheck::registerMatchers(MatchFinder *Finder) {
|
|
if (!getLangOpts().CPlusPlus)
|
|
return;
|
|
|
|
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().getRawEncoding()) == 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().getRawEncoding());
|
|
}
|
|
|
|
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
|