clang-tools 24.0.0git
RawStringLiteralCheck.cpp
Go to the documentation of this file.
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-exception
6//
7//===----------------------------------------------------------------------===//
8
10#include "clang/AST/ASTContext.h"
11#include "clang/ASTMatchers/ASTMatchFinder.h"
12#include "clang/Basic/LangOptions.h"
13#include "clang/Basic/SourceManager.h"
14#include "clang/Lex/Lexer.h"
15#include "llvm/ADT/StringRef.h"
16#include <optional>
17
18using namespace clang::ast_matchers;
19
20namespace clang::tidy::modernize {
21
22static bool containsEscapes(StringRef HayStack, StringRef Escapes) {
23 size_t BackSlash = HayStack.find('\\');
24 if (BackSlash == StringRef::npos)
25 return false;
26
27 while (BackSlash != StringRef::npos) {
28 if (!Escapes.contains(HayStack[BackSlash + 1]))
29 return false;
30 BackSlash = HayStack.find('\\', BackSlash + 2);
31 }
32
33 return true;
34}
35
36static bool isRawStringLiteral(StringRef Text) {
37 // Already a raw string literal if R comes before ".
38 const size_t QuotePos = Text.find('"');
39 assert(QuotePos != StringRef::npos);
40 return (QuotePos > 0) && (Text[QuotePos - 1] == 'R');
41}
42
43// Clang synthesizes StringLiteral nodes that are not spelled as a string in
44// the source, e.g. the argument of a raw literal operator call (`12_w` is
45// treated as `operator""_w("12")`). Such a node points at a non-string token,
46// so its source text must not be analyzed as a string literal.
47static bool isSpelledAsStringLiteral(const StringLiteral *Literal,
48 const SourceManager &SM,
49 const LangOptions &LangOpts) {
50 Token T;
51 if (Lexer::getRawToken(Literal->getBeginLoc(), T, SM, LangOpts))
52 return false;
53 return tok::isStringLiteral(T.getKind());
54}
55
56static bool containsEscapedCharacters(const MatchFinder::MatchResult &Result,
57 const StringLiteral *Literal,
58 const CharsBitSet &DisallowedChars) {
59 // FIXME: Handle L"", u8"", u"" and U"" literals.
60 if (!Literal->isOrdinary())
61 return false;
62
63 for (const unsigned char C : Literal->getBytes())
64 if (DisallowedChars.test(C))
65 return false;
66
67 if (!isSpelledAsStringLiteral(Literal, *Result.SourceManager,
68 Result.Context->getLangOpts()))
69 return false;
70
71 const CharSourceRange CharRange = Lexer::makeFileCharRange(
72 CharSourceRange::getTokenRange(Literal->getSourceRange()),
73 *Result.SourceManager, Result.Context->getLangOpts());
74 const StringRef Text = Lexer::getSourceText(CharRange, *Result.SourceManager,
75 Result.Context->getLangOpts());
76 if (Text.empty() || isRawStringLiteral(Text))
77 return false;
78
79 return containsEscapes(Text, R"('\"?x01)");
80}
81
82static bool containsDelimiter(StringRef Bytes, const std::string &Delimiter) {
83 return Bytes.contains(Delimiter.empty() ? std::string(R"lit()")lit")
84 : (")" + Delimiter + R"(")"));
85}
86
88 ClangTidyContext *Context)
89 : ClangTidyCheck(Name, Context),
90 DelimiterStem(Options.get("DelimiterStem", "lit")),
91 ReplaceShorterLiterals(Options.get("ReplaceShorterLiterals", false)) {
92 // Non-printing characters are disallowed:
93 // \007 = \a bell
94 // \010 = \b backspace
95 // \011 = \t horizontal tab
96 // \012 = \n new line
97 // \013 = \v vertical tab
98 // \014 = \f form feed
99 // \015 = \r carriage return
100 // \177 = delete
101 for (const unsigned char C : StringRef("\000\001\002\003\004\005\006\a"
102 "\b\t\n\v\f\r\016\017"
103 "\020\021\022\023\024\025\026\027"
104 "\030\031\032\033\034\035\036\037"
105 "\177",
106 33))
107 DisallowedChars.set(C);
108
109 // Non-ASCII are disallowed too.
110 for (unsigned int C = 0x80U; C <= 0xFFU; ++C)
111 DisallowedChars.set(static_cast<unsigned char>(C));
112}
113
115 Options.store(Opts, "DelimiterStem", DelimiterStem);
116 Options.store(Opts, "ReplaceShorterLiterals", ReplaceShorterLiterals);
117}
118
120 Finder->addMatcher(
121 stringLiteral(unless(hasParent(predefinedExpr()))).bind("lit"), this);
122}
123
124static std::optional<StringRef>
125createUserDefinedSuffix(const StringLiteral *Literal, const SourceManager &SM,
126 const LangOptions &LangOpts) {
127 const CharSourceRange TokenRange =
128 CharSourceRange::getTokenRange(Literal->getSourceRange());
129 Token T;
130 if (Lexer::getRawToken(Literal->getBeginLoc(), T, SM, LangOpts))
131 return std::nullopt;
132 const CharSourceRange CharRange =
133 Lexer::makeFileCharRange(TokenRange, SM, LangOpts);
134 if (T.hasUDSuffix()) {
135 const StringRef Text = Lexer::getSourceText(CharRange, SM, LangOpts);
136 const size_t UDSuffixPos = Text.find_last_of('"');
137 if (UDSuffixPos == StringRef::npos)
138 return std::nullopt;
139 return Text.slice(UDSuffixPos + 1, Text.size());
140 }
141 return std::nullopt;
142}
143
144static std::string createRawStringLiteral(const StringLiteral *Literal,
145 const std::string &DelimiterStem,
146 const SourceManager &SM,
147 const LangOptions &LangOpts) {
148 const StringRef Bytes = Literal->getBytes();
149 std::string Delimiter;
150 for (int I = 0; containsDelimiter(Bytes, Delimiter); ++I)
151 Delimiter = (I == 0) ? DelimiterStem : DelimiterStem + std::to_string(I);
152
153 const std::optional<StringRef> UserDefinedSuffix =
154 createUserDefinedSuffix(Literal, SM, LangOpts);
155
156 if (Delimiter.empty())
157 return (R"(R"()" + Bytes + R"lit()")lit" + UserDefinedSuffix.value_or(""))
158 .str();
159
160 return (R"(R")" + Delimiter + "(" + Bytes + ")" + Delimiter + R"(")" +
161 UserDefinedSuffix.value_or(""))
162 .str();
163}
164
165static bool compareStringLength(StringRef Replacement,
166 const StringLiteral *Literal,
167 const SourceManager &SM,
168 const LangOptions &LangOpts) {
169 return Replacement.size() <=
170 Lexer::MeasureTokenLength(Literal->getBeginLoc(), SM, LangOpts);
171}
172
173void RawStringLiteralCheck::check(const MatchFinder::MatchResult &Result) {
174 const auto *Literal = Result.Nodes.getNodeAs<StringLiteral>("lit");
175 if (Literal->getBeginLoc().isMacroID())
176 return;
177 const SourceManager &SM = *Result.SourceManager;
178 const LangOptions &LangOpts = getLangOpts();
179 if (containsEscapedCharacters(Result, Literal, DisallowedChars)) {
180 const std::string Replacement =
181 createRawStringLiteral(Literal, DelimiterStem, SM, LangOpts);
182 if (ReplaceShorterLiterals ||
183 compareStringLength(Replacement, Literal, SM, LangOpts)) {
184 diag(Literal->getBeginLoc(),
185 "escaped string literal can be written as a raw string literal")
186 << FixItHint::CreateReplacement(Literal->getSourceRange(),
187 Replacement);
188 }
189 }
190}
191
192} // namespace clang::tidy::modernize
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
void storeOptions(ClangTidyOptions::OptionMap &Opts) override
RawStringLiteralCheck(StringRef Name, ClangTidyContext *Context)
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
void registerMatchers(ast_matchers::MatchFinder *Finder) override
static bool containsEscapedCharacters(const MatchFinder::MatchResult &Result, const StringLiteral *Literal, const CharsBitSet &DisallowedChars)
static bool compareStringLength(StringRef Replacement, const StringLiteral *Literal, const SourceManager &SM, const LangOptions &LangOpts)
static std::string createRawStringLiteral(const StringLiteral *Literal, const std::string &DelimiterStem, const SourceManager &SM, const LangOptions &LangOpts)
static bool isSpelledAsStringLiteral(const StringLiteral *Literal, const SourceManager &SM, const LangOptions &LangOpts)
static std::optional< StringRef > createUserDefinedSuffix(const StringLiteral *Literal, const SourceManager &SM, const LangOptions &LangOpts)
static bool isRawStringLiteral(StringRef Text)
static bool containsEscapes(StringRef HayStack, StringRef Escapes)
std::bitset< 1<< CHAR_BIT > CharsBitSet
static bool containsDelimiter(StringRef Bytes, const std::string &Delimiter)
llvm::StringMap< ClangTidyValue > OptionMap