llvm-project/clang-tools-extra/clang-tidy/readability/ContainerSizeEmptyCheck.cpp

334 lines
14 KiB
C++

//===--- ContainerSizeEmptyCheck.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 "ContainerSizeEmptyCheck.h"
#include "../utils/ASTUtils.h"
#include "../utils/Matchers.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/StringRef.h"
using namespace clang::ast_matchers;
namespace clang {
namespace ast_matchers {
AST_POLYMORPHIC_MATCHER_P2(hasAnyArgumentWithParam,
AST_POLYMORPHIC_SUPPORTED_TYPES(CallExpr,
CXXConstructExpr),
internal::Matcher<Expr>, ArgMatcher,
internal::Matcher<ParmVarDecl>, ParamMatcher) {
BoundNodesTreeBuilder Result;
// The first argument of an overloaded member operator is the implicit object
// argument of the method which should not be matched against a parameter, so
// we skip over it here.
BoundNodesTreeBuilder Matches;
unsigned ArgIndex = cxxOperatorCallExpr(callee(cxxMethodDecl()))
.matches(Node, Finder, &Matches)
? 1
: 0;
int ParamIndex = 0;
for (; ArgIndex < Node.getNumArgs(); ++ArgIndex) {
BoundNodesTreeBuilder ArgMatches(*Builder);
if (ArgMatcher.matches(*(Node.getArg(ArgIndex)->IgnoreParenCasts()), Finder,
&ArgMatches)) {
BoundNodesTreeBuilder ParamMatches(ArgMatches);
if (expr(anyOf(cxxConstructExpr(hasDeclaration(cxxConstructorDecl(
hasParameter(ParamIndex, ParamMatcher)))),
callExpr(callee(functionDecl(
hasParameter(ParamIndex, ParamMatcher))))))
.matches(Node, Finder, &ParamMatches)) {
Result.addMatch(ParamMatches);
*Builder = std::move(Result);
return true;
}
}
++ParamIndex;
}
return false;
}
AST_MATCHER(Expr, usedInBooleanContext) {
const char *ExprName = "__booleanContextExpr";
auto Result =
expr(expr().bind(ExprName),
anyOf(hasParent(
mapAnyOf(varDecl, fieldDecl).with(hasType(booleanType()))),
hasParent(cxxConstructorDecl(
hasAnyConstructorInitializer(cxxCtorInitializer(
withInitializer(expr(equalsBoundNode(ExprName))),
forField(hasType(booleanType())))))),
hasParent(stmt(anyOf(
explicitCastExpr(hasDestinationType(booleanType())),
mapAnyOf(ifStmt, doStmt, whileStmt, forStmt,
conditionalOperator)
.with(hasCondition(expr(equalsBoundNode(ExprName)))),
parenListExpr(hasParent(varDecl(hasType(booleanType())))),
parenExpr(hasParent(
explicitCastExpr(hasDestinationType(booleanType())))),
returnStmt(forFunction(returns(booleanType()))),
cxxUnresolvedConstructExpr(hasType(booleanType())),
invocation(hasAnyArgumentWithParam(
expr(equalsBoundNode(ExprName)),
parmVarDecl(hasType(booleanType())))),
binaryOperator(hasAnyOperatorName("&&", "||")),
unaryOperator(hasOperatorName("!")).bind("NegOnSize"))))))
.matches(Node, Finder, Builder);
Builder->removeBindings([ExprName](const BoundNodesMap &Nodes) {
return Nodes.getNode(ExprName).getNodeKind().isNone();
});
return Result;
}
AST_MATCHER(CXXConstructExpr, isDefaultConstruction) {
return Node.getConstructor()->isDefaultConstructor();
}
AST_MATCHER(QualType, isIntegralType) {
return Node->isIntegralType(Finder->getASTContext());
}
} // namespace ast_matchers
namespace tidy {
namespace readability {
using utils::isBinaryOrTernary;
ContainerSizeEmptyCheck::ContainerSizeEmptyCheck(StringRef Name,
ClangTidyContext *Context)
: ClangTidyCheck(Name, Context) {}
void ContainerSizeEmptyCheck::registerMatchers(MatchFinder *Finder) {
const auto ValidContainerRecord = cxxRecordDecl(isSameOrDerivedFrom(
namedDecl(
has(cxxMethodDecl(
isConst(), parameterCountIs(0), isPublic(), hasName("size"),
returns(qualType(isIntegralType(), unless(booleanType()))))
.bind("size")),
has(cxxMethodDecl(isConst(), parameterCountIs(0), isPublic(),
hasName("empty"), returns(booleanType()))
.bind("empty")))
.bind("container")));
const auto ValidContainerNonTemplateType =
qualType(hasUnqualifiedDesugaredType(
recordType(hasDeclaration(ValidContainerRecord))));
const auto ValidContainerTemplateType =
qualType(hasUnqualifiedDesugaredType(templateSpecializationType(
hasDeclaration(classTemplateDecl(has(ValidContainerRecord))))));
const auto ValidContainer = qualType(
anyOf(ValidContainerNonTemplateType, ValidContainerTemplateType));
const auto WrongUse =
anyOf(hasParent(binaryOperator(
isComparisonOperator(),
hasEitherOperand(anyOf(integerLiteral(equals(1)),
integerLiteral(equals(0)))))
.bind("SizeBinaryOp")),
usedInBooleanContext());
Finder->addMatcher(
cxxMemberCallExpr(on(expr(anyOf(hasType(ValidContainer),
hasType(pointsTo(ValidContainer)),
hasType(references(ValidContainer))))
.bind("MemberCallObject")),
callee(cxxMethodDecl(hasName("size"))), WrongUse,
unless(hasAncestor(cxxMethodDecl(
ofClass(equalsBoundNode("container"))))))
.bind("SizeCallExpr"),
this);
Finder->addMatcher(
callExpr(has(cxxDependentScopeMemberExpr(
hasObjectExpression(
expr(anyOf(hasType(ValidContainer),
hasType(pointsTo(ValidContainer)),
hasType(references(ValidContainer))))
.bind("MemberCallObject")),
hasMemberName("size"))),
WrongUse,
unless(hasAncestor(
cxxMethodDecl(ofClass(equalsBoundNode("container"))))))
.bind("SizeCallExpr"),
this);
// Comparison to empty string or empty constructor.
const auto WrongComparend = anyOf(
stringLiteral(hasSize(0)), cxxConstructExpr(isDefaultConstruction()),
cxxUnresolvedConstructExpr(argumentCountIs(0)));
// Match the object being compared.
const auto STLArg =
anyOf(unaryOperator(
hasOperatorName("*"),
hasUnaryOperand(
expr(hasType(pointsTo(ValidContainer))).bind("Pointee"))),
expr(hasType(ValidContainer)).bind("STLObject"));
Finder->addMatcher(
binaryOperation(hasAnyOperatorName("==", "!="),
hasOperands(WrongComparend,
STLArg),
unless(hasAncestor(cxxMethodDecl(
ofClass(equalsBoundNode("container"))))))
.bind("BinCmp"),
this);
}
void ContainerSizeEmptyCheck::check(const MatchFinder::MatchResult &Result) {
const auto *MemberCall = Result.Nodes.getNodeAs<Expr>("SizeCallExpr");
const auto *MemberCallObject =
Result.Nodes.getNodeAs<Expr>("MemberCallObject");
const auto *BinCmp = Result.Nodes.getNodeAs<CXXOperatorCallExpr>("BinCmp");
const auto *BinCmpTempl = Result.Nodes.getNodeAs<BinaryOperator>("BinCmp");
const auto *BinCmpRewritten =
Result.Nodes.getNodeAs<CXXRewrittenBinaryOperator>("BinCmp");
const auto *BinaryOp = Result.Nodes.getNodeAs<BinaryOperator>("SizeBinaryOp");
const auto *Pointee = Result.Nodes.getNodeAs<Expr>("Pointee");
const auto *E =
MemberCallObject
? MemberCallObject
: (Pointee ? Pointee : Result.Nodes.getNodeAs<Expr>("STLObject"));
FixItHint Hint;
std::string ReplacementText = std::string(
Lexer::getSourceText(CharSourceRange::getTokenRange(E->getSourceRange()),
*Result.SourceManager, getLangOpts()));
const auto *OpCallExpr = dyn_cast<CXXOperatorCallExpr>(E);
if (isBinaryOrTernary(E) || isa<UnaryOperator>(E) ||
(OpCallExpr && (OpCallExpr->getOperator() == OO_Star))) {
ReplacementText = "(" + ReplacementText + ")";
}
if (OpCallExpr &&
OpCallExpr->getOperator() == OverloadedOperatorKind::OO_Arrow) {
// This can happen if the object is a smart pointer. Don't add anything
// because a '->' is already there (PR#51776), just call the method.
ReplacementText += "empty()";
} else if (E->getType()->isPointerType())
ReplacementText += "->empty()";
else
ReplacementText += ".empty()";
if (BinCmp) {
if (BinCmp->getOperator() == OO_ExclaimEqual) {
ReplacementText = "!" + ReplacementText;
}
Hint =
FixItHint::CreateReplacement(BinCmp->getSourceRange(), ReplacementText);
} else if (BinCmpTempl) {
if (BinCmpTempl->getOpcode() == BinaryOperatorKind::BO_NE) {
ReplacementText = "!" + ReplacementText;
}
Hint = FixItHint::CreateReplacement(BinCmpTempl->getSourceRange(),
ReplacementText);
} else if (BinCmpRewritten) {
if (BinCmpRewritten->getOpcode() == BinaryOperatorKind::BO_NE) {
ReplacementText = "!" + ReplacementText;
}
Hint = FixItHint::CreateReplacement(BinCmpRewritten->getSourceRange(),
ReplacementText);
} else if (BinaryOp) { // Determine the correct transformation.
const auto *LiteralLHS =
llvm::dyn_cast<IntegerLiteral>(BinaryOp->getLHS()->IgnoreImpCasts());
const auto *LiteralRHS =
llvm::dyn_cast<IntegerLiteral>(BinaryOp->getRHS()->IgnoreImpCasts());
const bool ContainerIsLHS = !LiteralLHS;
uint64_t Value = 0;
if (LiteralLHS)
Value = LiteralLHS->getValue().getLimitedValue();
else if (LiteralRHS)
Value = LiteralRHS->getValue().getLimitedValue();
else
return;
bool Negation = false;
const auto OpCode = BinaryOp->getOpcode();
// Constant that is not handled.
if (Value > 1)
return;
if (Value == 1 && (OpCode == BinaryOperatorKind::BO_EQ ||
OpCode == BinaryOperatorKind::BO_NE))
return;
// Always true, no warnings for that.
if ((OpCode == BinaryOperatorKind::BO_GE && Value == 0 && ContainerIsLHS) ||
(OpCode == BinaryOperatorKind::BO_LE && Value == 0 && !ContainerIsLHS))
return;
// Do not warn for size > 1, 1 < size, size <= 1, 1 >= size.
if (Value == 1) {
if ((OpCode == BinaryOperatorKind::BO_GT && ContainerIsLHS) ||
(OpCode == BinaryOperatorKind::BO_LT && !ContainerIsLHS))
return;
if ((OpCode == BinaryOperatorKind::BO_LE && ContainerIsLHS) ||
(OpCode == BinaryOperatorKind::BO_GE && !ContainerIsLHS))
return;
}
if (OpCode == BinaryOperatorKind::BO_NE && Value == 0)
Negation = true;
if ((OpCode == BinaryOperatorKind::BO_GT ||
OpCode == BinaryOperatorKind::BO_GE) &&
ContainerIsLHS)
Negation = true;
if ((OpCode == BinaryOperatorKind::BO_LT ||
OpCode == BinaryOperatorKind::BO_LE) &&
!ContainerIsLHS)
Negation = true;
if (Negation)
ReplacementText = "!" + ReplacementText;
Hint = FixItHint::CreateReplacement(BinaryOp->getSourceRange(),
ReplacementText);
} else {
// If there is a conversion above the size call to bool, it is safe to just
// replace size with empty.
if (const auto *UnaryOp =
Result.Nodes.getNodeAs<UnaryOperator>("NegOnSize"))
Hint = FixItHint::CreateReplacement(UnaryOp->getSourceRange(),
ReplacementText);
else
Hint = FixItHint::CreateReplacement(MemberCall->getSourceRange(),
"!" + ReplacementText);
}
auto WarnLoc = MemberCall ? MemberCall->getBeginLoc() : SourceLocation{};
if (WarnLoc.isValid()) {
diag(WarnLoc, "the 'empty' method should be used to check "
"for emptiness instead of 'size'")
<< Hint;
} else {
WarnLoc = BinCmpTempl
? BinCmpTempl->getBeginLoc()
: (BinCmp ? BinCmp->getBeginLoc()
: (BinCmpRewritten ? BinCmpRewritten->getBeginLoc()
: SourceLocation{}));
diag(WarnLoc, "the 'empty' method should be used to check "
"for emptiness instead of comparing to an empty object")
<< Hint;
}
const auto *Container = Result.Nodes.getNodeAs<NamedDecl>("container");
if (const auto *CTS = dyn_cast<ClassTemplateSpecializationDecl>(Container)) {
// The definition of the empty() method is the same for all implicit
// instantiations. In order to avoid duplicate or inconsistent warnings
// (depending on how deduplication is done), we use the same class name
// for all implicit instantiations of a template.
if (CTS->getSpecializationKind() == TSK_ImplicitInstantiation)
Container = CTS->getSpecializedTemplate();
}
const auto *Empty = Result.Nodes.getNodeAs<FunctionDecl>("empty");
diag(Empty->getLocation(), "method %0::empty() defined here",
DiagnosticIDs::Note)
<< Container;
}
} // namespace readability
} // namespace tidy
} // namespace clang