xref: /llvm-project/clang-tools-extra/clang-tidy/abseil/StringFindStartswithCheck.cpp (revision fc2a9ad10e21bda3dafbb85d8317ef5e3e5f99a1)
1 //===--- StringFindStartswithCheck.cc - clang-tidy---------------*- C++ -*-===//
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 "StringFindStartswithCheck.h"
10 
11 #include "../utils/OptionsUtils.h"
12 #include "clang/AST/ASTContext.h"
13 #include "clang/ASTMatchers/ASTMatchers.h"
14 #include "clang/Frontend/CompilerInstance.h"
15 #include "clang/Lex/Lexer.h"
16 #include "clang/Lex/Preprocessor.h"
17 
18 using namespace clang::ast_matchers;
19 
20 namespace clang::tidy::abseil {
21 
22 StringFindStartswithCheck::StringFindStartswithCheck(StringRef Name,
23                                                      ClangTidyContext *Context)
24     : ClangTidyCheck(Name, Context),
25       StringLikeClasses(utils::options::parseStringList(
26           Options.get("StringLikeClasses", "::std::basic_string"))),
27       IncludeInserter(Options.getLocalOrGlobal("IncludeStyle",
28                                                utils::IncludeSorter::IS_LLVM),
29                       areDiagsSelfContained()),
30       AbseilStringsMatchHeader(
31           Options.get("AbseilStringsMatchHeader", "absl/strings/match.h")) {}
32 
33 void StringFindStartswithCheck::registerMatchers(MatchFinder *Finder) {
34   auto ZeroLiteral = integerLiteral(equals(0));
35   auto StringClassMatcher = cxxRecordDecl(hasAnyName(StringLikeClasses));
36   auto StringType = hasUnqualifiedDesugaredType(
37       recordType(hasDeclaration(StringClassMatcher)));
38 
39   auto StringFind = cxxMemberCallExpr(
40       // .find()-call on a string...
41       callee(cxxMethodDecl(hasName("find")).bind("findfun")),
42       on(hasType(StringType)),
43       // ... with some search expression ...
44       hasArgument(0, expr().bind("needle")),
45       // ... and either "0" as second argument or the default argument (also 0).
46       anyOf(hasArgument(1, ZeroLiteral), hasArgument(1, cxxDefaultArgExpr())));
47 
48   Finder->addMatcher(
49       // Match [=!]= with a zero on one side and a string.find on the other.
50       binaryOperator(
51           hasAnyOperatorName("==", "!="),
52           hasOperands(ignoringParenImpCasts(ZeroLiteral),
53                       ignoringParenImpCasts(StringFind.bind("findexpr"))))
54           .bind("expr"),
55       this);
56 
57   auto StringRFind = cxxMemberCallExpr(
58       // .rfind()-call on a string...
59       callee(cxxMethodDecl(hasName("rfind")).bind("findfun")),
60       on(hasType(StringType)),
61       // ... with some search expression ...
62       hasArgument(0, expr().bind("needle")),
63       // ... and "0" as second argument.
64       hasArgument(1, ZeroLiteral));
65 
66   Finder->addMatcher(
67       // Match [=!]= with either a zero or npos on one side and a string.rfind
68       // on the other.
69       binaryOperator(
70           hasAnyOperatorName("==", "!="),
71           hasOperands(ignoringParenImpCasts(ZeroLiteral),
72                       ignoringParenImpCasts(StringRFind.bind("findexpr"))))
73           .bind("expr"),
74       this);
75 }
76 
77 void StringFindStartswithCheck::check(const MatchFinder::MatchResult &Result) {
78   const ASTContext &Context = *Result.Context;
79   const SourceManager &Source = Context.getSourceManager();
80 
81   // Extract matching (sub)expressions
82   const auto *ComparisonExpr = Result.Nodes.getNodeAs<BinaryOperator>("expr");
83   assert(ComparisonExpr != nullptr);
84   const auto *Needle = Result.Nodes.getNodeAs<Expr>("needle");
85   assert(Needle != nullptr);
86   const Expr *Haystack = Result.Nodes.getNodeAs<CXXMemberCallExpr>("findexpr")
87                              ->getImplicitObjectArgument();
88   assert(Haystack != nullptr);
89   const auto *FindFun = Result.Nodes.getNodeAs<CXXMethodDecl>("findfun");
90   assert(FindFun != nullptr);
91 
92   bool Rev = FindFun->getName().contains("rfind");
93 
94   if (ComparisonExpr->getBeginLoc().isMacroID())
95     return;
96 
97   // Get the source code blocks (as characters) for both the string object
98   // and the search expression
99   const StringRef NeedleExprCode = Lexer::getSourceText(
100       CharSourceRange::getTokenRange(Needle->getSourceRange()), Source,
101       Context.getLangOpts());
102   const StringRef HaystackExprCode = Lexer::getSourceText(
103       CharSourceRange::getTokenRange(Haystack->getSourceRange()), Source,
104       Context.getLangOpts());
105 
106   // Create the StartsWith string, negating if comparison was "!=".
107   bool Neg = ComparisonExpr->getOpcode() == BO_NE;
108 
109   // Create the warning message and a FixIt hint replacing the original expr.
110   auto Diagnostic =
111       diag(ComparisonExpr->getBeginLoc(),
112            "use %select{absl::StartsWith|!absl::StartsWith}0 "
113            "instead of %select{find()|rfind()}1 %select{==|!=}0 0")
114       << Neg << Rev;
115 
116   Diagnostic << FixItHint::CreateReplacement(
117       ComparisonExpr->getSourceRange(),
118       ((Neg ? "!absl::StartsWith(" : "absl::StartsWith(") + HaystackExprCode +
119        ", " + NeedleExprCode + ")")
120           .str());
121 
122   // Create a preprocessor #include FixIt hint (createIncludeInsertion checks
123   // whether this already exists).
124   Diagnostic << IncludeInserter.createIncludeInsertion(
125       Source.getFileID(ComparisonExpr->getBeginLoc()),
126       AbseilStringsMatchHeader);
127 }
128 
129 void StringFindStartswithCheck::registerPPCallbacks(
130     const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
131   IncludeInserter.registerPreprocessor(PP);
132 }
133 
134 void StringFindStartswithCheck::storeOptions(
135     ClangTidyOptions::OptionMap &Opts) {
136   Options.store(Opts, "StringLikeClasses",
137                 utils::options::serializeStringList(StringLikeClasses));
138   Options.store(Opts, "IncludeStyle", IncludeInserter.getStyle());
139   Options.store(Opts, "AbseilStringsMatchHeader", AbseilStringsMatchHeader);
140 }
141 
142 } // namespace clang::tidy::abseil
143