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 "clang/AST/ASTContext.h"
12 #include "clang/ASTMatchers/ASTMatchers.h"
13 #include "clang/Lex/Lexer.h"
14 #include "llvm/ADT/StringRef.h"
15 
16 using namespace clang::ast_matchers;
17 
18 namespace clang {
19 namespace ast_matchers {
20 AST_POLYMORPHIC_MATCHER_P2(hasAnyArgumentWithParam,
21                            AST_POLYMORPHIC_SUPPORTED_TYPES(CallExpr,
22                                                            CXXConstructExpr),
23                            internal::Matcher<Expr>, ArgMatcher,
24                            internal::Matcher<ParmVarDecl>, ParamMatcher) {
25   BoundNodesTreeBuilder Result;
26   // The first argument of an overloaded member operator is the implicit object
27   // argument of the method which should not be matched against a parameter, so
28   // we skip over it here.
29   BoundNodesTreeBuilder Matches;
30   unsigned ArgIndex = cxxOperatorCallExpr(callee(cxxMethodDecl()))
31                               .matches(Node, Finder, &Matches)
32                           ? 1
33                           : 0;
34   int ParamIndex = 0;
35   for (; ArgIndex < Node.getNumArgs(); ++ArgIndex) {
36     BoundNodesTreeBuilder ArgMatches(*Builder);
37     if (ArgMatcher.matches(*(Node.getArg(ArgIndex)->IgnoreParenCasts()), Finder,
38                            &ArgMatches)) {
39       BoundNodesTreeBuilder ParamMatches(ArgMatches);
40       if (expr(anyOf(cxxConstructExpr(hasDeclaration(cxxConstructorDecl(
41                          hasParameter(ParamIndex, ParamMatcher)))),
42                      callExpr(callee(functionDecl(
43                          hasParameter(ParamIndex, ParamMatcher))))))
44               .matches(Node, Finder, &ParamMatches)) {
45         Result.addMatch(ParamMatches);
46         *Builder = std::move(Result);
47         return true;
48       }
49     }
50     ++ParamIndex;
51   }
52   return false;
53 }
54 
55 AST_MATCHER(Expr, usedInBooleanContext) {
56   const char *ExprName = "__booleanContextExpr";
57   auto Result =
58       expr(expr().bind(ExprName),
59            anyOf(hasParent(
60                      mapAnyOf(varDecl, fieldDecl).with(hasType(booleanType()))),
61                  hasParent(cxxConstructorDecl(
62                      hasAnyConstructorInitializer(cxxCtorInitializer(
63                          withInitializer(expr(equalsBoundNode(ExprName))),
64                          forField(hasType(booleanType())))))),
65                  hasParent(stmt(anyOf(
66                      explicitCastExpr(hasDestinationType(booleanType())),
67                      mapAnyOf(ifStmt, doStmt, whileStmt, forStmt,
68                               conditionalOperator)
69                          .with(hasCondition(expr(equalsBoundNode(ExprName)))),
70                      parenListExpr(hasParent(varDecl(hasType(booleanType())))),
71                      parenExpr(hasParent(
72                          explicitCastExpr(hasDestinationType(booleanType())))),
73                      returnStmt(forFunction(returns(booleanType()))),
74                      cxxUnresolvedConstructExpr(hasType(booleanType())),
75                      invocation(hasAnyArgumentWithParam(
76                          expr(equalsBoundNode(ExprName)),
77                          parmVarDecl(hasType(booleanType())))),
78                      binaryOperator(hasAnyOperatorName("&&", "||")),
79                      unaryOperator(hasOperatorName("!")).bind("NegOnSize"))))))
80           .matches(Node, Finder, Builder);
81   Builder->removeBindings([ExprName](const BoundNodesMap &Nodes) {
82     return Nodes.getNode(ExprName).getNodeKind().isNone();
83   });
84   return Result;
85 }
86 AST_MATCHER(CXXConstructExpr, isDefaultConstruction) {
87   return Node.getConstructor()->isDefaultConstructor();
88 }
89 } // namespace ast_matchers
90 namespace tidy {
91 namespace readability {
92 
93 using utils::IsBinaryOrTernary;
94 
95 ContainerSizeEmptyCheck::ContainerSizeEmptyCheck(StringRef Name,
96                                                  ClangTidyContext *Context)
97     : ClangTidyCheck(Name, Context) {}
98 
99 void ContainerSizeEmptyCheck::registerMatchers(MatchFinder *Finder) {
100   const auto ValidContainerRecord = cxxRecordDecl(isSameOrDerivedFrom(
101       namedDecl(
102           has(cxxMethodDecl(isConst(), parameterCountIs(0), isPublic(),
103                             hasName("size"),
104                             returns(qualType(isInteger(), unless(booleanType()),
105                                              unless(elaboratedType()))))
106                   .bind("size")),
107           has(cxxMethodDecl(isConst(), parameterCountIs(0), isPublic(),
108                             hasName("empty"), returns(booleanType()))
109                   .bind("empty")))
110           .bind("container")));
111 
112   const auto ValidContainerNonTemplateType =
113       qualType(hasUnqualifiedDesugaredType(
114           recordType(hasDeclaration(ValidContainerRecord))));
115   const auto ValidContainerTemplateType =
116       qualType(hasUnqualifiedDesugaredType(templateSpecializationType(
117           hasDeclaration(classTemplateDecl(has(ValidContainerRecord))))));
118 
119   const auto ValidContainer = qualType(
120       anyOf(ValidContainerNonTemplateType, ValidContainerTemplateType));
121 
122   const auto WrongUse =
123       anyOf(hasParent(binaryOperator(
124                           isComparisonOperator(),
125                           hasEitherOperand(anyOf(integerLiteral(equals(1)),
126                                                  integerLiteral(equals(0)))))
127                           .bind("SizeBinaryOp")),
128             usedInBooleanContext());
129 
130   Finder->addMatcher(
131       cxxMemberCallExpr(on(expr(anyOf(hasType(ValidContainer),
132                                       hasType(pointsTo(ValidContainer)),
133                                       hasType(references(ValidContainer))))
134                                .bind("MemberCallObject")),
135                         callee(cxxMethodDecl(hasName("size"))), WrongUse,
136                         unless(hasAncestor(cxxMethodDecl(
137                             ofClass(equalsBoundNode("container"))))))
138           .bind("SizeCallExpr"),
139       this);
140 
141   Finder->addMatcher(
142       callExpr(has(cxxDependentScopeMemberExpr(
143                    hasObjectExpression(
144                        expr(anyOf(hasType(ValidContainer),
145                                   hasType(pointsTo(ValidContainer)),
146                                   hasType(references(ValidContainer))))
147                            .bind("MemberCallObject")),
148                    hasMemberName("size"))),
149                WrongUse,
150                unless(hasAncestor(
151                    cxxMethodDecl(ofClass(equalsBoundNode("container"))))))
152           .bind("SizeCallExpr"),
153       this);
154 
155   // Comparison to empty string or empty constructor.
156   const auto WrongComparend = anyOf(
157       stringLiteral(hasSize(0)), cxxConstructExpr(isDefaultConstruction()),
158       cxxUnresolvedConstructExpr(argumentCountIs(0)));
159   // Match the object being compared.
160   const auto STLArg =
161       anyOf(unaryOperator(
162                 hasOperatorName("*"),
163                 hasUnaryOperand(
164                     expr(hasType(pointsTo(ValidContainer))).bind("Pointee"))),
165             expr(hasType(ValidContainer)).bind("STLObject"));
166   Finder->addMatcher(
167       binaryOperation(hasAnyOperatorName("==", "!="),
168                       hasOperands(WrongComparend,
169                                   STLArg),
170                           unless(hasAncestor(cxxMethodDecl(
171                               ofClass(equalsBoundNode("container"))))))
172           .bind("BinCmp"),
173       this);
174 }
175 
176 void ContainerSizeEmptyCheck::check(const MatchFinder::MatchResult &Result) {
177   const auto *MemberCall = Result.Nodes.getNodeAs<Expr>("SizeCallExpr");
178   const auto *MemberCallObject =
179       Result.Nodes.getNodeAs<Expr>("MemberCallObject");
180   const auto *BinCmp = Result.Nodes.getNodeAs<CXXOperatorCallExpr>("BinCmp");
181   const auto *BinCmpTempl = Result.Nodes.getNodeAs<BinaryOperator>("BinCmp");
182   const auto *BinCmpRewritten =
183       Result.Nodes.getNodeAs<CXXRewrittenBinaryOperator>("BinCmp");
184   const auto *BinaryOp = Result.Nodes.getNodeAs<BinaryOperator>("SizeBinaryOp");
185   const auto *Pointee = Result.Nodes.getNodeAs<Expr>("Pointee");
186   const auto *E =
187       MemberCallObject
188           ? MemberCallObject
189           : (Pointee ? Pointee : Result.Nodes.getNodeAs<Expr>("STLObject"));
190   FixItHint Hint;
191   std::string ReplacementText = std::string(
192       Lexer::getSourceText(CharSourceRange::getTokenRange(E->getSourceRange()),
193                            *Result.SourceManager, getLangOpts()));
194   if (IsBinaryOrTernary(E) || isa<UnaryOperator>(E)) {
195     ReplacementText = "(" + ReplacementText + ")";
196   }
197   if (E->getType()->isPointerType())
198     ReplacementText += "->empty()";
199   else
200     ReplacementText += ".empty()";
201 
202   if (BinCmp) {
203     if (BinCmp->getOperator() == OO_ExclaimEqual) {
204       ReplacementText = "!" + ReplacementText;
205     }
206     Hint =
207         FixItHint::CreateReplacement(BinCmp->getSourceRange(), ReplacementText);
208   } else if (BinCmpTempl) {
209     if (BinCmpTempl->getOpcode() == BinaryOperatorKind::BO_NE) {
210       ReplacementText = "!" + ReplacementText;
211     }
212     Hint = FixItHint::CreateReplacement(BinCmpTempl->getSourceRange(),
213                                         ReplacementText);
214   } else if (BinCmpRewritten) {
215     if (BinCmpRewritten->getOpcode() == BinaryOperatorKind::BO_NE) {
216       ReplacementText = "!" + ReplacementText;
217     }
218     Hint = FixItHint::CreateReplacement(BinCmpRewritten->getSourceRange(),
219                                         ReplacementText);
220   } else if (BinaryOp) { // Determine the correct transformation.
221     bool Negation = false;
222     const bool ContainerIsLHS =
223         !llvm::isa<IntegerLiteral>(BinaryOp->getLHS()->IgnoreImpCasts());
224     const auto OpCode = BinaryOp->getOpcode();
225     uint64_t Value = 0;
226     if (ContainerIsLHS) {
227       if (const auto *Literal = llvm::dyn_cast<IntegerLiteral>(
228               BinaryOp->getRHS()->IgnoreImpCasts()))
229         Value = Literal->getValue().getLimitedValue();
230       else
231         return;
232     } else {
233       Value =
234           llvm::dyn_cast<IntegerLiteral>(BinaryOp->getLHS()->IgnoreImpCasts())
235               ->getValue()
236               .getLimitedValue();
237     }
238 
239     // Constant that is not handled.
240     if (Value > 1)
241       return;
242 
243     if (Value == 1 && (OpCode == BinaryOperatorKind::BO_EQ ||
244                        OpCode == BinaryOperatorKind::BO_NE))
245       return;
246 
247     // Always true, no warnings for that.
248     if ((OpCode == BinaryOperatorKind::BO_GE && Value == 0 && ContainerIsLHS) ||
249         (OpCode == BinaryOperatorKind::BO_LE && Value == 0 && !ContainerIsLHS))
250       return;
251 
252     // Do not warn for size > 1, 1 < size, size <= 1, 1 >= size.
253     if (Value == 1) {
254       if ((OpCode == BinaryOperatorKind::BO_GT && ContainerIsLHS) ||
255           (OpCode == BinaryOperatorKind::BO_LT && !ContainerIsLHS))
256         return;
257       if ((OpCode == BinaryOperatorKind::BO_LE && ContainerIsLHS) ||
258           (OpCode == BinaryOperatorKind::BO_GE && !ContainerIsLHS))
259         return;
260     }
261 
262     if (OpCode == BinaryOperatorKind::BO_NE && Value == 0)
263       Negation = true;
264     if ((OpCode == BinaryOperatorKind::BO_GT ||
265          OpCode == BinaryOperatorKind::BO_GE) &&
266         ContainerIsLHS)
267       Negation = true;
268     if ((OpCode == BinaryOperatorKind::BO_LT ||
269          OpCode == BinaryOperatorKind::BO_LE) &&
270         !ContainerIsLHS)
271       Negation = true;
272 
273     if (Negation)
274       ReplacementText = "!" + ReplacementText;
275     Hint = FixItHint::CreateReplacement(BinaryOp->getSourceRange(),
276                                         ReplacementText);
277 
278   } else {
279     // If there is a conversion above the size call to bool, it is safe to just
280     // replace size with empty.
281     if (const auto *UnaryOp =
282             Result.Nodes.getNodeAs<UnaryOperator>("NegOnSize"))
283       Hint = FixItHint::CreateReplacement(UnaryOp->getSourceRange(),
284                                           ReplacementText);
285     else
286       Hint = FixItHint::CreateReplacement(MemberCall->getSourceRange(),
287                                           "!" + ReplacementText);
288   }
289 
290   auto WarnLoc = MemberCall ? MemberCall->getBeginLoc() : SourceLocation{};
291 
292   if (WarnLoc.isValid()) {
293     diag(WarnLoc, "the 'empty' method should be used to check "
294                   "for emptiness instead of 'size'")
295         << Hint;
296   } else {
297     WarnLoc = BinCmpTempl
298                   ? BinCmpTempl->getBeginLoc()
299                   : (BinCmp ? BinCmp->getBeginLoc()
300                             : (BinCmpRewritten ? BinCmpRewritten->getBeginLoc()
301                                                : SourceLocation{}));
302     diag(WarnLoc, "the 'empty' method should be used to check "
303                   "for emptiness instead of comparing to an empty object")
304         << Hint;
305   }
306 
307   const auto *Container = Result.Nodes.getNodeAs<NamedDecl>("container");
308   if (const auto *CTS = dyn_cast<ClassTemplateSpecializationDecl>(Container)) {
309     // The definition of the empty() method is the same for all implicit
310     // instantiations. In order to avoid duplicate or inconsistent warnings
311     // (depending on how deduplication is done), we use the same class name
312     // for all implicit instantiations of a template.
313     if (CTS->getSpecializationKind() == TSK_ImplicitInstantiation)
314       Container = CTS->getSpecializedTemplate();
315   }
316   const auto *Empty = Result.Nodes.getNodeAs<FunctionDecl>("empty");
317 
318   diag(Empty->getLocation(), "method %0::empty() defined here",
319        DiagnosticIDs::Note)
320       << Container;
321 }
322 
323 } // namespace readability
324 } // namespace tidy
325 } // namespace clang
326