xref: /llvm-project/clang-tools-extra/clang-tidy/readability/ContainerSizeEmptyCheck.cpp (revision 429e5be768c21d208ab688f8dfa1399c04ec5626)
1 //===--- ContainerSizeEmptyCheck.cpp - clang-tidy -------------------------===//
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-exception
6 //
7 //===----------------------------------------------------------------------===//
8 #include "ContainerSizeEmptyCheck.h"
9 #include "../utils/ASTUtils.h"
10 #include "../utils/Matchers.h"
11 #include "../utils/OptionsUtils.h"
12 #include "clang/AST/ASTContext.h"
13 #include "clang/ASTMatchers/ASTMatchers.h"
14 #include "clang/Lex/Lexer.h"
15 #include "llvm/ADT/StringRef.h"
16 
17 using namespace clang::ast_matchers;
18 
19 namespace clang {
20 namespace ast_matchers {
21 
AST_POLYMORPHIC_MATCHER_P2(hasAnyArgumentWithParam,AST_POLYMORPHIC_SUPPORTED_TYPES (CallExpr,CXXConstructExpr),internal::Matcher<Expr>,ArgMatcher,internal::Matcher<ParmVarDecl>,ParamMatcher)22 AST_POLYMORPHIC_MATCHER_P2(hasAnyArgumentWithParam,
23                            AST_POLYMORPHIC_SUPPORTED_TYPES(CallExpr,
24                                                            CXXConstructExpr),
25                            internal::Matcher<Expr>, ArgMatcher,
26                            internal::Matcher<ParmVarDecl>, ParamMatcher) {
27   BoundNodesTreeBuilder Result;
28   // The first argument of an overloaded member operator is the implicit object
29   // argument of the method which should not be matched against a parameter, so
30   // we skip over it here.
31   BoundNodesTreeBuilder Matches;
32   unsigned ArgIndex = cxxOperatorCallExpr(callee(cxxMethodDecl()))
33                               .matches(Node, Finder, &Matches)
34                           ? 1
35                           : 0;
36   int ParamIndex = 0;
37   for (; ArgIndex < Node.getNumArgs(); ++ArgIndex) {
38     BoundNodesTreeBuilder ArgMatches(*Builder);
39     if (ArgMatcher.matches(*(Node.getArg(ArgIndex)->IgnoreParenCasts()), Finder,
40                            &ArgMatches)) {
41       BoundNodesTreeBuilder ParamMatches(ArgMatches);
42       if (expr(anyOf(cxxConstructExpr(hasDeclaration(cxxConstructorDecl(
43                          hasParameter(ParamIndex, ParamMatcher)))),
44                      callExpr(callee(functionDecl(
45                          hasParameter(ParamIndex, ParamMatcher))))))
46               .matches(Node, Finder, &ParamMatches)) {
47         Result.addMatch(ParamMatches);
48         *Builder = std::move(Result);
49         return true;
50       }
51     }
52     ++ParamIndex;
53   }
54   return false;
55 }
56 
AST_MATCHER(Expr,usedInBooleanContext)57 AST_MATCHER(Expr, usedInBooleanContext) {
58   const char *ExprName = "__booleanContextExpr";
59   auto Result =
60       expr(expr().bind(ExprName),
61            anyOf(hasParent(
62                      mapAnyOf(varDecl, fieldDecl).with(hasType(booleanType()))),
63                  hasParent(cxxConstructorDecl(
64                      hasAnyConstructorInitializer(cxxCtorInitializer(
65                          withInitializer(expr(equalsBoundNode(ExprName))),
66                          forField(hasType(booleanType())))))),
67                  hasParent(stmt(anyOf(
68                      explicitCastExpr(hasDestinationType(booleanType())),
69                      mapAnyOf(ifStmt, doStmt, whileStmt, forStmt,
70                               conditionalOperator)
71                          .with(hasCondition(expr(equalsBoundNode(ExprName)))),
72                      parenListExpr(hasParent(varDecl(hasType(booleanType())))),
73                      parenExpr(hasParent(
74                          explicitCastExpr(hasDestinationType(booleanType())))),
75                      returnStmt(forFunction(returns(booleanType()))),
76                      cxxUnresolvedConstructExpr(hasType(booleanType())),
77                      invocation(hasAnyArgumentWithParam(
78                          expr(equalsBoundNode(ExprName)),
79                          parmVarDecl(hasType(booleanType())))),
80                      binaryOperator(hasAnyOperatorName("&&", "||")),
81                      unaryOperator(hasOperatorName("!")).bind("NegOnSize"))))))
82           .matches(Node, Finder, Builder);
83   Builder->removeBindings([ExprName](const BoundNodesMap &Nodes) {
84     return Nodes.getNode(ExprName).getNodeKind().isNone();
85   });
86   return Result;
87 }
88 
AST_MATCHER(CXXConstructExpr,isDefaultConstruction)89 AST_MATCHER(CXXConstructExpr, isDefaultConstruction) {
90   return Node.getConstructor()->isDefaultConstructor();
91 }
92 
AST_MATCHER(QualType,isIntegralType)93 AST_MATCHER(QualType, isIntegralType) {
94   return Node->isIntegralType(Finder->getASTContext());
95 }
96 
AST_MATCHER_P(UserDefinedLiteral,hasLiteral,clang::ast_matchers::internal::Matcher<Expr>,InnerMatcher)97 AST_MATCHER_P(UserDefinedLiteral, hasLiteral,
98               clang::ast_matchers::internal::Matcher<Expr>, InnerMatcher) {
99   const UserDefinedLiteral::LiteralOperatorKind LOK =
100       Node.getLiteralOperatorKind();
101   if (LOK == UserDefinedLiteral::LOK_Template ||
102       LOK == UserDefinedLiteral::LOK_Raw)
103     return false;
104 
105   if (const Expr *CookedLiteral = Node.getCookedLiteral())
106     return InnerMatcher.matches(*CookedLiteral, Finder, Builder);
107   return false;
108 }
109 
110 } // namespace ast_matchers
111 namespace tidy::readability {
112 
113 using utils::isBinaryOrTernary;
114 
ContainerSizeEmptyCheck(StringRef Name,ClangTidyContext * Context)115 ContainerSizeEmptyCheck::ContainerSizeEmptyCheck(StringRef Name,
116                                                  ClangTidyContext *Context)
117     : ClangTidyCheck(Name, Context),
118       ExcludedComparisonTypes(utils::options::parseStringList(
119           Options.get("ExcludedComparisonTypes", "::std::array"))) {}
120 
storeOptions(ClangTidyOptions::OptionMap & Opts)121 void ContainerSizeEmptyCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
122   Options.store(Opts, "ExcludedComparisonTypes",
123                 utils::options::serializeStringList(ExcludedComparisonTypes));
124 }
125 
registerMatchers(MatchFinder * Finder)126 void ContainerSizeEmptyCheck::registerMatchers(MatchFinder *Finder) {
127   const auto ValidContainerRecord = cxxRecordDecl(isSameOrDerivedFrom(
128       namedDecl(has(cxxMethodDecl(isConst(), parameterCountIs(0), isPublic(),
129                                   hasAnyName("size", "length"),
130                                   returns(qualType(isIntegralType(),
131                                                    unless(booleanType()))))
132                         .bind("size")),
133                 has(cxxMethodDecl(isConst(), parameterCountIs(0), isPublic(),
134                                   hasName("empty"), returns(booleanType()))
135                         .bind("empty")))
136           .bind("container")));
137 
138   const auto ValidContainerNonTemplateType =
139       qualType(hasUnqualifiedDesugaredType(
140           recordType(hasDeclaration(ValidContainerRecord))));
141   const auto ValidContainerTemplateType =
142       qualType(hasUnqualifiedDesugaredType(templateSpecializationType(
143           hasDeclaration(classTemplateDecl(has(ValidContainerRecord))))));
144 
145   const auto ValidContainer = qualType(
146       anyOf(ValidContainerNonTemplateType, ValidContainerTemplateType));
147 
148   const auto WrongUse =
149       anyOf(hasParent(binaryOperator(
150                           isComparisonOperator(),
151                           hasEitherOperand(anyOf(integerLiteral(equals(1)),
152                                                  integerLiteral(equals(0)))))
153                           .bind("SizeBinaryOp")),
154             usedInBooleanContext());
155 
156   Finder->addMatcher(
157       cxxMemberCallExpr(
158           argumentCountIs(0),
159           on(expr(anyOf(hasType(ValidContainer),
160                         hasType(pointsTo(ValidContainer)),
161                         hasType(references(ValidContainer))))
162                  .bind("MemberCallObject")),
163           callee(
164               cxxMethodDecl(hasAnyName("size", "length")).bind("SizeMethod")),
165           WrongUse,
166           unless(hasAncestor(
167               cxxMethodDecl(ofClass(equalsBoundNode("container"))))))
168           .bind("SizeCallExpr"),
169       this);
170 
171   Finder->addMatcher(
172       callExpr(argumentCountIs(0),
173                has(cxxDependentScopeMemberExpr(
174                        hasObjectExpression(
175                            expr(anyOf(hasType(ValidContainer),
176                                       hasType(pointsTo(ValidContainer)),
177                                       hasType(references(ValidContainer))))
178                                .bind("MemberCallObject")),
179                        anyOf(hasMemberName("size"), hasMemberName("length")))
180                        .bind("DependentExpr")),
181                WrongUse,
182                unless(hasAncestor(
183                    cxxMethodDecl(ofClass(equalsBoundNode("container"))))))
184           .bind("SizeCallExpr"),
185       this);
186 
187   // Comparison to empty string or empty constructor.
188   const auto WrongComparend =
189       anyOf(stringLiteral(hasSize(0)),
190             userDefinedLiteral(hasLiteral(stringLiteral(hasSize(0)))),
191             cxxConstructExpr(isDefaultConstruction()),
192             cxxUnresolvedConstructExpr(argumentCountIs(0)));
193   // Match the object being compared.
194   const auto STLArg =
195       anyOf(unaryOperator(
196                 hasOperatorName("*"),
197                 hasUnaryOperand(
198                     expr(hasType(pointsTo(ValidContainer))).bind("Pointee"))),
199             expr(hasType(ValidContainer)).bind("STLObject"));
200 
201   const auto ExcludedComparisonTypesMatcher = qualType(anyOf(
202       hasDeclaration(
203           cxxRecordDecl(matchers::matchesAnyListedName(ExcludedComparisonTypes))
204               .bind("excluded")),
205       hasCanonicalType(hasDeclaration(
206           cxxRecordDecl(matchers::matchesAnyListedName(ExcludedComparisonTypes))
207               .bind("excluded")))));
208   const auto SameExcludedComparisonTypesMatcher =
209       qualType(anyOf(hasDeclaration(cxxRecordDecl(equalsBoundNode("excluded"))),
210                      hasCanonicalType(hasDeclaration(
211                          cxxRecordDecl(equalsBoundNode("excluded"))))));
212 
213   Finder->addMatcher(
214       binaryOperation(
215           hasAnyOperatorName("==", "!="), hasOperands(WrongComparend, STLArg),
216           unless(allOf(hasLHS(hasType(ExcludedComparisonTypesMatcher)),
217                        hasRHS(hasType(SameExcludedComparisonTypesMatcher)))),
218           unless(hasAncestor(
219               cxxMethodDecl(ofClass(equalsBoundNode("container"))))))
220           .bind("BinCmp"),
221       this);
222 }
223 
check(const MatchFinder::MatchResult & Result)224 void ContainerSizeEmptyCheck::check(const MatchFinder::MatchResult &Result) {
225   const auto *MemberCall = Result.Nodes.getNodeAs<Expr>("SizeCallExpr");
226   const auto *MemberCallObject =
227       Result.Nodes.getNodeAs<Expr>("MemberCallObject");
228   const auto *BinCmp = Result.Nodes.getNodeAs<CXXOperatorCallExpr>("BinCmp");
229   const auto *BinCmpTempl = Result.Nodes.getNodeAs<BinaryOperator>("BinCmp");
230   const auto *BinCmpRewritten =
231       Result.Nodes.getNodeAs<CXXRewrittenBinaryOperator>("BinCmp");
232   const auto *BinaryOp = Result.Nodes.getNodeAs<BinaryOperator>("SizeBinaryOp");
233   const auto *Pointee = Result.Nodes.getNodeAs<Expr>("Pointee");
234   const auto *E =
235       MemberCallObject
236           ? MemberCallObject
237           : (Pointee ? Pointee : Result.Nodes.getNodeAs<Expr>("STLObject"));
238   FixItHint Hint;
239   std::string ReplacementText = std::string(
240       Lexer::getSourceText(CharSourceRange::getTokenRange(E->getSourceRange()),
241                            *Result.SourceManager, getLangOpts()));
242   const auto *OpCallExpr = dyn_cast<CXXOperatorCallExpr>(E);
243   if (isBinaryOrTernary(E) || isa<UnaryOperator>(E) ||
244       (OpCallExpr && (OpCallExpr->getOperator() == OO_Star))) {
245     ReplacementText = "(" + ReplacementText + ")";
246   }
247   if (OpCallExpr &&
248       OpCallExpr->getOperator() == OverloadedOperatorKind::OO_Arrow) {
249     // This can happen if the object is a smart pointer. Don't add anything
250     // because a '->' is already there (PR#51776), just call the method.
251     ReplacementText += "empty()";
252   } else if (E->getType()->isPointerType())
253     ReplacementText += "->empty()";
254   else
255     ReplacementText += ".empty()";
256 
257   if (BinCmp) {
258     if (BinCmp->getOperator() == OO_ExclaimEqual) {
259       ReplacementText = "!" + ReplacementText;
260     }
261     Hint =
262         FixItHint::CreateReplacement(BinCmp->getSourceRange(), ReplacementText);
263   } else if (BinCmpTempl) {
264     if (BinCmpTempl->getOpcode() == BinaryOperatorKind::BO_NE) {
265       ReplacementText = "!" + ReplacementText;
266     }
267     Hint = FixItHint::CreateReplacement(BinCmpTempl->getSourceRange(),
268                                         ReplacementText);
269   } else if (BinCmpRewritten) {
270     if (BinCmpRewritten->getOpcode() == BinaryOperatorKind::BO_NE) {
271       ReplacementText = "!" + ReplacementText;
272     }
273     Hint = FixItHint::CreateReplacement(BinCmpRewritten->getSourceRange(),
274                                         ReplacementText);
275   } else if (BinaryOp) { // Determine the correct transformation.
276     const auto *LiteralLHS =
277         llvm::dyn_cast<IntegerLiteral>(BinaryOp->getLHS()->IgnoreImpCasts());
278     const auto *LiteralRHS =
279         llvm::dyn_cast<IntegerLiteral>(BinaryOp->getRHS()->IgnoreImpCasts());
280     const bool ContainerIsLHS = !LiteralLHS;
281 
282     uint64_t Value = 0;
283     if (LiteralLHS)
284       Value = LiteralLHS->getValue().getLimitedValue();
285     else if (LiteralRHS)
286       Value = LiteralRHS->getValue().getLimitedValue();
287     else
288       return;
289 
290     bool Negation = false;
291     const auto OpCode = BinaryOp->getOpcode();
292 
293     // Constant that is not handled.
294     if (Value > 1)
295       return;
296 
297     if (Value == 1 && (OpCode == BinaryOperatorKind::BO_EQ ||
298                        OpCode == BinaryOperatorKind::BO_NE))
299       return;
300 
301     // Always true/false, no warnings for that.
302     if (Value == 0) {
303       if ((OpCode == BinaryOperatorKind::BO_GT && !ContainerIsLHS) ||
304           (OpCode == BinaryOperatorKind::BO_LT && ContainerIsLHS) ||
305           (OpCode == BinaryOperatorKind::BO_LE && !ContainerIsLHS) ||
306           (OpCode == BinaryOperatorKind::BO_GE && ContainerIsLHS))
307         return;
308     }
309 
310     // Do not warn for size > 1, 1 < size, size <= 1, 1 >= size.
311     if (Value == 1) {
312       if ((OpCode == BinaryOperatorKind::BO_GT && ContainerIsLHS) ||
313           (OpCode == BinaryOperatorKind::BO_LT && !ContainerIsLHS))
314         return;
315       if ((OpCode == BinaryOperatorKind::BO_LE && ContainerIsLHS) ||
316           (OpCode == BinaryOperatorKind::BO_GE && !ContainerIsLHS))
317         return;
318     }
319 
320     // Do not warn for size < 1, 1 > size, size <= 0, 0 >= size for non signed
321     // types
322     if ((OpCode == BinaryOperatorKind::BO_GT && Value == 1 &&
323          !ContainerIsLHS) ||
324         (OpCode == BinaryOperatorKind::BO_LT && Value == 1 && ContainerIsLHS) ||
325         (OpCode == BinaryOperatorKind::BO_GE && Value == 0 &&
326          !ContainerIsLHS) ||
327         (OpCode == BinaryOperatorKind::BO_LE && Value == 0 && ContainerIsLHS)) {
328       const Expr *Container = ContainerIsLHS
329                                   ? BinaryOp->getLHS()->IgnoreImpCasts()
330                                   : BinaryOp->getRHS()->IgnoreImpCasts();
331       if (Container->getType()
332               .getCanonicalType()
333               .getNonReferenceType()
334               ->isSignedIntegerType())
335         return;
336     }
337 
338     if (OpCode == BinaryOperatorKind::BO_NE && Value == 0)
339       Negation = true;
340 
341     if ((OpCode == BinaryOperatorKind::BO_GT ||
342          OpCode == BinaryOperatorKind::BO_GE) &&
343         ContainerIsLHS)
344       Negation = true;
345 
346     if ((OpCode == BinaryOperatorKind::BO_LT ||
347          OpCode == BinaryOperatorKind::BO_LE) &&
348         !ContainerIsLHS)
349       Negation = true;
350 
351     if (Negation)
352       ReplacementText = "!" + ReplacementText;
353     Hint = FixItHint::CreateReplacement(BinaryOp->getSourceRange(),
354                                         ReplacementText);
355 
356   } else {
357     // If there is a conversion above the size call to bool, it is safe to just
358     // replace size with empty.
359     if (const auto *UnaryOp =
360             Result.Nodes.getNodeAs<UnaryOperator>("NegOnSize"))
361       Hint = FixItHint::CreateReplacement(UnaryOp->getSourceRange(),
362                                           ReplacementText);
363     else
364       Hint = FixItHint::CreateReplacement(MemberCall->getSourceRange(),
365                                           "!" + ReplacementText);
366   }
367 
368   auto WarnLoc = MemberCall ? MemberCall->getBeginLoc() : SourceLocation{};
369 
370   if (WarnLoc.isValid()) {
371     auto Diag = diag(WarnLoc, "the 'empty' method should be used to check "
372                               "for emptiness instead of %0");
373     if (const auto *SizeMethod =
374             Result.Nodes.getNodeAs<NamedDecl>("SizeMethod"))
375       Diag << SizeMethod;
376     else if (const auto *DependentExpr =
377                  Result.Nodes.getNodeAs<CXXDependentScopeMemberExpr>(
378                      "DependentExpr"))
379       Diag << DependentExpr->getMember();
380     else
381       Diag << "unknown method";
382     Diag << Hint;
383   } else {
384     WarnLoc = BinCmpTempl
385                   ? BinCmpTempl->getBeginLoc()
386                   : (BinCmp ? BinCmp->getBeginLoc()
387                             : (BinCmpRewritten ? BinCmpRewritten->getBeginLoc()
388                                                : SourceLocation{}));
389     diag(WarnLoc, "the 'empty' method should be used to check "
390                   "for emptiness instead of comparing to an empty object")
391         << Hint;
392   }
393 
394   const auto *Container = Result.Nodes.getNodeAs<NamedDecl>("container");
395   if (const auto *CTS = dyn_cast<ClassTemplateSpecializationDecl>(Container)) {
396     // The definition of the empty() method is the same for all implicit
397     // instantiations. In order to avoid duplicate or inconsistent warnings
398     // (depending on how deduplication is done), we use the same class name
399     // for all implicit instantiations of a template.
400     if (CTS->getSpecializationKind() == TSK_ImplicitInstantiation)
401       Container = CTS->getSpecializedTemplate();
402   }
403   const auto *Empty = Result.Nodes.getNodeAs<FunctionDecl>("empty");
404 
405   diag(Empty->getLocation(), "method %0::empty() defined here",
406        DiagnosticIDs::Note)
407       << Container;
408 }
409 
410 } // namespace tidy::readability
411 } // namespace clang
412