xref: /llvm-project/clang-tools-extra/clang-tidy/modernize/UseStartsEndsWithCheck.cpp (revision 2f02b5af6ecb973d3a7faad9b0daff22646e724d)
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 
18 using namespace clang::ast_matchers;
19 
20 namespace clang::tidy::modernize {
21 
22 static 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 
36 struct NotLengthExprForStringNode {
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 
83 private:
84   std::string ID;
85   DynTypedNode Node;
86   ASTContext *Context;
87 };
88 
89 AST_MATCHER_P(Expr, lengthExprForStringNode, std::string, ID) {
90   return Builder->removeBindings(NotLengthExprForStringNode(
91       ID, DynTypedNode::create(Node), &(Finder->getASTContext())));
92 }
93 
94 UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,
95                                                ClangTidyContext *Context)
96     : ClangTidyCheck(Name, Context) {}
97 
98 void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {
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 
207 void 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
257