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