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