clang-tools 20.0.0git
UseStartsEndsWithCheck.cpp
Go to the documentation of this file.
1//===--- UseStartsEndsWithCheck.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
10
11#include "../utils/ASTUtils.h"
12#include "../utils/Matchers.h"
13#include "clang/ASTMatchers/ASTMatchers.h"
14#include "clang/Lex/Lexer.h"
15
16#include <string>
17
18using namespace clang::ast_matchers;
19
20namespace clang::tidy::modernize {
21
22static bool isNegativeComparison(const Expr *ComparisonExpr) {
23 if (const auto *Op = llvm::dyn_cast<BinaryOperator>(ComparisonExpr))
24 return Op->getOpcode() == BO_NE;
25
26 if (const auto *Op = llvm::dyn_cast<CXXOperatorCallExpr>(ComparisonExpr))
27 return Op->getOperator() == OO_ExclaimEqual;
28
29 if (const auto *Op =
30 llvm::dyn_cast<CXXRewrittenBinaryOperator>(ComparisonExpr))
31 return Op->getOperator() == BO_NE;
32
33 return false;
34}
35
37 NotLengthExprForStringNode(std::string ID, DynTypedNode Node,
38 ASTContext *Context)
39 : ID(std::move(ID)), Node(std::move(Node)), Context(Context) {}
40 bool operator()(const internal::BoundNodesMap &Nodes) const {
41 // Match a string literal and an integer size or strlen() call.
42 if (const auto *StringLiteralNode = Nodes.getNodeAs<StringLiteral>(ID)) {
43 if (const auto *IntegerLiteralSizeNode = Node.get<IntegerLiteral>()) {
44 return StringLiteralNode->getLength() !=
45 IntegerLiteralSizeNode->getValue().getZExtValue();
46 }
47
48 if (const auto *StrlenNode = Node.get<CallExpr>()) {
49 if (StrlenNode->getDirectCallee()->getName() != "strlen" ||
50 StrlenNode->getNumArgs() != 1) {
51 return true;
52 }
53
54 if (const auto *StrlenArgNode = dyn_cast<StringLiteral>(
55 StrlenNode->getArg(0)->IgnoreParenImpCasts())) {
56 return StrlenArgNode->getLength() != StringLiteralNode->getLength();
57 }
58 }
59 }
60
61 // Match a string variable and a call to length() or size().
62 if (const auto *ExprNode = Nodes.getNodeAs<Expr>(ID)) {
63 if (const auto *MemberCallNode = Node.get<CXXMemberCallExpr>()) {
64 const CXXMethodDecl *MethodDeclNode = MemberCallNode->getMethodDecl();
65 const StringRef Name = MethodDeclNode->getName();
66 if (!MethodDeclNode->isConst() || MethodDeclNode->getNumParams() != 0 ||
67 (Name != "size" && Name != "length")) {
68 return true;
69 }
70
71 if (const auto *OnNode =
72 dyn_cast<Expr>(MemberCallNode->getImplicitObjectArgument())) {
73 return !utils::areStatementsIdentical(OnNode->IgnoreParenImpCasts(),
74 ExprNode->IgnoreParenImpCasts(),
75 *Context);
76 }
77 }
78 }
79
80 return true;
81 }
82
83private:
84 std::string ID;
85 DynTypedNode Node;
86 ASTContext *Context;
87};
88
89AST_MATCHER_P(Expr, lengthExprForStringNode, std::string, ID) {
90 return Builder->removeBindings(NotLengthExprForStringNode(
91 ID, DynTypedNode::create(Node), &(Finder->getASTContext())));
92}
93
95 ClangTidyContext *Context)
96 : ClangTidyCheck(Name, Context) {}
97
99 const auto ZeroLiteral = integerLiteral(equals(0));
100
101 const auto ClassTypeWithMethod = [](const StringRef MethodBoundName,
102 const auto... Methods) {
103 return cxxRecordDecl(anyOf(
104 hasMethod(cxxMethodDecl(isConst(), parameterCountIs(1),
105 returns(booleanType()), hasAnyName(Methods))
106 .bind(MethodBoundName))...));
107 };
108
109 const auto OnClassWithStartsWithFunction =
110 ClassTypeWithMethod("starts_with_fun", "starts_with", "startsWith",
111 "startswith", "StartsWith");
112
113 const auto OnClassWithEndsWithFunction = ClassTypeWithMethod(
114 "ends_with_fun", "ends_with", "endsWith", "endswith", "EndsWith");
115
116 // Case 1: X.find(Y) [!=]= 0 -> starts_with.
117 const auto FindExpr = cxxMemberCallExpr(
118 anyOf(argumentCountIs(1), hasArgument(1, ZeroLiteral)),
119 callee(
120 cxxMethodDecl(hasName("find"), ofClass(OnClassWithStartsWithFunction))
121 .bind("find_fun")),
122 hasArgument(0, expr().bind("needle")));
123
124 // Case 2: X.rfind(Y, 0) [!=]= 0 -> starts_with.
125 const auto RFindExpr = cxxMemberCallExpr(
126 hasArgument(1, ZeroLiteral),
127 callee(cxxMethodDecl(hasName("rfind"),
128 ofClass(OnClassWithStartsWithFunction))
129 .bind("find_fun")),
130 hasArgument(0, expr().bind("needle")));
131
132 // Case 3: X.compare(0, LEN(Y), Y) [!=]= 0 -> starts_with.
133 const auto CompareExpr = cxxMemberCallExpr(
134 argumentCountIs(3), hasArgument(0, ZeroLiteral),
135 callee(cxxMethodDecl(hasName("compare"),
136 ofClass(OnClassWithStartsWithFunction))
137 .bind("find_fun")),
138 hasArgument(2, expr().bind("needle")),
139 hasArgument(1, lengthExprForStringNode("needle")));
140
141 // Case 4: X.compare(LEN(X) - LEN(Y), LEN(Y), Y) [!=]= 0 -> ends_with.
142 const auto CompareEndsWithExpr = cxxMemberCallExpr(
143 argumentCountIs(3),
144 callee(cxxMethodDecl(hasName("compare"),
145 ofClass(OnClassWithEndsWithFunction))
146 .bind("find_fun")),
147 on(expr().bind("haystack")), hasArgument(2, expr().bind("needle")),
148 hasArgument(1, lengthExprForStringNode("needle")),
149 hasArgument(0,
150 binaryOperator(hasOperatorName("-"),
151 hasLHS(lengthExprForStringNode("haystack")),
152 hasRHS(lengthExprForStringNode("needle")))));
153
154 // All cases comparing to 0.
155 Finder->addMatcher(
156 binaryOperator(
157 matchers::isEqualityOperator(),
158 hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr,
159 CompareEndsWithExpr))
160 .bind("find_expr"),
161 ZeroLiteral))
162 .bind("expr"),
163 this);
164
165 // Case 5: X.rfind(Y) [!=]= LEN(X) - LEN(Y) -> ends_with.
166 Finder->addMatcher(
167 binaryOperator(
168 matchers::isEqualityOperator(),
169 hasOperands(
170 cxxMemberCallExpr(
171 anyOf(
172 argumentCountIs(1),
173 allOf(argumentCountIs(2),
174 hasArgument(
175 1,
176 anyOf(declRefExpr(to(varDecl(hasName("npos")))),
177 memberExpr(member(hasName("npos"))))))),
178 callee(cxxMethodDecl(hasName("rfind"),
179 ofClass(OnClassWithEndsWithFunction))
180 .bind("find_fun")),
181 on(expr().bind("haystack")),
182 hasArgument(0, expr().bind("needle")))
183 .bind("find_expr"),
184 binaryOperator(hasOperatorName("-"),
185 hasLHS(lengthExprForStringNode("haystack")),
186 hasRHS(lengthExprForStringNode("needle")))))
187 .bind("expr"),
188 this);
189
190 // Case 6: X.substr(0, LEN(Y)) [!=]= Y -> starts_with.
191 Finder->addMatcher(
192 binaryOperation(
193 hasAnyOperatorName("==", "!="),
194 hasOperands(
195 expr().bind("needle"),
196 cxxMemberCallExpr(
197 argumentCountIs(2), hasArgument(0, ZeroLiteral),
198 hasArgument(1, lengthExprForStringNode("needle")),
199 callee(cxxMethodDecl(hasName("substr"),
200 ofClass(OnClassWithStartsWithFunction))
201 .bind("find_fun")))
202 .bind("find_expr")))
203 .bind("expr"),
204 this);
205}
206
207void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {
208 const auto *ComparisonExpr = Result.Nodes.getNodeAs<Expr>("expr");
209 const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");
210 const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");
211 const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("needle");
212 const auto *StartsWithFunction =
213 Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");
214 const auto *EndsWithFunction =
215 Result.Nodes.getNodeAs<CXXMethodDecl>("ends_with_fun");
216 assert(bool(StartsWithFunction) != bool(EndsWithFunction));
217
218 const CXXMethodDecl *ReplacementFunction =
219 StartsWithFunction ? StartsWithFunction : EndsWithFunction;
220
221 if (ComparisonExpr->getBeginLoc().isMacroID() ||
222 FindExpr->getBeginLoc().isMacroID())
223 return;
224
225 // Make sure FindExpr->getArg(0) can be used to make a range in the FitItHint.
226 if (FindExpr->getNumArgs() == 0)
227 return;
228
229 // Retrieve the source text of the search expression.
230 const auto SearchExprText = Lexer::getSourceText(
231 CharSourceRange::getTokenRange(SearchExpr->getSourceRange()),
232 *Result.SourceManager, Result.Context->getLangOpts());
233
234 auto Diagnostic = diag(FindExpr->getExprLoc(), "use %0 instead of %1")
235 << ReplacementFunction->getName() << FindFun->getName();
236
237 // Remove everything before the function call.
238 Diagnostic << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
239 ComparisonExpr->getBeginLoc(), FindExpr->getBeginLoc()));
240
241 // Rename the function to `starts_with` or `ends_with`.
242 Diagnostic << FixItHint::CreateReplacement(FindExpr->getExprLoc(),
243 ReplacementFunction->getName());
244
245 // Replace arguments and everything after the function call.
246 Diagnostic << FixItHint::CreateReplacement(
247 CharSourceRange::getTokenRange(FindExpr->getArg(0)->getBeginLoc(),
248 ComparisonExpr->getEndLoc()),
249 (SearchExprText + ")").str());
250
251 // Add negation if necessary.
252 if (isNegativeComparison(ComparisonExpr))
253 Diagnostic << FixItHint::CreateInsertion(FindExpr->getBeginLoc(), "!");
254}
255
256} // namespace clang::tidy::modernize
llvm::SmallString< 256U > Name
CodeCompletionBuilder Builder
DiagnosticCallback Diagnostic
::clang::DynTypedNode Node
Base class for all clang-tidy checks.
DiagnosticBuilder diag(SourceLocation Loc, StringRef Description, DiagnosticIDs::Level Level=DiagnosticIDs::Warning)
Add a diagnostic with the check's name.
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
ClangTidyChecks that register ASTMatchers should do the actual work in here.
void registerMatchers(ast_matchers::MatchFinder *Finder) override
Override this to register AST matchers with Finder.
UseStartsEndsWithCheck(StringRef Name, ClangTidyContext *Context)
static bool isNegativeComparison(const Expr *ComparisonExpr)
AST_MATCHER_P(Expr, lengthExprForStringNode, std::string, ID)
bool areStatementsIdentical(const Stmt *FirstStmt, const Stmt *SecondStmt, const ASTContext &Context, bool Canonical)
Definition: ASTUtils.cpp:91
bool operator()(const internal::BoundNodesMap &Nodes) const
NotLengthExprForStringNode(std::string ID, DynTypedNode Node, ASTContext *Context)