1 | //===--- OperatorsRepresentationCheck.cpp - clang-tidy |
2 | //--------------------------===// |
3 | // |
4 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
5 | // See https://llvm.org/LICENSE.txt for license information. |
6 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
7 | // |
8 | //===----------------------------------------------------------------------===// |
9 | |
10 | #include "OperatorsRepresentationCheck.h" |
11 | #include "../utils/OptionsUtils.h" |
12 | #include "clang/AST/ASTContext.h" |
13 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
14 | #include "clang/Lex/Lexer.h" |
15 | #include "llvm/ADT/STLExtras.h" |
16 | #include <array> |
17 | #include <utility> |
18 | |
19 | using namespace clang::ast_matchers; |
20 | |
21 | namespace clang::tidy::readability { |
22 | |
23 | static StringRef getOperatorSpelling(SourceLocation Loc, ASTContext &Context) { |
24 | if (Loc.isInvalid()) |
25 | return {}; |
26 | |
27 | SourceManager &SM = Context.getSourceManager(); |
28 | |
29 | Loc = SM.getSpellingLoc(Loc); |
30 | if (Loc.isInvalid()) |
31 | return {}; |
32 | |
33 | const CharSourceRange TokenRange = CharSourceRange::getTokenRange(R: Loc); |
34 | return Lexer::getSourceText(Range: TokenRange, SM, LangOpts: Context.getLangOpts()); |
35 | } |
36 | |
37 | namespace { |
38 | |
39 | AST_MATCHER_P2(BinaryOperator, hasInvalidBinaryOperatorRepresentation, |
40 | BinaryOperatorKind, Kind, llvm::StringRef, |
41 | ExpectedRepresentation) { |
42 | if (Node.getOpcode() != Kind || ExpectedRepresentation.empty()) |
43 | return false; |
44 | |
45 | StringRef Spelling = |
46 | getOperatorSpelling(Loc: Node.getOperatorLoc(), Context&: Finder->getASTContext()); |
47 | return !Spelling.empty() && Spelling != ExpectedRepresentation; |
48 | } |
49 | |
50 | AST_MATCHER_P2(UnaryOperator, hasInvalidUnaryOperatorRepresentation, |
51 | UnaryOperatorKind, Kind, llvm::StringRef, |
52 | ExpectedRepresentation) { |
53 | if (Node.getOpcode() != Kind || ExpectedRepresentation.empty()) |
54 | return false; |
55 | |
56 | StringRef Spelling = |
57 | getOperatorSpelling(Loc: Node.getOperatorLoc(), Context&: Finder->getASTContext()); |
58 | return !Spelling.empty() && Spelling != ExpectedRepresentation; |
59 | } |
60 | |
61 | AST_MATCHER_P2(CXXOperatorCallExpr, hasInvalidOverloadedOperatorRepresentation, |
62 | OverloadedOperatorKind, Kind, llvm::StringRef, |
63 | ExpectedRepresentation) { |
64 | if (Node.getOperator() != Kind || ExpectedRepresentation.empty()) |
65 | return false; |
66 | |
67 | StringRef Spelling = |
68 | getOperatorSpelling(Loc: Node.getOperatorLoc(), Context&: Finder->getASTContext()); |
69 | return !Spelling.empty() && Spelling != ExpectedRepresentation; |
70 | } |
71 | |
72 | } // namespace |
73 | |
74 | constexpr std::array<std::pair<llvm::StringRef, llvm::StringRef>, 2U> |
75 | UnaryRepresentation{._M_elems: {{"!" , "not" }, {"~" , "compl" }}}; |
76 | |
77 | constexpr std::array<std::pair<llvm::StringRef, llvm::StringRef>, 9U> |
78 | OperatorsRepresentation{._M_elems: {{"&&" , "and" }, |
79 | {"||" , "or" }, |
80 | {"^" , "xor" }, |
81 | {"&" , "bitand" }, |
82 | {"|" , "bitor" }, |
83 | {"&=" , "and_eq" }, |
84 | {"|=" , "or_eq" }, |
85 | {"!=" , "not_eq" }, |
86 | {"^=" , "xor_eq" }}}; |
87 | |
88 | static llvm::StringRef translate(llvm::StringRef Value) { |
89 | for (const auto &[Traditional, Alternative] : UnaryRepresentation) { |
90 | if (Value == Traditional) |
91 | return Alternative; |
92 | if (Value == Alternative) |
93 | return Traditional; |
94 | } |
95 | |
96 | for (const auto &[Traditional, Alternative] : OperatorsRepresentation) { |
97 | if (Value == Traditional) |
98 | return Alternative; |
99 | if (Value == Alternative) |
100 | return Traditional; |
101 | } |
102 | return {}; |
103 | } |
104 | |
105 | static bool isNotOperatorStr(llvm::StringRef Value) { |
106 | return translate(Value).empty(); |
107 | } |
108 | |
109 | static bool isSeparator(char C) noexcept { |
110 | constexpr llvm::StringRef Separators(" \t\r\n\0()<>{};," ); |
111 | return Separators.contains(C); |
112 | } |
113 | |
114 | static bool needEscaping(llvm::StringRef Operator) { |
115 | switch (Operator[0]) { |
116 | case '&': |
117 | case '|': |
118 | case '!': |
119 | case '^': |
120 | case '~': |
121 | return false; |
122 | default: |
123 | return true; |
124 | } |
125 | } |
126 | |
127 | static llvm::StringRef |
128 | getRepresentation(const std::vector<llvm::StringRef> &Config, |
129 | llvm::StringRef Traditional, llvm::StringRef Alternative) { |
130 | if (llvm::is_contained(Range: Config, Element: Traditional)) |
131 | return Traditional; |
132 | if (llvm::is_contained(Range: Config, Element: Alternative)) |
133 | return Alternative; |
134 | return {}; |
135 | } |
136 | |
137 | template <typename T> |
138 | static bool isAnyOperatorEnabled(const std::vector<llvm::StringRef> &Config, |
139 | const T &Operators) { |
140 | for (const auto &[traditional, alternative] : Operators) { |
141 | if (!getRepresentation(Config, traditional, alternative).empty()) |
142 | return true; |
143 | } |
144 | return false; |
145 | } |
146 | |
147 | OperatorsRepresentationCheck::OperatorsRepresentationCheck( |
148 | StringRef Name, ClangTidyContext *Context) |
149 | : ClangTidyCheck(Name, Context), |
150 | BinaryOperators( |
151 | utils::options::parseStringList(Option: Options.get(LocalName: "BinaryOperators" , Default: "" ))), |
152 | OverloadedOperators(utils::options::parseStringList( |
153 | Option: Options.get(LocalName: "OverloadedOperators" , Default: "" ))) { |
154 | llvm::erase_if(C&: BinaryOperators, P: isNotOperatorStr); |
155 | llvm::erase_if(C&: OverloadedOperators, P: isNotOperatorStr); |
156 | } |
157 | |
158 | void OperatorsRepresentationCheck::storeOptions( |
159 | ClangTidyOptions::OptionMap &Opts) { |
160 | Options.store(Options&: Opts, LocalName: "BinaryOperators" , |
161 | Value: utils::options::serializeStringList(Strings: BinaryOperators)); |
162 | Options.store(Options&: Opts, LocalName: "OverloadedOperators" , |
163 | Value: utils::options::serializeStringList(Strings: OverloadedOperators)); |
164 | } |
165 | |
166 | std::optional<TraversalKind> |
167 | OperatorsRepresentationCheck::getCheckTraversalKind() const { |
168 | return TK_IgnoreUnlessSpelledInSource; |
169 | } |
170 | |
171 | bool OperatorsRepresentationCheck::isLanguageVersionSupported( |
172 | const LangOptions &LangOpts) const { |
173 | return LangOpts.CPlusPlus; |
174 | } |
175 | |
176 | void OperatorsRepresentationCheck::registerBinaryOperatorMatcher( |
177 | MatchFinder *Finder) { |
178 | if (!isAnyOperatorEnabled(Config: BinaryOperators, Operators: OperatorsRepresentation)) |
179 | return; |
180 | |
181 | Finder->addMatcher( |
182 | NodeMatch: binaryOperator( |
183 | unless(isExpansionInSystemHeader()), |
184 | anyOf(hasInvalidBinaryOperatorRepresentation( |
185 | Kind: BO_LAnd, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "&&" , Alternative: "and" )), |
186 | hasInvalidBinaryOperatorRepresentation( |
187 | Kind: BO_LOr, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "||" , Alternative: "or" )), |
188 | hasInvalidBinaryOperatorRepresentation( |
189 | Kind: BO_NE, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "!=" , Alternative: "not_eq" )), |
190 | hasInvalidBinaryOperatorRepresentation( |
191 | Kind: BO_Xor, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "^" , Alternative: "xor" )), |
192 | hasInvalidBinaryOperatorRepresentation( |
193 | Kind: BO_And, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "&" , Alternative: "bitand" )), |
194 | hasInvalidBinaryOperatorRepresentation( |
195 | Kind: BO_Or, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "|" , Alternative: "bitor" )), |
196 | hasInvalidBinaryOperatorRepresentation( |
197 | Kind: BO_AndAssign, |
198 | ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "&=" , Alternative: "and_eq" )), |
199 | hasInvalidBinaryOperatorRepresentation( |
200 | Kind: BO_OrAssign, |
201 | ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "|=" , Alternative: "or_eq" )), |
202 | hasInvalidBinaryOperatorRepresentation( |
203 | Kind: BO_XorAssign, |
204 | ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "^=" , Alternative: "xor_eq" )))) |
205 | .bind(ID: "binary_op" ), |
206 | Action: this); |
207 | } |
208 | |
209 | void OperatorsRepresentationCheck::registerUnaryOperatorMatcher( |
210 | MatchFinder *Finder) { |
211 | if (!isAnyOperatorEnabled(Config: BinaryOperators, Operators: UnaryRepresentation)) |
212 | return; |
213 | |
214 | Finder->addMatcher( |
215 | NodeMatch: unaryOperator( |
216 | unless(isExpansionInSystemHeader()), |
217 | anyOf(hasInvalidUnaryOperatorRepresentation( |
218 | Kind: UO_LNot, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "!" , Alternative: "not" )), |
219 | hasInvalidUnaryOperatorRepresentation( |
220 | Kind: UO_Not, ExpectedRepresentation: getRepresentation(Config: BinaryOperators, Traditional: "~" , Alternative: "compl" )))) |
221 | .bind(ID: "unary_op" ), |
222 | Action: this); |
223 | } |
224 | |
225 | void OperatorsRepresentationCheck::registerOverloadedOperatorMatcher( |
226 | MatchFinder *Finder) { |
227 | if (!isAnyOperatorEnabled(Config: OverloadedOperators, Operators: OperatorsRepresentation) && |
228 | !isAnyOperatorEnabled(Config: OverloadedOperators, Operators: UnaryRepresentation)) |
229 | return; |
230 | |
231 | Finder->addMatcher( |
232 | NodeMatch: cxxOperatorCallExpr( |
233 | unless(isExpansionInSystemHeader()), |
234 | anyOf( |
235 | hasInvalidOverloadedOperatorRepresentation( |
236 | Kind: OO_AmpAmp, |
237 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "&&" , Alternative: "and" )), |
238 | hasInvalidOverloadedOperatorRepresentation( |
239 | Kind: OO_PipePipe, |
240 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "||" , Alternative: "or" )), |
241 | hasInvalidOverloadedOperatorRepresentation( |
242 | Kind: OO_Exclaim, |
243 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "!" , Alternative: "not" )), |
244 | hasInvalidOverloadedOperatorRepresentation( |
245 | Kind: OO_ExclaimEqual, |
246 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "!=" , Alternative: "not_eq" )), |
247 | hasInvalidOverloadedOperatorRepresentation( |
248 | Kind: OO_Caret, ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "^" , Alternative: "xor" )), |
249 | hasInvalidOverloadedOperatorRepresentation( |
250 | Kind: OO_Amp, |
251 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "&" , Alternative: "bitand" )), |
252 | hasInvalidOverloadedOperatorRepresentation( |
253 | Kind: OO_Pipe, |
254 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "|" , Alternative: "bitor" )), |
255 | hasInvalidOverloadedOperatorRepresentation( |
256 | Kind: OO_AmpEqual, |
257 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "&=" , Alternative: "and_eq" )), |
258 | hasInvalidOverloadedOperatorRepresentation( |
259 | Kind: OO_PipeEqual, |
260 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "|=" , Alternative: "or_eq" )), |
261 | hasInvalidOverloadedOperatorRepresentation( |
262 | Kind: OO_CaretEqual, |
263 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "^=" , Alternative: "xor_eq" )), |
264 | hasInvalidOverloadedOperatorRepresentation( |
265 | Kind: OO_Tilde, |
266 | ExpectedRepresentation: getRepresentation(Config: OverloadedOperators, Traditional: "~" , Alternative: "compl" )))) |
267 | .bind(ID: "overloaded_op" ), |
268 | Action: this); |
269 | } |
270 | |
271 | void OperatorsRepresentationCheck::registerMatchers(MatchFinder *Finder) { |
272 | registerBinaryOperatorMatcher(Finder); |
273 | registerUnaryOperatorMatcher(Finder); |
274 | registerOverloadedOperatorMatcher(Finder); |
275 | } |
276 | |
277 | void OperatorsRepresentationCheck::check( |
278 | const MatchFinder::MatchResult &Result) { |
279 | |
280 | SourceLocation Loc; |
281 | |
282 | if (const auto *Op = Result.Nodes.getNodeAs<BinaryOperator>(ID: "binary_op" )) |
283 | Loc = Op->getOperatorLoc(); |
284 | else if (const auto *Op = Result.Nodes.getNodeAs<UnaryOperator>(ID: "unary_op" )) |
285 | Loc = Op->getOperatorLoc(); |
286 | else if (const auto *Op = |
287 | Result.Nodes.getNodeAs<CXXOperatorCallExpr>(ID: "overloaded_op" )) |
288 | Loc = Op->getOperatorLoc(); |
289 | |
290 | if (Loc.isInvalid()) |
291 | return; |
292 | |
293 | Loc = Result.SourceManager->getSpellingLoc(Loc); |
294 | if (Loc.isInvalid() || Loc.isMacroID()) |
295 | return; |
296 | |
297 | const CharSourceRange TokenRange = CharSourceRange::getTokenRange(R: Loc); |
298 | if (TokenRange.isInvalid()) |
299 | return; |
300 | |
301 | StringRef Spelling = Lexer::getSourceText(Range: TokenRange, SM: *Result.SourceManager, |
302 | LangOpts: Result.Context->getLangOpts()); |
303 | StringRef TranslatedSpelling = translate(Value: Spelling); |
304 | |
305 | if (TranslatedSpelling.empty()) |
306 | return; |
307 | |
308 | std::string FixSpelling = TranslatedSpelling.str(); |
309 | |
310 | StringRef SourceRepresentation = "an alternative" ; |
311 | StringRef TargetRepresentation = "a traditional" ; |
312 | if (needEscaping(Operator: TranslatedSpelling)) { |
313 | SourceRepresentation = "a traditional" ; |
314 | TargetRepresentation = "an alternative" ; |
315 | |
316 | StringRef SpellingEx = Lexer::getSourceText( |
317 | Range: CharSourceRange::getCharRange( |
318 | B: TokenRange.getBegin().getLocWithOffset(Offset: -1), |
319 | E: TokenRange.getBegin().getLocWithOffset(Offset: Spelling.size() + 1U)), |
320 | SM: *Result.SourceManager, LangOpts: Result.Context->getLangOpts()); |
321 | if (SpellingEx.empty() || !isSeparator(C: SpellingEx.front())) |
322 | FixSpelling.insert(p: FixSpelling.begin(), c: ' '); |
323 | if (SpellingEx.empty() || !isSeparator(C: SpellingEx.back())) |
324 | FixSpelling.push_back(c: ' '); |
325 | } |
326 | |
327 | diag( |
328 | Loc, |
329 | Description: "'%0' is %1 token spelling, consider using %2 token '%3' for consistency" ) |
330 | << Spelling << SourceRepresentation << TargetRepresentation |
331 | << TranslatedSpelling |
332 | << FixItHint::CreateReplacement(RemoveRange: TokenRange, Code: FixSpelling); |
333 | } |
334 | |
335 | } // namespace clang::tidy::readability |
336 | |