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   // FIXME: This probably breaks on comments between the namespace and its '{'.
106   auto TextRange =
107       Lexer::getAsCharRange(SourceRange(NestedNamespaceBegin, LBracketLocation),
108                             Sources, getLangOpts());
109   StringRef NestedNamespaceName =
110       Lexer::getSourceText(TextRange, Sources, getLangOpts())
111           .rtrim('{') // Drop the { itself.
112           .rtrim();   // Drop any whitespace before it.
113   bool IsNested = NestedNamespaceName.contains(':');
114 
115   if (IsNested)
116     Ends.push_back(LBracketLocation);
117   else
118     NestedNamespaceName = ND->getName();
119 
120   // Skip whitespace until we find the next token.
121   while (Lexer::getRawToken(Loc, Tok, Sources, getLangOpts()) ||
122          Tok.is(tok::semi)) {
123     Loc = Loc.getLocWithOffset(1);
124   }
125 
126   if (!locationsInSameFile(Sources, ND->getRBraceLoc(), Loc))
127     return;
128 
129   bool NextTokenIsOnSameLine = Sources.getSpellingLineNumber(Loc) == EndLine;
130   // If we insert a line comment before the token in the same line, we need
131   // to insert a line break.
132   bool NeedLineBreak = NextTokenIsOnSameLine && Tok.isNot(tok::eof);
133 
134   SourceRange OldCommentRange(AfterRBrace, AfterRBrace);
135   std::string Message = "%0 not terminated with a closing comment";
136 
137   // Try to find existing namespace closing comment on the same line.
138   if (Tok.is(tok::comment) && NextTokenIsOnSameLine) {
139     StringRef Comment(Sources.getCharacterData(Loc), Tok.getLength());
140     SmallVector<StringRef, 7> Groups;
141     if (NamespaceCommentPattern.match(Comment, &Groups)) {
142       StringRef NamespaceNameInComment = Groups.size() > 5 ? Groups[5] : "";
143       StringRef Anonymous = Groups.size() > 3 ? Groups[3] : "";
144 
145       if (IsNested && NestedNamespaceName == NamespaceNameInComment) {
146         // C++17 nested namespace.
147         return;
148       } else if ((ND->isAnonymousNamespace() &&
149                   NamespaceNameInComment.empty()) ||
150                  (ND->getNameAsString() == NamespaceNameInComment &&
151                   Anonymous.empty())) {
152         // Check if the namespace in the comment is the same.
153         // FIXME: Maybe we need a strict mode, where we always fix namespace
154         // comments with different format.
155         return;
156       }
157 
158       // Otherwise we need to fix the comment.
159       NeedLineBreak = Comment.startswith("/*");
160       OldCommentRange =
161           SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
162       Message =
163           (llvm::Twine(
164                "%0 ends with a comment that refers to a wrong namespace '") +
165            NamespaceNameInComment + "'")
166               .str();
167     } else if (Comment.startswith("//")) {
168       // Assume that this is an unrecognized form of a namespace closing line
169       // comment. Replace it.
170       NeedLineBreak = false;
171       OldCommentRange =
172           SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
173       Message = "%0 ends with an unrecognized comment";
174     }
175     // If it's a block comment, just move it to the next line, as it can be
176     // multi-line or there may be other tokens behind it.
177   }
178 
179   std::string NamespaceName =
180       ND->isAnonymousNamespace()
181           ? "anonymous namespace"
182           : ("namespace '" + NestedNamespaceName.str() + "'");
183 
184   diag(AfterRBrace, Message)
185       << NamespaceName
186       << FixItHint::CreateReplacement(
187              CharSourceRange::getCharRange(OldCommentRange),
188              std::string(SpacesBeforeComments, ' ') +
189                  (IsNested
190                       ? getNamespaceComment(NestedNamespaceName, NeedLineBreak)
191                       : getNamespaceComment(ND, NeedLineBreak)));
192   diag(ND->getLocation(), "%0 starts here", DiagnosticIDs::Note)
193       << NamespaceName;
194 }
195 
196 } // namespace readability
197 } // namespace tidy
198 } // namespace clang
199