| 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 | |