273 lines · cpp
1//===----------------------------------------------------------------------===//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-exception6//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>(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 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(OnNode->IgnoreParenImpCasts(),76 ExprNode->IgnoreParenImpCasts(),77 *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(NotLengthExprForStringNode(93 ID, DynTypedNode::create(Node), &(Finder->getASTContext())));94}95 96} // namespace97 98UseStartsEndsWithCheck::UseStartsEndsWithCheck(StringRef Name,99 ClangTidyContext *Context)100 : ClangTidyCheck(Name, Context) {}101 102void UseStartsEndsWithCheck::registerMatchers(MatchFinder *Finder) {103 const auto ZeroLiteral = integerLiteral(equals(0));104 105 const auto ClassTypeWithMethod = [](const StringRef MethodBoundName,106 const auto... Methods) {107 return cxxRecordDecl(anyOf(108 hasMethod(cxxMethodDecl(isConst(), parameterCountIs(1),109 returns(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 cxxMethodDecl(hasName("find"), ofClass(OnClassWithStartsWithFunction))124 .bind("find_fun")),125 hasArgument(0, expr().bind("needle")),126 anyOf(127 // Detect the expression: X.find(Y);128 argumentCountIs(1),129 // Detect the expression: X.find(Y, 0);130 allOf(argumentCountIs(2), hasArgument(1, ZeroLiteral)),131 // Detect the expression: X.find(Y, 0, LEN(Y));132 allOf(argumentCountIs(3), hasArgument(1, ZeroLiteral),133 hasArgument(2, lengthExprForStringNode("needle")))));134 135 // Case 2: X.rfind(Y, 0, [LEN(Y)]) [!=]= 0 -> starts_with.136 const auto RFindExpr = cxxMemberCallExpr(137 callee(cxxMethodDecl(hasName("rfind"),138 ofClass(OnClassWithStartsWithFunction))139 .bind("find_fun")),140 hasArgument(0, expr().bind("needle")),141 anyOf(142 // Detect the expression: X.rfind(Y, 0);143 allOf(argumentCountIs(2), hasArgument(1, ZeroLiteral)),144 // Detect the expression: X.rfind(Y, 0, LEN(Y));145 allOf(argumentCountIs(3), hasArgument(1, ZeroLiteral),146 hasArgument(2, lengthExprForStringNode("needle")))));147 148 // Case 3: X.compare(0, LEN(Y), Y) [!=]= 0 -> starts_with.149 const auto CompareExpr = cxxMemberCallExpr(150 argumentCountIs(3), hasArgument(0, ZeroLiteral),151 callee(cxxMethodDecl(hasName("compare"),152 ofClass(OnClassWithStartsWithFunction))153 .bind("find_fun")),154 hasArgument(2, expr().bind("needle")),155 hasArgument(1, lengthExprForStringNode("needle")));156 157 // Case 4: X.compare(LEN(X) - LEN(Y), LEN(Y), Y) [!=]= 0 -> ends_with.158 const auto CompareEndsWithExpr = cxxMemberCallExpr(159 argumentCountIs(3),160 callee(cxxMethodDecl(hasName("compare"),161 ofClass(OnClassWithEndsWithFunction))162 .bind("find_fun")),163 on(expr().bind("haystack")), hasArgument(2, expr().bind("needle")),164 hasArgument(1, lengthExprForStringNode("needle")),165 hasArgument(0,166 binaryOperator(hasOperatorName("-"),167 hasLHS(lengthExprForStringNode("haystack")),168 hasRHS(lengthExprForStringNode("needle")))));169 170 // All cases comparing to 0.171 Finder->addMatcher(172 binaryOperator(173 matchers::isEqualityOperator(),174 hasOperands(cxxMemberCallExpr(anyOf(FindExpr, RFindExpr, CompareExpr,175 CompareEndsWithExpr))176 .bind("find_expr"),177 ZeroLiteral))178 .bind("expr"),179 this);180 181 // Case 5: X.rfind(Y) [!=]= LEN(X) - LEN(Y) -> ends_with.182 Finder->addMatcher(183 binaryOperator(184 matchers::isEqualityOperator(),185 hasOperands(186 cxxMemberCallExpr(187 anyOf(188 argumentCountIs(1),189 allOf(argumentCountIs(2),190 hasArgument(191 1,192 anyOf(declRefExpr(to(varDecl(hasName("npos")))),193 memberExpr(member(hasName("npos"))))))),194 callee(cxxMethodDecl(hasName("rfind"),195 ofClass(OnClassWithEndsWithFunction))196 .bind("find_fun")),197 on(expr().bind("haystack")),198 hasArgument(0, expr().bind("needle")))199 .bind("find_expr"),200 binaryOperator(hasOperatorName("-"),201 hasLHS(lengthExprForStringNode("haystack")),202 hasRHS(lengthExprForStringNode("needle")))))203 .bind("expr"),204 this);205 206 // Case 6: X.substr(0, LEN(Y)) [!=]= Y -> starts_with.207 Finder->addMatcher(208 binaryOperation(209 hasAnyOperatorName("==", "!="),210 hasOperands(211 expr().bind("needle"),212 cxxMemberCallExpr(213 argumentCountIs(2), hasArgument(0, ZeroLiteral),214 hasArgument(1, lengthExprForStringNode("needle")),215 callee(cxxMethodDecl(hasName("substr"),216 ofClass(OnClassWithStartsWithFunction))217 .bind("find_fun")))218 .bind("find_expr")))219 .bind("expr"),220 this);221}222 223void UseStartsEndsWithCheck::check(const MatchFinder::MatchResult &Result) {224 const auto *ComparisonExpr = Result.Nodes.getNodeAs<Expr>("expr");225 const auto *FindExpr = Result.Nodes.getNodeAs<CXXMemberCallExpr>("find_expr");226 const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("find_fun");227 const auto *SearchExpr = Result.Nodes.getNodeAs<Expr>("needle");228 const auto *StartsWithFunction =229 Result.Nodes.getNodeAs<CXXMethodDecl>("starts_with_fun");230 const auto *EndsWithFunction =231 Result.Nodes.getNodeAs<CXXMethodDecl>("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 CharSourceRange::getTokenRange(SearchExpr->getSourceRange()),248 *Result.SourceManager, Result.Context->getLangOpts());249 250 auto Diagnostic = diag(FindExpr->getExprLoc(), "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(FindExpr->getBeginLoc(), "!");270}271 272} // namespace clang::tidy::modernize273