clang-tools 22.0.0git
NamespaceCommentCheck.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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
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 <optional>
17
18using namespace clang::ast_matchers;
19
21
23 ClangTidyContext *Context)
24 : ClangTidyCheck(Name, Context),
25 NamespaceCommentPattern(
26 "^/[/*] *(end (of )?)? *(anonymous|unnamed)? *"
27 "namespace( +(((inline )|([a-zA-Z0-9_:]))+))?\\.? *(\\*/)?$",
28 llvm::Regex::IgnoreCase),
29 ShortNamespaceLines(Options.get("ShortNamespaceLines", 1U)),
30 SpacesBeforeComments(Options.get("SpacesBeforeComments", 1U)),
31 AllowOmittingNamespaceComments(
32 Options.get("AllowOmittingNamespaceComments", false)) {}
33
34void NamespaceCommentCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
35 Options.store(Opts, "ShortNamespaceLines", ShortNamespaceLines);
36 Options.store(Opts, "SpacesBeforeComments", SpacesBeforeComments);
37 Options.store(Opts, "AllowOmittingNamespaceComments",
38 AllowOmittingNamespaceComments);
39}
40
41void NamespaceCommentCheck::registerMatchers(MatchFinder *Finder) {
42 Finder->addMatcher(namespaceDecl().bind("namespace"), this);
43}
44
45static 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
51static std::optional<std::string>
52getNamespaceNameAsWritten(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 (std::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")
75 Result.append(std::string(ID));
76 if (ID == "inline")
77 Result.append(" ");
78 } else if (T->is(tok::coloncolon)) {
79 Result.append("::");
80 } else { // Any other kind of token is unexpected here.
81 return std::nullopt;
82 }
83 }
84 }
85 return Result;
86}
87
88void NamespaceCommentCheck::check(const MatchFinder::MatchResult &Result) {
89 const auto *ND = Result.Nodes.getNodeAs<NamespaceDecl>("namespace");
90 const SourceManager &Sources = *Result.SourceManager;
91
92 // Ignore namespaces inside macros and namespaces split across files.
93 if (ND->getBeginLoc().isMacroID() ||
94 !locationsInSameFile(Sources, ND->getBeginLoc(), ND->getRBraceLoc()))
95 return;
96
97 // Don't require closing comments for namespaces spanning less than certain
98 // number of lines.
99 unsigned StartLine = Sources.getSpellingLineNumber(ND->getBeginLoc());
100 unsigned EndLine = Sources.getSpellingLineNumber(ND->getRBraceLoc());
101 if (EndLine - StartLine + 1 <= ShortNamespaceLines)
102 return;
103
104 // Find next token after the namespace closing brace.
105 SourceLocation AfterRBrace = Lexer::getLocForEndOfToken(
106 ND->getRBraceLoc(), /*Offset=*/0, Sources, getLangOpts());
107 SourceLocation Loc = AfterRBrace;
108 SourceLocation LBraceLoc = ND->getBeginLoc();
109
110 // Currently for nested namespace (n1::n2::...) the AST matcher will match foo
111 // then bar instead of a single match. So if we got a nested namespace we have
112 // to skip the next ones.
113 for (const SourceLocation &EndOfNameLocation : Ends) {
114 if (Sources.isBeforeInTranslationUnit(ND->getLocation(), EndOfNameLocation))
115 return;
116 }
117
118 std::optional<std::string> NamespaceNameAsWritten =
119 getNamespaceNameAsWritten(LBraceLoc, Sources, getLangOpts());
120 if (!NamespaceNameAsWritten)
121 return;
122
123 if (NamespaceNameAsWritten->empty() != ND->isAnonymousNamespace()) {
124 // Apparently, we didn't find the correct namespace name. Give up.
125 return;
126 }
127
128 Ends.push_back(LBraceLoc);
129
130 Token Tok;
131 // Skip whitespace until we find the next token.
132 while (Lexer::getRawToken(Loc, Tok, Sources, getLangOpts()) ||
133 Tok.is(tok::semi)) {
134 Loc = Loc.getLocWithOffset(1);
135 }
136
137 if (!locationsInSameFile(Sources, ND->getRBraceLoc(), Loc))
138 return;
139
140 bool NextTokenIsOnSameLine = Sources.getSpellingLineNumber(Loc) == EndLine;
141 // If we insert a line comment before the token in the same line, we need
142 // to insert a line break.
143 bool NeedLineBreak = NextTokenIsOnSameLine && Tok.isNot(tok::eof);
144
145 SourceRange OldCommentRange(AfterRBrace, AfterRBrace);
146 std::string Message = "%0 not terminated with a closing comment";
147 bool HasComment = false;
148
149 // Try to find existing namespace closing comment on the same line.
150 if (Tok.is(tok::comment) && NextTokenIsOnSameLine) {
151 StringRef Comment(Sources.getCharacterData(Loc), Tok.getLength());
152 SmallVector<StringRef, 7> Groups;
153 if (NamespaceCommentPattern.match(Comment, &Groups)) {
154 StringRef NamespaceNameInComment = Groups.size() > 5 ? Groups[5] : "";
155 StringRef Anonymous = Groups.size() > 3 ? Groups[3] : "";
156
157 if ((ND->isAnonymousNamespace() && NamespaceNameInComment.empty()) ||
158 (*NamespaceNameAsWritten == NamespaceNameInComment &&
159 Anonymous.empty())) {
160 // Check if the namespace in the comment is the same.
161 // FIXME: Maybe we need a strict mode, where we always fix namespace
162 // comments with different format.
163 return;
164 }
165
166 HasComment = true;
167
168 // Otherwise we need to fix the comment.
169 NeedLineBreak = Comment.starts_with("/*");
170 OldCommentRange =
171 SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
172 Message =
173 (llvm::Twine(
174 "%0 ends with a comment that refers to a wrong namespace '") +
175 NamespaceNameInComment + "'")
176 .str();
177 } else if (Comment.starts_with("//")) {
178 // Assume that this is an unrecognized form of a namespace closing line
179 // comment. Replace it.
180 NeedLineBreak = false;
181 OldCommentRange =
182 SourceRange(AfterRBrace, Loc.getLocWithOffset(Tok.getLength()));
183 Message = "%0 ends with an unrecognized comment";
184 }
185 // If it's a block comment, just move it to the next line, as it can be
186 // multi-line or there may be other tokens behind it.
187 }
188
189 std::string NamespaceNameForDiag =
190 ND->isAnonymousNamespace()
191 ? "anonymous namespace"
192 : ("namespace '" + *NamespaceNameAsWritten + "'");
193
194 // If no namespace comment is allowed
195 if (!HasComment && AllowOmittingNamespaceComments)
196 return;
197
198 std::string Fix(SpacesBeforeComments, ' ');
199 Fix.append("// namespace");
200 if (!ND->isAnonymousNamespace())
201 Fix.append(" ").append(*NamespaceNameAsWritten);
202 if (NeedLineBreak)
203 Fix.append("\n");
204
205 // Place diagnostic at an old comment, or closing brace if we did not have it.
206 SourceLocation DiagLoc =
207 OldCommentRange.getBegin() != OldCommentRange.getEnd()
208 ? OldCommentRange.getBegin()
209 : ND->getRBraceLoc();
210
211 diag(DiagLoc, Message) << NamespaceNameForDiag
212 << FixItHint::CreateReplacement(
213 CharSourceRange::getCharRange(OldCommentRange),
214 Fix);
215 diag(ND->getLocation(), "%0 starts here", DiagnosticIDs::Note)
216 << NamespaceNameForDiag;
217}
218
219} // namespace clang::tidy::readability
static cl::opt< bool > Fix("fix", desc(R"( Apply suggested fixes. Without -fix-errors clang-tidy will bail out if any compilation errors were found. )"), cl::init(false), cl::cat(ClangTidyCategory))
Every ClangTidyCheck reports errors through a DiagnosticsEngine provided by this context.
void registerMatchers(ast_matchers::MatchFinder *Finder) override
NamespaceCommentCheck(StringRef Name, ClangTidyContext *Context)
void check(const ast_matchers::MatchFinder::MatchResult &Result) override
static std::optional< std::string > getNamespaceNameAsWritten(SourceLocation &Loc, const SourceManager &Sources, const LangOptions &LangOpts)
static bool locationsInSameFile(const SourceManager &Sources, SourceLocation Loc1, SourceLocation Loc2)
std::optional< Token > findNextTokenSkippingComments(SourceLocation Start, const SourceManager &SM, const LangOptions &LangOpts)
Some operations such as code completion produce a set of candidates.
Definition Generators.h:66
llvm::StringMap< ClangTidyValue > OptionMap