clang-tools 23.0.0git
MissingEndComparisonCheck.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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
11#include "clang/AST/ASTContext.h"
12#include "clang/ASTMatchers/ASTMatchFinder.h"
13#include "clang/Lex/Lexer.h"
14#include "clang/Tooling/FixIt.h"
15
16using namespace clang::ast_matchers;
17
18namespace clang::tidy::bugprone {
19
20static constexpr llvm::StringRef IteratorAlgorithms[] = {
21 "::std::find", "::std::find_if",
22 "::std::find_if_not", "::std::search",
23 "::std::search_n", "::std::find_end",
24 "::std::find_first_of", "::std::lower_bound",
25 "::std::upper_bound", "::std::partition_point",
26 "::std::min_element", "::std::max_element",
27 "::std::adjacent_find", "::std::is_sorted_until"};
28
29static constexpr llvm::StringRef RangeAlgorithms[] = {
30 "::std::ranges::find", "::std::ranges::find_if",
31 "::std::ranges::find_if_not", "::std::ranges::lower_bound",
32 "::std::ranges::upper_bound", "::std::ranges::min_element",
33 "::std::ranges::max_element", "::std::ranges::find_first_of",
34 "::std::ranges::adjacent_find", "::std::ranges::is_sorted_until"};
35
37 ClangTidyContext *Context)
38 : ClangTidyCheck(Name, Context),
39 ExtraAlgorithms(
40 utils::options::parseStringList(Options.get("ExtraAlgorithms", ""))) {
41}
42
45 Options.store(Opts, "ExtraAlgorithms",
47}
48
49static std::optional<std::string> getRangesEndText(const ASTContext &Context,
50 const CallExpr *Call) {
51 const FunctionDecl *Callee = Call->getDirectCallee();
52 assert(Callee && Callee->getNumParams() > 0 &&
53 "Matcher should ensure Callee has parameters");
54
55 // Range overloads take a reference (R&&), Iterator overloads pass by value.
56 const bool IsIterPair =
57 !Callee->getParamDecl(0)->getType()->isReferenceType();
58
59 if (IsIterPair) {
60 if (Call->getNumArgs() < 3)
61 return std::nullopt;
62 // CPO(Iter, Sent, Val...) -> Sent is Arg 2.
63 const Expr *EndArg = Call->getArg(2);
64 return tooling::fixit::getText(*EndArg, Context).str();
65 }
66
67 if (Call->getNumArgs() < 2)
68 return std::nullopt;
69 // CPO(Range, Val, Proj) -> Range is Arg 1.
70 const Expr *RangeArg = Call->getArg(1);
71 // Avoid potential side-effects
72 const Expr *InnerRange = RangeArg->IgnoreParenImpCasts();
73 if (isa<DeclRefExpr, MemberExpr>(InnerRange)) {
74 const StringRef RangeText = tooling::fixit::getText(*RangeArg, Context);
75 if (!RangeText.empty())
76 return ("std::ranges::end(" + RangeText + ")").str();
77 }
78 return "";
79}
80
81static std::optional<std::string> getStandardEndText(ASTContext &Context,
82 const CallExpr *Call) {
83 if (Call->getNumArgs() < 2)
84 return std::nullopt;
85
86 // Heuristic: if the first argument is a record type and the types of the
87 // first two arguments are distinct, we assume it's a range algorithm.
88 if (Call->getNumArgs() == 2) {
89 const Expr *Arg0 = Call->getArg(0);
90 const Expr *Arg1 = Call->getArg(1);
91 const QualType T0 = Arg0->getType().getCanonicalType();
92 const QualType T1 = Arg1->getType().getCanonicalType();
93
94 if (T0 != T1 && T0.getNonReferenceType()->isRecordType()) {
95 const StringRef ContainerText = tooling::fixit::getText(*Arg0, Context);
96 if (!ContainerText.empty())
97 return ("std::end(" + ContainerText + ")").str();
98 }
99 }
100
101 unsigned EndIdx = 1;
102 const Expr *FirstArg = Call->getArg(0);
103 if (const auto *Record =
104 FirstArg->getType().getNonReferenceType()->getAsCXXRecordDecl()) {
105 if (Record->getIdentifier() && Record->getName().ends_with("_policy"))
106 EndIdx = 2;
107 }
108
109 if (Call->getNumArgs() <= EndIdx)
110 return std::nullopt;
111
112 const Expr *EndArg = Call->getArg(EndIdx);
113 // Filters nullptr, we assume the intent might be a valid check against null
114 if (EndArg->IgnoreParenCasts()->isNullPointerConstant(
115 Context, Expr::NPC_ValueDependentIsNull))
116 return std::nullopt;
117
118 return tooling::fixit::getText(*EndArg, Context).str();
119}
120
122 llvm::SmallVector<StringRef, 32> ExpandedIteratorAlgorithms;
123 ExpandedIteratorAlgorithms.append(std::begin(IteratorAlgorithms),
124 std::end(IteratorAlgorithms));
125 ExpandedIteratorAlgorithms.append(ExtraAlgorithms.begin(),
126 ExtraAlgorithms.end());
127
128 const auto StdAlgoCall = callExpr(callee(functionDecl(
129 hasAnyName(ExpandedIteratorAlgorithms), unless(parameterCountIs(0)))));
130
131 // Captures customization point object
132 const auto RangesCall = cxxOperatorCallExpr(
133 hasOverloadedOperatorName("()"),
134 callee(cxxMethodDecl(unless(parameterCountIs(0)))),
135 hasArgument(0, declRefExpr(to(
136 varDecl(hasAnyName(RangeAlgorithms)).bind("cpo")))));
137
138 const auto AnyAlgoCall =
139 getLangOpts().CPlusPlus20
140 ? expr(anyOf(StdAlgoCall, RangesCall)).bind("algoCall")
141 : expr(StdAlgoCall).bind("algoCall");
142
143 // Captures implicit pointer-to-bool casts and operator bool() calls.
144 const auto IsBoolUsage = anyOf(
145 implicitCastExpr(hasCastKind(CK_PointerToBoolean),
146 hasSourceExpression(ignoringParenImpCasts(AnyAlgoCall))),
147 cxxMemberCallExpr(callee(cxxConversionDecl(returns(booleanType()))),
148 on(ignoringParenImpCasts(AnyAlgoCall))));
149
150 // Captures variable usage: `auto it = std::find(...); if (it)`
151 // FIXME: This only handles variables initialized directly by the algorithm.
152 // We may need to introduce more accurate dataflow analysis in the future.
153 const auto VarWithAlgoInit =
154 varDecl(decl().bind("initVar"),
155 hasInitializer(expr(ignoringParenImpCasts(AnyAlgoCall))),
156 optionally(hasParent(declStmt(
157 hasParent(mapAnyOf(ifStmt, whileStmt, forStmt)
158 .with(hasConditionVariableStatement(declStmt(
159 has(varDecl(equalsBoundNode("initVar"))))))
160 .bind("condVarParent"))))));
161
162 const auto IsVariableBoolUsage =
163 anyOf(implicitCastExpr(hasCastKind(CK_PointerToBoolean),
164 hasSourceExpression(ignoringParenImpCasts(
165 declRefExpr(to(VarWithAlgoInit))))),
166 cxxMemberCallExpr(
167 callee(cxxConversionDecl(returns(booleanType()))),
168 on(ignoringParenImpCasts(declRefExpr(to(VarWithAlgoInit))))));
169
170 const auto BoolUsage =
171 expr(anyOf(IsBoolUsage, IsVariableBoolUsage),
172 optionally(hasParent(varDecl().bind("boolOpParentVar"))));
173
174 Finder->addMatcher(
175 unaryOperator(hasOperatorName("!"),
176 hasUnaryOperand(ignoringParens(BoolUsage.bind("boolOp"))))
177 .bind("Neg"),
178 this);
179
180 Finder->addMatcher(
181 expr(BoolUsage,
182 unless(hasAncestor(unaryOperator(
183 hasOperatorName("!"), hasUnaryOperand(ignoringParens(
184 expr(equalsBoundNode("boolOp"))))))))
185 .bind("boolOp"),
186 this);
187}
188
189void MissingEndComparisonCheck::check(const MatchFinder::MatchResult &Result) {
190 const auto *BoolOp = Result.Nodes.getNodeAs<Expr>("boolOp");
191 assert(BoolOp);
192
193 std::optional<std::string> EndExprText;
194
195 if (Result.Nodes.getNodeAs<VarDecl>("cpo")) {
196 const auto *Call = Result.Nodes.getNodeAs<CallExpr>("algoCall");
197 EndExprText = getRangesEndText(*Result.Context, Call);
198 } else if (const auto *Call = Result.Nodes.getNodeAs<CallExpr>("algoCall")) {
199 EndExprText = getStandardEndText(*Result.Context, Call);
200 } else {
201 llvm_unreachable("Matcher should bind 'algoCall' or 'cpo'");
202 }
203
204 if (!EndExprText)
205 return;
206
207 auto Diag = diag(BoolOp->getBeginLoc(),
208 "result of standard algorithm used as 'bool'; did you "
209 "mean to compare with the end iterator?");
210
211 if (EndExprText->empty())
212 return;
213
214 // Suppress fix-it if the expression is part of a variable declaration or a
215 // condition variable declaration: a correct fix may need to rewrite the
216 // declaration or the enclosing condition, not just the matched expression.
217 if (const auto *InitVar = Result.Nodes.getNodeAs<VarDecl>("initVar");
218 InitVar && (InitVar->getType()->isBooleanType() ||
219 Result.Nodes.getNodeAs<Stmt>("condVarParent")))
220 return;
221
222 if (Result.Nodes.getNodeAs<VarDecl>("boolOpParentVar"))
223 return;
224
225 const SourceLocation AfterBoolOp =
226 Lexer::getLocForEndOfToken(BoolOp->getEndLoc(), 0, *Result.SourceManager,
227 Result.Context->getLangOpts());
228
229 const auto *NotOp = Result.Nodes.getNodeAs<UnaryOperator>("Neg");
230
231 if (NotOp)
232 Diag << FixItHint::CreateReplacement(NotOp->getOperatorLoc(), "(");
233 else
234 Diag << FixItHint::CreateInsertion(BoolOp->getBeginLoc(), "(");
235
236 Diag << FixItHint::CreateInsertion(
237 AfterBoolOp,
238 (llvm::Twine(NotOp ? " == " : " != ") + *EndExprText + ")").str());
239}
240
241} // namespace clang::tidy::bugprone
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
MissingEndComparisonCheck(StringRef Name, ClangTidyContext *Context)
void registerMatchers(ast_matchers::MatchFinder *Finder) override
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
void storeOptions(ClangTidyOptions::OptionMap &Opts) override
static std::optional< std::string > getRangesEndText(const ASTContext &Context, const CallExpr *Call)
static std::optional< std::string > getStandardEndText(ASTContext &Context, const CallExpr *Call)
static constexpr llvm::StringRef IteratorAlgorithms[]
static constexpr llvm::StringRef RangeAlgorithms[]
std::string serializeStringList(ArrayRef< StringRef > Strings)
Serialize a sequence of names that can be parsed by parseStringList.
llvm::StringMap< ClangTidyValue > OptionMap