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

source code of clang-tools-extra/clang-tidy/readability/ContainerSizeEmptyCheck.cpp