1 //===--- NamespaceCommentCheck.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 "NamespaceCommentCheck.h"
10 #include "../utils/LexerUtils.h"
11 #include "clang/AST/ASTContext.h"
12 #include "clang/ASTMatchers/ASTMatchers.h"
13 #include "clang/Basic/SourceLocation.h"
14 #include "clang/Basic/TokenKinds.h"
15 #include "clang/Lex/Lexer.h"
16 #include "llvm/ADT/StringExtras.h"
17 
18 using namespace clang::ast_matchers;
19 
20 namespace clang {
21 namespace tidy {
22 namespace readability {
23 
24 NamespaceCommentCheck::NamespaceCommentCheck(StringRef Name,
25                                              ClangTidyContext *Context)
26     : ClangTidyCheck(Name, Context),
27       NamespaceCommentPattern("^/[/*] *(end (of )?)? *(anonymous|unnamed)? *"
28                               "namespace( +([a-zA-Z0-9_:]+))?\\.? *(\\*/)?$",
29                               llvm::Regex::IgnoreCase),
30       ShortNamespaceLines(Options.get("ShortNamespaceLines", 1u)),
31       SpacesBeforeComments(Options.get("SpacesBeforeComments", 1u)) {}
32 
33 void NamespaceCommentCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
34   Options.store(Opts, "ShortNamespaceLines", ShortNamespaceLines);
35   Options.store(Opts, "SpacesBeforeComments", SpacesBeforeComments);
36 }
37 
38 void NamespaceCommentCheck::registerMatchers(MatchFinder *Finder) {
39   // Only register the matchers for C++; the functionality currently does not
40   // provide any benefit to other languages, despite being benign.
41   if (getLangOpts().CPlusPlus)
42     Finder->addMatcher(namespaceDecl().bind("namespace"), this);
43 }
44 
45 static bool locationsInSameFile(const SourceManager &Sources,
46                                 SourceLocation Loc1, SourceLocation Loc2) {
47   return Loc1.isFileID() && Loc2.isFileID() &&
48          Sources.getFileID(Loc1) == Sources.getFileID(Loc2);
49 }
50 
51 static llvm::Optional<std::string>
52 getNamespaceNameAsWritten(SourceLocation &Loc, const SourceManager &Sources,
53                           const LangOptions &LangOpts) {
54   // Loc should be at the begin of the namespace decl (usually, `namespace`
55   // token). We skip the first token right away, but in case of `inline
56   // namespace` or `namespace a::inline b` we can see both `inline` and
57   // `namespace` keywords, which we just ignore. Nested parens/squares before
58   // the opening brace can result from attributes.
59   std::string Result;
60   int Nesting = 0;
61   while (llvm::Optional<Token> T = utils::lexer::findNextTokenSkippingComments(
62              Loc, Sources, LangOpts)) {
63     Loc = T->getLocation();
64     if (T->is(tok::l_brace))
65       break;
66 
67     if (T->isOneOf(tok::l_square, tok::l_paren)) {
68       ++Nesting;
69     } else if (T->isOneOf(tok::r_square, tok::r_paren)) {
70       --Nesting;
71     } else if (Nesting == 0) {
72       if (T->is(tok::raw_identifier)) {
73         StringRef ID = T->getRawIdentifier();
74         if (ID != "namespace" && ID != "inline")
75           Result.append(std::string(ID));
76       } else if (T->is(tok::coloncolon)) {
77         Result.append("::");
78       } else { // Any other kind of token is unexpected here.
79         return llvm::None;
80       }
81     }
82   }
83   return Result;
84 }
85 
86 void NamespaceCommentCheck::check(const MatchFinder::MatchResult &Result) {
87   const auto *ND = Result.Nodes.getNodeAs<NamespaceDecl>("namespace");
88   const SourceManager &Sources = *Result.SourceManager;
89 
90   // Ignore namespaces inside macros and namespaces split across files.
91   if (ND->getBeginLoc().isMacroID() ||
92       !locationsInSameFile(Sources, ND->getBeginLoc(), ND->getRBraceLoc()))
93     return;
94 
95   // Don't require closing comments for namespaces spanning less than certain
96   // number of lines.
97   unsigned StartLine = Sources.getSpellingLineNumber(ND->getBeginLoc());
98   unsigned EndLine = Sources.getSpellingLineNumber(ND->getRBraceLoc());
99   if (EndLine - StartLine + 1 <= ShortNamespaceLines)
100     return;
101 
102   // Find next token after the namespace closing brace.
103   SourceLocation AfterRBrace = Lexer::getLocForEndOfToken(
104       ND->getRBraceLoc(), /*Offset=*/0, Sources, getLangOpts());
105   SourceLocation Loc = AfterRBrace;
106   SourceLocation LBraceLoc = ND->getBeginLoc();
107 
108   // Currently for nested namepsace (n1::n2::...) the AST matcher will match foo
109   // then bar instead of a single match. So if we got a nested namespace we have
110   // to skip the next ones.
111   for (const auto &EndOfNameLocation : Ends) {
112     if (Sources.isBeforeInTranslationUnit(ND->getLocation(), EndOfNameLocation))
113       return;
114   }
115 
116   llvm::Optional<std::string> NamespaceNameAsWritten =
117       getNamespaceNameAsWritten(LBraceLoc, Sources, getLangOpts());
118   if (!NamespaceNameAsWritten)
119     return;
120 
121   if (NamespaceNameAsWritten->empty() != ND->isAnonymousNamespace()) {
122     // Apparently, we didn't find the correct namespace name. Give up.
123     return;
124   }
125 
126   Ends.push_back(LBraceLoc);
127 
128   Token Tok;
129   // Skip whitespace until we find the next token.
130   while (Lexer::getRawToken(Loc, Tok, Sources, getLangOpts()) ||
131          Tok.is(tok::semi)) {
132     Loc = Loc.getLocWithOffset(1);
133   }
134 
135   if (!locationsInSameFile(Sources, ND->getRBraceLoc(), Loc))
136     return;
137 
138   bool NextTokenIsOnSameLine = Sources.getSpellingLineNumber(Loc) == EndLine;
139   // If we insert a line comment before the token in the same line, we need
140   // to insert a line break.
141   bool NeedLineBreak = NextTokenIsOnSameLine && Tok.isNot(tok::eof);
142 
143   SourceRange OldCommentRange(AfterRBrace, AfterRBrace);
144   std::string Message = "%0 not terminated with a closing comment";
145 
146   // Try to find existing namespace closing comment on the same line.
147   if (Tok.is(tok::comment) && NextTokenIsOnSameLine) {
148     StringRef Comment(Sources.getCharacterData(Loc), Tok.getLength());
149     SmallVector<StringRef, 7> Groups;
150     if (NamespaceCommentPattern.match(Comment, &Groups)) {
151       StringRef NamespaceNameInComment = Groups.size() > 5 ? Groups[5] : "";
152       StringRef Anonymous = Groups.size() > 3 ? Groups[3] : "";
153 
154       if ((ND->isAnonymousNamespace() && NamespaceNameInComment.empty()) ||
155           (*NamespaceNameAsWritten == NamespaceNameInComment &&
156            Anonymous.empty())) {
157         // Check if the namespace in the comment is the same.
158         // FIXME: Maybe we need a strict mode, where we always fix namespace
159         // comments with different format.
160         return;
161       }
162 
163       // Otherwise we need to fix the comment.
164       NeedLineBreak = Comment.startswith("/*");
165       OldCommentRange =
166           SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
167       Message =
168           (llvm::Twine(
169                "%0 ends with a comment that refers to a wrong namespace '") +
170            NamespaceNameInComment + "'")
171               .str();
172     } else if (Comment.startswith("//")) {
173       // Assume that this is an unrecognized form of a namespace closing line
174       // comment. Replace it.
175       NeedLineBreak = false;
176       OldCommentRange =
177           SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
178       Message = "%0 ends with an unrecognized comment";
179     }
180     // If it's a block comment, just move it to the next line, as it can be
181     // multi-line or there may be other tokens behind it.
182   }
183 
184   std::string NamespaceNameForDiag =
185       ND->isAnonymousNamespace() ? "anonymous namespace"
186                                  : ("namespace '" + *NamespaceNameAsWritten + "'");
187 
188   std::string Fix(SpacesBeforeComments, ' ');
189   Fix.append("// namespace");
190   if (!ND->isAnonymousNamespace())
191     Fix.append(" ").append(*NamespaceNameAsWritten);
192   if (NeedLineBreak)
193     Fix.append("\n");
194 
195   // Place diagnostic at an old comment, or closing brace if we did not have it.
196   SourceLocation DiagLoc =
197       OldCommentRange.getBegin() != OldCommentRange.getEnd()
198           ? OldCommentRange.getBegin()
199           : ND->getRBraceLoc();
200 
201   diag(DiagLoc, Message) << NamespaceNameForDiag
202                          << FixItHint::CreateReplacement(
203                                 CharSourceRange::getCharRange(OldCommentRange),
204                                 Fix);
205   diag(ND->getLocation(), "%0 starts here", DiagnosticIDs::Note)
206       << NamespaceNameForDiag;
207 }
208 
209 } // namespace readability
210 } // namespace tidy
211 } // namespace clang
212