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 <optional>
17
18using namespace clang::ast_matchers;
19
20namespace clang::tidy::readability {
21
22NamespaceCommentCheck::NamespaceCommentCheck(StringRef Name,
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(LocalName: "ShortNamespaceLines", Default: 1U)),
30 SpacesBeforeComments(Options.get(LocalName: "SpacesBeforeComments", Default: 1U)),
31 AllowOmittingNamespaceComments(
32 Options.get(LocalName: "AllowOmittingNamespaceComments", Default: false)) {}
33
34void NamespaceCommentCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
35 Options.store(Options&: Opts, LocalName: "ShortNamespaceLines", Value: ShortNamespaceLines);
36 Options.store(Options&: Opts, LocalName: "SpacesBeforeComments", Value: SpacesBeforeComments);
37 Options.store(Options&: Opts, LocalName: "AllowOmittingNamespaceComments",
38 Value: AllowOmittingNamespaceComments);
39}
40
41void NamespaceCommentCheck::registerMatchers(MatchFinder *Finder) {
42 Finder->addMatcher(NodeMatch: namespaceDecl().bind(ID: "namespace"), Action: this);
43}
44
45static bool locationsInSameFile(const SourceManager &Sources,
46 SourceLocation Loc1, SourceLocation Loc2) {
47 return Loc1.isFileID() && Loc2.isFileID() &&
48 Sources.getFileID(SpellingLoc: Loc1) == Sources.getFileID(SpellingLoc: 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 Start: Loc, SM: Sources, LangOpts)) {
63 Loc = T->getLocation();
64 if (T->is(K: tok::l_brace))
65 break;
66
67 if (T->isOneOf(K1: tok::l_square, K2: tok::l_paren)) {
68 ++Nesting;
69 } else if (T->isOneOf(K1: tok::r_square, K2: tok::r_paren)) {
70 --Nesting;
71 } else if (Nesting == 0) {
72 if (T->is(K: tok::raw_identifier)) {
73 StringRef ID = T->getRawIdentifier();
74 if (ID != "namespace")
75 Result.append(str: std::string(ID));
76 if (ID == "inline")
77 Result.append(s: " ");
78 } else if (T->is(K: tok::coloncolon)) {
79 Result.append(s: "::");
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>(ID: "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, Loc1: ND->getBeginLoc(), Loc2: 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(Loc: ND->getBeginLoc());
100 unsigned EndLine = Sources.getSpellingLineNumber(Loc: 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 Loc: ND->getRBraceLoc(), /*Offset=*/0, SM: Sources, LangOpts: 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(LHS: ND->getLocation(), RHS: EndOfNameLocation))
115 return;
116 }
117
118 std::optional<std::string> NamespaceNameAsWritten =
119 getNamespaceNameAsWritten(Loc&: LBraceLoc, Sources, LangOpts: 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(Elt: LBraceLoc);
129
130 Token Tok;
131 // Skip whitespace until we find the next token.
132 while (Lexer::getRawToken(Loc, Result&: Tok, SM: Sources, LangOpts: getLangOpts()) ||
133 Tok.is(K: tok::semi)) {
134 Loc = Loc.getLocWithOffset(Offset: 1);
135 }
136
137 if (!locationsInSameFile(Sources, Loc1: ND->getRBraceLoc(), Loc2: 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(K: 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(K: tok::comment) && NextTokenIsOnSameLine) {
151 StringRef Comment(Sources.getCharacterData(SL: Loc), Tok.getLength());
152 SmallVector<StringRef, 7> Groups;
153 if (NamespaceCommentPattern.match(String: Comment, Matches: &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(Prefix: "/*");
170 OldCommentRange =
171 SourceRange(AfterRBrace, Loc.getLocWithOffset(Offset: 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(Prefix: "//")) {
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(Offset: 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(s: "// namespace");
200 if (!ND->isAnonymousNamespace())
201 Fix.append(s: " ").append(str: *NamespaceNameAsWritten);
202 if (NeedLineBreak)
203 Fix.append(s: "\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(Loc: DiagLoc, Description: Message) << NamespaceNameForDiag
212 << FixItHint::CreateReplacement(
213 RemoveRange: CharSourceRange::getCharRange(R: OldCommentRange),
214 Code: Fix);
215 diag(ND->getLocation(), "%0 starts here", DiagnosticIDs::Note)
216 << NamespaceNameForDiag;
217}
218
219} // namespace clang::tidy::readability
220

Provided by KDAB

Privacy Policy
Update your C++ knowledge – Modern C++11/14/17 Training
Find out more

source code of clang-tools-extra/clang-tidy/readability/NamespaceCommentCheck.cpp