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