| //===--- 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(); |
| } |
| } // 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(isInteger(), unless(booleanType()), |
| unless(elaboratedType())))) |
| .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())); |
| if (isBinaryOrTernary(E) || isa<UnaryOperator>(E)) { |
| ReplacementText = "(" + ReplacementText + ")"; |
| } |
| 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. |
| bool Negation = false; |
| const bool ContainerIsLHS = |
| !llvm::isa<IntegerLiteral>(BinaryOp->getLHS()->IgnoreImpCasts()); |
| const auto OpCode = BinaryOp->getOpcode(); |
| uint64_t Value = 0; |
| if (ContainerIsLHS) { |
| if (const auto *Literal = llvm::dyn_cast<IntegerLiteral>( |
| BinaryOp->getRHS()->IgnoreImpCasts())) |
| Value = Literal->getValue().getLimitedValue(); |
| else |
| return; |
| } else { |
| Value = |
| llvm::dyn_cast<IntegerLiteral>(BinaryOp->getLHS()->IgnoreImpCasts()) |
| ->getValue() |
| .getLimitedValue(); |
| } |
| |
| // 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 |