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

Provided by KDAB

Privacy Policy
Update your C++ knowledge – Modern C++11/14/17 Training
Find out more

source code of clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp