xref: /llvm-project/clang-tools-extra/clang-tidy/modernize/ConcatNestedNamespacesCheck.cpp (revision 32aaacc609e7a0523d498b244e081ac6f3df532b)
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 
18 namespace clang::tidy::modernize {
19 
locationsInSameFile(const SourceManager & Sources,SourceLocation Loc1,SourceLocation Loc2)20 static bool locationsInSameFile(const SourceManager &Sources,
21                                 SourceLocation Loc1, SourceLocation Loc2) {
22   return Loc1.isFileID() && Loc2.isFileID() &&
23          Sources.getFileID(Loc1) == Sources.getFileID(Loc2);
24 }
25 
getRawStringRef(const SourceRange & Range,const SourceManager & Sources,const LangOptions & LangOpts)26 static StringRef getRawStringRef(const SourceRange &Range,
27                                  const SourceManager &Sources,
28                                  const LangOptions &LangOpts) {
29   CharSourceRange TextRange = Lexer::getAsCharRange(Range, Sources, LangOpts);
30   return Lexer::getSourceText(TextRange, Sources, LangOpts);
31 }
32 
33 std::optional<SourceRange>
getCleanedNamespaceFrontRange(const SourceManager & SM,const LangOptions & LangOpts) const34 NS::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           back()->getLocation(), SM, LangOpts);
40   if (!Tok)
41     return std::nullopt;
42   while (Tok->getKind() != tok::TokenKind::l_brace) {
43     Tok = utils::lexer::findNextTokenSkippingComments(Tok->getEndLoc(), SM,
44                                                       LangOpts);
45     if (!Tok)
46       return std::nullopt;
47   }
48   return SourceRange{front()->getBeginLoc(), Tok->getEndLoc()};
49 }
getReplacedNamespaceFrontRange() const50 SourceRange NS::getReplacedNamespaceFrontRange() const {
51   return SourceRange{front()->getBeginLoc(), back()->getLocation()};
52 }
53 
getDefaultNamespaceBackRange() const54 SourceRange NS::getDefaultNamespaceBackRange() const {
55   return SourceRange{front()->getRBraceLoc(), front()->getRBraceLoc()};
56 }
getNamespaceBackRange(const SourceManager & SM,const LangOptions & LangOpts) const57 SourceRange 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(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(TokRange, SM, LangOpts);
69   NamespaceName CloseComment{"namespace "};
70   appendCloseComment(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(L) == "//" &&
75       TokText.drop_front(L).trim() != CloseComment)
76     return getDefaultNamespaceBackRange();
77   return SourceRange{front()->getRBraceLoc(), Tok->getEndLoc()};
78 }
79 
appendName(NamespaceName & Str) const80 void NS::appendName(NamespaceName &Str) const {
81   for (const NamespaceDecl *ND : *this) {
82     if (ND->isInlineNamespace())
83       Str.append("inline ");
84     Str.append(ND->getName());
85     if (ND != back())
86       Str.append("::");
87   }
88 }
appendCloseComment(NamespaceName & Str) const89 void NS::appendCloseComment(NamespaceName &Str) const {
90   if (size() == 1)
91     Str.append(back()->getName());
92   else
93     appendName(Str);
94 }
95 
unsupportedNamespace(const NamespaceDecl & ND,bool IsChild) const96 bool 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 
singleNamedNamespaceChild(const NamespaceDecl & ND) const108 bool ConcatNestedNamespacesCheck::singleNamedNamespaceChild(
109     const NamespaceDecl &ND) const {
110   NamespaceDecl::decl_range Decls = ND.decls();
111   if (std::distance(Decls.begin(), Decls.end()) != 1)
112     return false;
113 
114   const auto *ChildNamespace = dyn_cast<const NamespaceDecl>(*Decls.begin());
115   return ChildNamespace && !unsupportedNamespace(*ChildNamespace, true);
116 }
117 
registerMatchers(ast_matchers::MatchFinder * Finder)118 void ConcatNestedNamespacesCheck::registerMatchers(
119     ast_matchers::MatchFinder *Finder) {
120   Finder->addMatcher(ast_matchers::namespaceDecl().bind("namespace"), this);
121 }
122 
reportDiagnostic(const SourceManager & SM,const LangOptions & LangOpts)123 void ConcatNestedNamespacesCheck::reportDiagnostic(
124     const SourceManager &SM, const LangOptions &LangOpts) {
125   DiagnosticBuilder DB =
126       diag(Namespaces.front().front()->getBeginLoc(),
127            "nested namespaces can be concatenated", DiagnosticIDs::Warning);
128 
129   SmallVector<SourceRange, 6> Fronts;
130   Fronts.reserve(Namespaces.size() - 1U);
131   SmallVector<SourceRange, 6> Backs;
132   Backs.reserve(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(SR.value());
140     Backs.push_back(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(ConcatNameSpace);
152     if (&NS != &Namespaces.back()) // compare address directly
153       ConcatNameSpace.append("::");
154   }
155 
156   for (SourceRange const &Front : Fronts)
157     DB << FixItHint::CreateRemoval(Front);
158   DB << FixItHint::CreateReplacement(
159       Namespaces.back().getReplacedNamespaceFrontRange(), ConcatNameSpace);
160   if (LastRBrace != Namespaces.back().getDefaultNamespaceBackRange())
161     DB << FixItHint::CreateReplacement(LastRBrace,
162                                        ("} // " + ConcatNameSpace).str());
163   for (SourceRange const &Back : llvm::reverse(Backs))
164     DB << FixItHint::CreateRemoval(Back);
165 }
166 
check(const ast_matchers::MatchFinder::MatchResult & Result)167 void ConcatNestedNamespacesCheck::check(
168     const ast_matchers::MatchFinder::MatchResult &Result) {
169   const NamespaceDecl &ND = *Result.Nodes.getNodeAs<NamespaceDecl>("namespace");
170   const SourceManager &Sources = *Result.SourceManager;
171 
172   if (!locationsInSameFile(Sources, ND.getBeginLoc(), ND.getRBraceLoc()))
173     return;
174 
175   if (unsupportedNamespace(ND, false))
176     return;
177 
178   if (!ND.isNested())
179     Namespaces.push_back(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(&ND);
184 
185   if (singleNamedNamespaceChild(ND))
186     return;
187 
188   if (Namespaces.size() > 1)
189     reportDiagnostic(Sources, getLangOpts());
190 
191   Namespaces.clear();
192 }
193 
194 } // namespace clang::tidy::modernize
195