xref: /llvm-project/clang-tools-extra/clang-tidy/abseil/FasterStrsplitDelimiterCheck.cpp (revision 76bbbcb41bcf4a1d7a26bb11b78cf97b60ea7d4b)
1 //===--- FasterStrsplitDelimiterCheck.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 "FasterStrsplitDelimiterCheck.h"
10 #include "clang/AST/ASTContext.h"
11 #include "clang/ASTMatchers/ASTMatchFinder.h"
12 #include "clang/Tooling/FixIt.h"
13 #include <optional>
14 
15 using namespace clang::ast_matchers;
16 
17 namespace clang::tidy::abseil {
18 
19 namespace {
20 
AST_MATCHER(StringLiteral,lengthIsOne)21 AST_MATCHER(StringLiteral, lengthIsOne) { return Node.getLength() == 1; }
22 
makeCharacterLiteral(const StringLiteral * Literal,const ASTContext & Context)23 std::optional<std::string> makeCharacterLiteral(const StringLiteral *Literal,
24                                                 const ASTContext &Context) {
25   assert(Literal->getLength() == 1 &&
26          "Only single character string should be matched");
27   assert(Literal->getCharByteWidth() == 1 &&
28          "StrSplit doesn't support wide char");
29   std::string Result = clang::tooling::fixit::getText(*Literal, Context).str();
30   bool IsRawStringLiteral = StringRef(Result).starts_with(R"(R")");
31   // Since raw string literal might contain unescaped non-printable characters,
32   // we normalize them using `StringLiteral::outputString`.
33   if (IsRawStringLiteral) {
34     Result.clear();
35     llvm::raw_string_ostream Stream(Result);
36     Literal->outputString(Stream);
37   }
38   // Special case: If the string contains a single quote, we just need to return
39   // a character of the single quote. This is a special case because we need to
40   // escape it in the character literal.
41   if (Result == R"("'")")
42     return std::string(R"('\'')");
43 
44   // Now replace the " with '.
45   std::string::size_type Pos = Result.find_first_of('"');
46   if (Pos == std::string::npos)
47     return std::nullopt;
48   Result[Pos] = '\'';
49   Pos = Result.find_last_of('"');
50   if (Pos == std::string::npos)
51     return std::nullopt;
52   Result[Pos] = '\'';
53   return Result;
54 }
55 
56 } // anonymous namespace
57 
registerMatchers(MatchFinder * Finder)58 void FasterStrsplitDelimiterCheck::registerMatchers(MatchFinder *Finder) {
59   // Binds to one character string literals.
60   const auto SingleChar =
61       expr(ignoringParenCasts(stringLiteral(lengthIsOne()).bind("Literal")));
62 
63   // Binds to a string_view (either absl or std) that was passed by value and
64   // constructed from string literal.
65   auto StringViewArg = ignoringElidableConstructorCall(ignoringImpCasts(
66       cxxConstructExpr(hasType(recordDecl(hasName("::absl::string_view"))),
67                        hasArgument(0, ignoringParenImpCasts(SingleChar)))));
68 
69   // Need to ignore the elidable constructor as otherwise there is no match for
70   // c++14 and earlier.
71   auto ByAnyCharArg =
72       expr(has(ignoringElidableConstructorCall(
73                ignoringParenCasts(cxxBindTemporaryExpr(has(cxxConstructExpr(
74                    hasType(recordDecl(hasName("::absl::ByAnyChar"))),
75                    hasArgument(0, StringViewArg))))))))
76           .bind("ByAnyChar");
77 
78   // Find uses of absl::StrSplit(..., "x") and absl::StrSplit(...,
79   // absl::ByAnyChar("x")) to transform them into absl::StrSplit(..., 'x').
80   Finder->addMatcher(
81       traverse(TK_AsIs,
82                callExpr(callee(functionDecl(hasName("::absl::StrSplit"))),
83                         hasArgument(1, anyOf(ByAnyCharArg, SingleChar)),
84                         unless(isInTemplateInstantiation()))
85                    .bind("StrSplit")),
86       this);
87 
88   // Find uses of absl::MaxSplits("x", N) and
89   // absl::MaxSplits(absl::ByAnyChar("x"), N) to transform them into
90   // absl::MaxSplits('x', N).
91   Finder->addMatcher(
92       traverse(TK_AsIs,
93                callExpr(callee(functionDecl(hasName("::absl::MaxSplits"))),
94                         hasArgument(0, anyOf(ByAnyCharArg,
95                                              ignoringParenCasts(SingleChar))),
96                         unless(isInTemplateInstantiation()))),
97       this);
98 }
99 
check(const MatchFinder::MatchResult & Result)100 void FasterStrsplitDelimiterCheck::check(
101     const MatchFinder::MatchResult &Result) {
102   const auto *Literal = Result.Nodes.getNodeAs<StringLiteral>("Literal");
103 
104   if (Literal->getBeginLoc().isMacroID() || Literal->getEndLoc().isMacroID())
105     return;
106 
107   std::optional<std::string> Replacement =
108       makeCharacterLiteral(Literal, *Result.Context);
109   if (!Replacement)
110     return;
111   SourceRange Range = Literal->getSourceRange();
112 
113   if (const auto *ByAnyChar = Result.Nodes.getNodeAs<Expr>("ByAnyChar"))
114     Range = ByAnyChar->getSourceRange();
115 
116   diag(
117       Literal->getBeginLoc(),
118       "%select{absl::StrSplit()|absl::MaxSplits()}0 called with a string "
119       "literal "
120       "consisting of a single character; consider using the character overload")
121       << (Result.Nodes.getNodeAs<CallExpr>("StrSplit") ? 0 : 1)
122       << FixItHint::CreateReplacement(CharSourceRange::getTokenRange(Range),
123                                       *Replacement);
124 }
125 
126 } // namespace clang::tidy::abseil
127