1//===--- ConcatNestedNamespacesCheck.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 "ConcatNestedNamespacesCheck.h"
10#include "../utils/LexerUtils.h"
11#include "clang/AST/ASTContext.h"
12#include "clang/AST/Decl.h"
13#include "clang/ASTMatchers/ASTMatchFinder.h"
14#include "clang/Basic/SourceLocation.h"
15#include <algorithm>
16#include <optional>
17
18namespace clang::tidy::modernize {
19
20static bool locationsInSameFile(const SourceManager &Sources,
21 SourceLocation Loc1, SourceLocation Loc2) {
22 return Loc1.isFileID() && Loc2.isFileID() &&
23 Sources.getFileID(SpellingLoc: Loc1) == Sources.getFileID(SpellingLoc: Loc2);
24}
25
26static StringRef getRawStringRef(const SourceRange &Range,
27 const SourceManager &Sources,
28 const LangOptions &LangOpts) {
29 CharSourceRange TextRange = Lexer::getAsCharRange(Range, SM: Sources, LangOpts);
30 return Lexer::getSourceText(Range: TextRange, SM: Sources, LangOpts);
31}
32
33std::optional<SourceRange>
34NS::getCleanedNamespaceFrontRange(const SourceManager &SM,
35 const LangOptions &LangOpts) const {
36 // Front from namespace tp '{'
37 std::optional<Token> Tok =
38 ::clang::tidy::utils::lexer::findNextTokenSkippingComments(
39 Start: back()->getLocation(), SM, LangOpts);
40 if (!Tok)
41 return std::nullopt;
42 while (Tok->getKind() != tok::TokenKind::l_brace) {
43 Tok = utils::lexer::findNextTokenSkippingComments(Start: Tok->getEndLoc(), SM,
44 LangOpts);
45 if (!Tok)
46 return std::nullopt;
47 }
48 return SourceRange{front()->getBeginLoc(), Tok->getEndLoc()};
49}
50SourceRange NS::getReplacedNamespaceFrontRange() const {
51 return SourceRange{front()->getBeginLoc(), back()->getLocation()};
52}
53
54SourceRange NS::getDefaultNamespaceBackRange() const {
55 return SourceRange{front()->getRBraceLoc(), front()->getRBraceLoc()};
56}
57SourceRange NS::getNamespaceBackRange(const SourceManager &SM,
58 const LangOptions &LangOpts) const {
59 // Back from '}' to conditional '// namespace xxx'
60 SourceLocation Loc = front()->getRBraceLoc();
61 std::optional<Token> Tok =
62 utils::lexer::findNextTokenIncludingComments(Start: Loc, SM, LangOpts);
63 if (!Tok)
64 return getDefaultNamespaceBackRange();
65 if (Tok->getKind() != tok::TokenKind::comment)
66 return getDefaultNamespaceBackRange();
67 SourceRange TokRange = SourceRange{Tok->getLocation(), Tok->getEndLoc()};
68 StringRef TokText = getRawStringRef(Range: TokRange, Sources: SM, LangOpts);
69 NamespaceName CloseComment{"namespace "};
70 appendCloseComment(Str&: CloseComment);
71 // current fix hint in readability/NamespaceCommentCheck.cpp use single line
72 // comment
73 constexpr size_t L = sizeof("//") - 1U;
74 if (TokText.take_front(N: L) == "//" &&
75 TokText.drop_front(N: L).trim() != CloseComment)
76 return getDefaultNamespaceBackRange();
77 return SourceRange{front()->getRBraceLoc(), Tok->getEndLoc()};
78}
79
80void NS::appendName(NamespaceName &Str) const {
81 for (const NamespaceDecl *ND : *this) {
82 if (ND->isInlineNamespace())
83 Str.append(RHS: "inline ");
84 Str.append(ND->getName());
85 if (ND != back())
86 Str.append(RHS: "::");
87 }
88}
89void NS::appendCloseComment(NamespaceName &Str) const {
90 if (size() == 1)
91 Str.append(back()->getName());
92 else
93 appendName(Str);
94}
95
96bool ConcatNestedNamespacesCheck::unsupportedNamespace(const NamespaceDecl &ND,
97 bool IsChild) const {
98 if (ND.isAnonymousNamespace() || !ND.attrs().empty())
99 return true;
100 if (getLangOpts().CPlusPlus20) {
101 // C++20 support inline nested namespace
102 bool IsFirstNS = IsChild || !Namespaces.empty();
103 return ND.isInlineNamespace() && !IsFirstNS;
104 }
105 return ND.isInlineNamespace();
106}
107
108bool ConcatNestedNamespacesCheck::singleNamedNamespaceChild(
109 const NamespaceDecl &ND) const {
110 NamespaceDecl::decl_range Decls = ND.decls();
111 if (std::distance(first: Decls.begin(), last: Decls.end()) != 1)
112 return false;
113
114 const auto *ChildNamespace = dyn_cast<const NamespaceDecl>(Val: *Decls.begin());
115 return ChildNamespace && !unsupportedNamespace(ND: *ChildNamespace, IsChild: true);
116}
117
118void ConcatNestedNamespacesCheck::registerMatchers(
119 ast_matchers::MatchFinder *Finder) {
120 Finder->addMatcher(NodeMatch: ast_matchers::namespaceDecl().bind(ID: "namespace"), Action: this);
121}
122
123void ConcatNestedNamespacesCheck::reportDiagnostic(
124 const SourceManager &SM, const LangOptions &LangOpts) {
125 DiagnosticBuilder DB =
126 diag(Loc: Namespaces.front().front()->getBeginLoc(),
127 Description: "nested namespaces can be concatenated", Level: DiagnosticIDs::Warning);
128
129 SmallVector<SourceRange, 6> Fronts;
130 Fronts.reserve(N: Namespaces.size() - 1U);
131 SmallVector<SourceRange, 6> Backs;
132 Backs.reserve(N: Namespaces.size());
133
134 for (const NS &ND : Namespaces) {
135 std::optional<SourceRange> SR =
136 ND.getCleanedNamespaceFrontRange(SM, LangOpts);
137 if (!SR)
138 return;
139 Fronts.push_back(Elt: SR.value());
140 Backs.push_back(Elt: ND.getNamespaceBackRange(SM, LangOpts));
141 }
142 if (Fronts.empty() || Backs.empty())
143 return;
144
145 // the last one should be handled specially
146 Fronts.pop_back();
147 SourceRange LastRBrace = Backs.pop_back_val();
148
149 NamespaceName ConcatNameSpace{"namespace "};
150 for (const NS &NS : Namespaces) {
151 NS.appendName(Str&: ConcatNameSpace);
152 if (&NS != &Namespaces.back()) // compare address directly
153 ConcatNameSpace.append(RHS: "::");
154 }
155
156 for (SourceRange const &Front : Fronts)
157 DB << FixItHint::CreateRemoval(RemoveRange: Front);
158 DB << FixItHint::CreateReplacement(
159 RemoveRange: Namespaces.back().getReplacedNamespaceFrontRange(), Code: ConcatNameSpace);
160 if (LastRBrace != Namespaces.back().getDefaultNamespaceBackRange())
161 DB << FixItHint::CreateReplacement(RemoveRange: LastRBrace,
162 Code: ("} // " + ConcatNameSpace).str());
163 for (SourceRange const &Back : llvm::reverse(C&: Backs))
164 DB << FixItHint::CreateRemoval(RemoveRange: Back);
165}
166
167void ConcatNestedNamespacesCheck::check(
168 const ast_matchers::MatchFinder::MatchResult &Result) {
169 const NamespaceDecl &ND = *Result.Nodes.getNodeAs<NamespaceDecl>(ID: "namespace");
170 const SourceManager &Sources = *Result.SourceManager;
171
172 if (!locationsInSameFile(Sources, Loc1: ND.getBeginLoc(), Loc2: ND.getRBraceLoc()))
173 return;
174
175 if (unsupportedNamespace(ND, IsChild: false))
176 return;
177
178 if (!ND.isNested())
179 Namespaces.push_back(Elt: NS{});
180 if (!Namespaces.empty())
181 // Otherwise it will crash with invalid input like `inline namespace
182 // a::b::c`.
183 Namespaces.back().push_back(Elt: &ND);
184
185 if (singleNamedNamespaceChild(ND))
186 return;
187
188 if (Namespaces.size() > 1)
189 reportDiagnostic(SM: Sources, LangOpts: getLangOpts());
190
191 Namespaces.clear();
192}
193
194} // namespace clang::tidy::modernize
195

source code of clang-tools-extra/clang-tidy/modernize/ConcatNestedNamespacesCheck.cpp