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>(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 | |
36 | namespace { |
37 | |
38 | struct 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 | |
85 | private: |
86 | std::string ID; |
87 | DynTypedNode Node; |
88 | ASTContext *Context; |
89 | }; |
90 | |
91 | AST_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 | |
98 | UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name, |
99 | ClangTidyContext *Context) |
100 | : ClangTidyCheck(Name, Context) {} |
101 | |
102 | void 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 | |
223 | void 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 | |