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