1 | //===--- BracesAroundStatementsCheck.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 "BracesAroundStatementsCheck.h" |
10 | #include "../utils/LexerUtils.h" |
11 | #include "clang/AST/ASTContext.h" |
12 | #include "clang/ASTMatchers/ASTMatchers.h" |
13 | #include "clang/Lex/Lexer.h" |
14 | |
15 | using namespace clang::ast_matchers; |
16 | |
17 | namespace clang::tidy::readability { |
18 | |
19 | static tok::TokenKind getTokenKind(SourceLocation Loc, const SourceManager &SM, |
20 | const ASTContext *Context) { |
21 | Token Tok; |
22 | SourceLocation Beginning = |
23 | Lexer::GetBeginningOfToken(Loc, SM, LangOpts: Context->getLangOpts()); |
24 | const bool Invalid = |
25 | Lexer::getRawToken(Loc: Beginning, Result&: Tok, SM, LangOpts: Context->getLangOpts()); |
26 | assert(!Invalid && "Expected a valid token." ); |
27 | |
28 | if (Invalid) |
29 | return tok::NUM_TOKENS; |
30 | |
31 | return Tok.getKind(); |
32 | } |
33 | |
34 | static SourceLocation |
35 | forwardSkipWhitespaceAndComments(SourceLocation Loc, const SourceManager &SM, |
36 | const ASTContext *Context) { |
37 | assert(Loc.isValid()); |
38 | for (;;) { |
39 | while (isWhitespace(c: *SM.getCharacterData(SL: Loc))) |
40 | Loc = Loc.getLocWithOffset(Offset: 1); |
41 | |
42 | tok::TokenKind TokKind = getTokenKind(Loc, SM, Context); |
43 | if (TokKind != tok::comment) |
44 | return Loc; |
45 | |
46 | // Fast-forward current token. |
47 | Loc = Lexer::getLocForEndOfToken(Loc, Offset: 0, SM, LangOpts: Context->getLangOpts()); |
48 | } |
49 | } |
50 | |
51 | static SourceLocation findEndLocation(const Stmt &S, const SourceManager &SM, |
52 | const ASTContext *Context) { |
53 | SourceLocation Loc = |
54 | utils::lexer::getUnifiedEndLoc(S, SM, LangOpts: Context->getLangOpts()); |
55 | if (!Loc.isValid()) |
56 | return Loc; |
57 | |
58 | // Start searching right after S. |
59 | Loc = Loc.getLocWithOffset(Offset: 1); |
60 | |
61 | for (;;) { |
62 | assert(Loc.isValid()); |
63 | while (isHorizontalWhitespace(c: *SM.getCharacterData(SL: Loc))) { |
64 | Loc = Loc.getLocWithOffset(Offset: 1); |
65 | } |
66 | |
67 | if (isVerticalWhitespace(c: *SM.getCharacterData(SL: Loc))) { |
68 | // EOL, insert brace before. |
69 | break; |
70 | } |
71 | tok::TokenKind TokKind = getTokenKind(Loc, SM, Context); |
72 | if (TokKind != tok::comment) { |
73 | // Non-comment token, insert brace before. |
74 | break; |
75 | } |
76 | |
77 | SourceLocation TokEndLoc = |
78 | Lexer::getLocForEndOfToken(Loc, Offset: 0, SM, LangOpts: Context->getLangOpts()); |
79 | SourceRange TokRange(Loc, TokEndLoc); |
80 | StringRef = Lexer::getSourceText( |
81 | Range: CharSourceRange::getTokenRange(R: TokRange), SM, LangOpts: Context->getLangOpts()); |
82 | if (Comment.starts_with(Prefix: "/*" ) && Comment.contains(C: '\n')) { |
83 | // Multi-line block comment, insert brace before. |
84 | break; |
85 | } |
86 | // else: Trailing comment, insert brace after the newline. |
87 | |
88 | // Fast-forward current token. |
89 | Loc = TokEndLoc; |
90 | } |
91 | return Loc; |
92 | } |
93 | |
94 | BracesAroundStatementsCheck::BracesAroundStatementsCheck( |
95 | StringRef Name, ClangTidyContext *Context) |
96 | : ClangTidyCheck(Name, Context), |
97 | // Always add braces by default. |
98 | ShortStatementLines(Options.get(LocalName: "ShortStatementLines" , Default: 0U)) {} |
99 | |
100 | void BracesAroundStatementsCheck::storeOptions( |
101 | ClangTidyOptions::OptionMap &Opts) { |
102 | Options.store(Options&: Opts, LocalName: "ShortStatementLines" , Value: ShortStatementLines); |
103 | } |
104 | |
105 | void BracesAroundStatementsCheck::registerMatchers(MatchFinder *Finder) { |
106 | Finder->addMatcher(NodeMatch: ifStmt().bind(ID: "if" ), Action: this); |
107 | Finder->addMatcher(NodeMatch: whileStmt().bind(ID: "while" ), Action: this); |
108 | Finder->addMatcher(NodeMatch: doStmt().bind(ID: "do" ), Action: this); |
109 | Finder->addMatcher(NodeMatch: forStmt().bind(ID: "for" ), Action: this); |
110 | Finder->addMatcher(NodeMatch: cxxForRangeStmt().bind(ID: "for-range" ), Action: this); |
111 | } |
112 | |
113 | void BracesAroundStatementsCheck::check( |
114 | const MatchFinder::MatchResult &Result) { |
115 | const SourceManager &SM = *Result.SourceManager; |
116 | const ASTContext *Context = Result.Context; |
117 | |
118 | // Get location of closing parenthesis or 'do' to insert opening brace. |
119 | if (const auto *S = Result.Nodes.getNodeAs<ForStmt>(ID: "for" )) { |
120 | checkStmt(Result, S: S->getBody(), StartLoc: S->getRParenLoc()); |
121 | } else if (const auto *S = |
122 | Result.Nodes.getNodeAs<CXXForRangeStmt>(ID: "for-range" )) { |
123 | checkStmt(Result, S: S->getBody(), StartLoc: S->getRParenLoc()); |
124 | } else if (const auto *S = Result.Nodes.getNodeAs<DoStmt>(ID: "do" )) { |
125 | checkStmt(Result, S: S->getBody(), StartLoc: S->getDoLoc(), EndLocHint: S->getWhileLoc()); |
126 | } else if (const auto *S = Result.Nodes.getNodeAs<WhileStmt>(ID: "while" )) { |
127 | SourceLocation StartLoc = findRParenLoc(S, SM, Context); |
128 | if (StartLoc.isInvalid()) |
129 | return; |
130 | checkStmt(Result, S: S->getBody(), StartLoc); |
131 | } else if (const auto *S = Result.Nodes.getNodeAs<IfStmt>(ID: "if" )) { |
132 | // "if consteval" always has braces. |
133 | if (S->isConsteval()) |
134 | return; |
135 | |
136 | SourceLocation StartLoc = findRParenLoc(S, SM, Context); |
137 | if (StartLoc.isInvalid()) |
138 | return; |
139 | if (ForceBracesStmts.erase(S)) |
140 | ForceBracesStmts.insert(x: S->getThen()); |
141 | bool BracedIf = checkStmt(Result, S: S->getThen(), StartLoc, EndLocHint: S->getElseLoc()); |
142 | const Stmt *Else = S->getElse(); |
143 | if (Else && BracedIf) |
144 | ForceBracesStmts.insert(x: Else); |
145 | if (Else && !isa<IfStmt>(Val: Else)) { |
146 | // Omit 'else if' statements here, they will be handled directly. |
147 | checkStmt(Result, S: Else, StartLoc: S->getElseLoc()); |
148 | } |
149 | } else { |
150 | llvm_unreachable("Invalid match" ); |
151 | } |
152 | } |
153 | |
154 | /// Find location of right parenthesis closing condition. |
155 | template <typename IfOrWhileStmt> |
156 | SourceLocation |
157 | BracesAroundStatementsCheck::findRParenLoc(const IfOrWhileStmt *S, |
158 | const SourceManager &SM, |
159 | const ASTContext *Context) { |
160 | // Skip macros. |
161 | if (S->getBeginLoc().isMacroID()) |
162 | return {}; |
163 | |
164 | SourceLocation CondEndLoc = S->getCond()->getEndLoc(); |
165 | if (const DeclStmt *CondVar = S->getConditionVariableDeclStmt()) |
166 | CondEndLoc = CondVar->getEndLoc(); |
167 | |
168 | if (!CondEndLoc.isValid()) { |
169 | return {}; |
170 | } |
171 | |
172 | SourceLocation PastCondEndLoc = |
173 | Lexer::getLocForEndOfToken(Loc: CondEndLoc, Offset: 0, SM, LangOpts: Context->getLangOpts()); |
174 | if (PastCondEndLoc.isInvalid()) |
175 | return {}; |
176 | SourceLocation RParenLoc = |
177 | forwardSkipWhitespaceAndComments(Loc: PastCondEndLoc, SM, Context); |
178 | if (RParenLoc.isInvalid()) |
179 | return {}; |
180 | tok::TokenKind TokKind = getTokenKind(Loc: RParenLoc, SM, Context); |
181 | if (TokKind != tok::r_paren) |
182 | return {}; |
183 | return RParenLoc; |
184 | } |
185 | |
186 | /// Determine if the statement needs braces around it, and add them if it does. |
187 | /// Returns true if braces where added. |
188 | bool BracesAroundStatementsCheck::checkStmt( |
189 | const MatchFinder::MatchResult &Result, const Stmt *S, |
190 | SourceLocation StartLoc, SourceLocation EndLocHint) { |
191 | |
192 | while (const auto *AS = dyn_cast<AttributedStmt>(Val: S)) |
193 | S = AS->getSubStmt(); |
194 | |
195 | const SourceManager &SM = *Result.SourceManager; |
196 | const ASTContext *Context = Result.Context; |
197 | |
198 | // 1) If there's a corresponding "else" or "while", the check inserts "} " |
199 | // right before that token. |
200 | // 2) If there's a multi-line block comment starting on the same line after |
201 | // the location we're inserting the closing brace at, or there's a non-comment |
202 | // token, the check inserts "\n}" right before that token. |
203 | // 3) Otherwise the check finds the end of line (possibly after some block or |
204 | // line comments) and inserts "\n}" right before that EOL. |
205 | if (!S || isa<CompoundStmt>(Val: S)) { |
206 | // Already inside braces. |
207 | return false; |
208 | } |
209 | |
210 | // When TreeTransform, Stmt in constexpr IfStmt will be transform to NullStmt. |
211 | // This NullStmt can be detected according to beginning token. |
212 | const SourceLocation StmtBeginLoc = S->getBeginLoc(); |
213 | if (isa<NullStmt>(Val: S) && StmtBeginLoc.isValid() && |
214 | getTokenKind(Loc: StmtBeginLoc, SM, Context) == tok::l_brace) |
215 | return false; |
216 | |
217 | if (StartLoc.isInvalid()) |
218 | return false; |
219 | |
220 | // Convert StartLoc to file location, if it's on the same macro expansion |
221 | // level as the start of the statement. We also need file locations for |
222 | // Lexer::getLocForEndOfToken working properly. |
223 | StartLoc = Lexer::makeFileCharRange( |
224 | Range: CharSourceRange::getCharRange(B: StartLoc, E: S->getBeginLoc()), SM, |
225 | LangOpts: Context->getLangOpts()) |
226 | .getBegin(); |
227 | if (StartLoc.isInvalid()) |
228 | return false; |
229 | StartLoc = |
230 | Lexer::getLocForEndOfToken(Loc: StartLoc, Offset: 0, SM, LangOpts: Context->getLangOpts()); |
231 | |
232 | // StartLoc points at the location of the opening brace to be inserted. |
233 | SourceLocation EndLoc; |
234 | std::string ClosingInsertion; |
235 | if (EndLocHint.isValid()) { |
236 | EndLoc = EndLocHint; |
237 | ClosingInsertion = "} " ; |
238 | } else { |
239 | EndLoc = findEndLocation(S: *S, SM, Context); |
240 | ClosingInsertion = "\n}" ; |
241 | } |
242 | |
243 | assert(StartLoc.isValid()); |
244 | |
245 | // Don't require braces for statements spanning less than certain number of |
246 | // lines. |
247 | if (ShortStatementLines && !ForceBracesStmts.erase(x: S)) { |
248 | unsigned StartLine = SM.getSpellingLineNumber(Loc: StartLoc); |
249 | unsigned EndLine = SM.getSpellingLineNumber(Loc: EndLoc); |
250 | if (EndLine - StartLine < ShortStatementLines) |
251 | return false; |
252 | } |
253 | |
254 | auto Diag = diag(Loc: StartLoc, Description: "statement should be inside braces" ); |
255 | |
256 | // Change only if StartLoc and EndLoc are on the same macro expansion level. |
257 | // This will also catch invalid EndLoc. |
258 | // Example: LLVM_DEBUG( for(...) do_something() ); |
259 | // In this case fix-it cannot be provided as the semicolon which is not |
260 | // visible here is part of the macro. Adding braces here would require adding |
261 | // another semicolon. |
262 | if (Lexer::makeFileCharRange( |
263 | Range: CharSourceRange::getTokenRange(R: SourceRange( |
264 | SM.getSpellingLoc(Loc: StartLoc), SM.getSpellingLoc(Loc: EndLoc))), |
265 | SM, LangOpts: Context->getLangOpts()) |
266 | .isInvalid()) |
267 | return false; |
268 | |
269 | Diag << FixItHint::CreateInsertion(InsertionLoc: StartLoc, Code: " {" ) |
270 | << FixItHint::CreateInsertion(InsertionLoc: EndLoc, Code: ClosingInsertion); |
271 | return true; |
272 | } |
273 | |
274 | void BracesAroundStatementsCheck::onEndOfTranslationUnit() { |
275 | ForceBracesStmts.clear(); |
276 | } |
277 | |
278 | } // namespace clang::tidy::readability |
279 | |