xref: /llvm-project/clang-tools-extra/clang-tidy/modernize/ConcatNestedNamespacesCheck.cpp (revision bdf7fd8297bcbcddc9c184a40c954c1f1b0b8340)
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 "llvm/ADT/STLExtras.h"
16 #include <algorithm>
17 #include <optional>
18 
19 namespace clang::tidy::modernize {
20 
21 static bool locationsInSameFile(const SourceManager &Sources,
22                                 SourceLocation Loc1, SourceLocation Loc2) {
23   return Loc1.isFileID() && Loc2.isFileID() &&
24          Sources.getFileID(Loc1) == Sources.getFileID(Loc2);
25 }
26 
27 static StringRef getRawStringRef(const SourceRange &Range,
28                                  const SourceManager &Sources,
29                                  const LangOptions &LangOpts) {
30   CharSourceRange TextRange = Lexer::getAsCharRange(Range, Sources, LangOpts);
31   return Lexer::getSourceText(TextRange, Sources, LangOpts);
32 }
33 
34 static bool unsupportedNamespace(const NamespaceDecl &ND) {
35   return ND.isAnonymousNamespace() || ND.isInlineNamespace() ||
36          !ND.attrs().empty();
37 }
38 
39 static bool singleNamedNamespaceChild(const NamespaceDecl &ND) {
40   NamespaceDecl::decl_range Decls = ND.decls();
41   if (std::distance(Decls.begin(), Decls.end()) != 1)
42     return false;
43 
44   const auto *ChildNamespace = dyn_cast<const NamespaceDecl>(*Decls.begin());
45   return ChildNamespace && !unsupportedNamespace(*ChildNamespace);
46 }
47 
48 static bool alreadyConcatenated(std::size_t NumCandidates,
49                                 const SourceRange &ReplacementRange,
50                                 const SourceManager &Sources,
51                                 const LangOptions &LangOpts) {
52   // FIXME: This logic breaks when there is a comment with ':'s in the middle.
53   return getRawStringRef(ReplacementRange, Sources, LangOpts).count(':') ==
54          (NumCandidates - 1) * 2;
55 }
56 
57 static std::optional<SourceRange>
58 getCleanedNamespaceFrontRange(const NamespaceDecl *ND, const SourceManager &SM,
59                               const LangOptions &LangOpts) {
60   // Front from namespace tp '{'
61   std::optional<Token> Tok =
62       ::clang::tidy::utils::lexer::findNextTokenSkippingComments(
63           ND->getLocation(), SM, LangOpts);
64   if (!Tok)
65     return std::nullopt;
66   while (Tok->getKind() != tok::TokenKind::l_brace) {
67     Tok = utils::lexer::findNextTokenSkippingComments(Tok->getEndLoc(), SM,
68                                                       LangOpts);
69     if (!Tok)
70       return std::nullopt;
71   }
72   return SourceRange{ND->getBeginLoc(), Tok->getEndLoc()};
73 }
74 
75 static SourceRange getCleanedNamespaceBackRange(const NamespaceDecl *ND,
76                                                 const SourceManager &SM,
77                                                 const LangOptions &LangOpts) {
78   // Back from '}' to conditional '// namespace xxx'
79   const SourceRange DefaultSourceRange =
80       SourceRange{ND->getRBraceLoc(), ND->getRBraceLoc()};
81   SourceLocation Loc = ND->getRBraceLoc();
82   std::optional<Token> Tok =
83       utils::lexer::findNextTokenIncludingComments(Loc, SM, LangOpts);
84   if (!Tok)
85     return DefaultSourceRange;
86   if (Tok->getKind() != tok::TokenKind::comment)
87     return DefaultSourceRange;
88   SourceRange TokRange = SourceRange{Tok->getLocation(), Tok->getEndLoc()};
89   StringRef TokText = getRawStringRef(TokRange, SM, LangOpts);
90   std::string CloseComment = "namespace " + ND->getNameAsString();
91   // current fix hint in readability/NamespaceCommentCheck.cpp use single line
92   // comment
93   if (TokText != "// " + CloseComment && TokText != "//" + CloseComment)
94     return DefaultSourceRange;
95   return SourceRange{ND->getRBraceLoc(), Tok->getEndLoc()};
96 }
97 
98 ConcatNestedNamespacesCheck::NamespaceString
99 ConcatNestedNamespacesCheck::concatNamespaces() {
100   NamespaceString Result("namespace ");
101   Result.append(Namespaces.front()->getName());
102 
103   std::for_each(std::next(Namespaces.begin()), Namespaces.end(),
104                 [&Result](const NamespaceDecl *ND) {
105                   Result.append("::");
106                   Result.append(ND->getName());
107                 });
108 
109   return Result;
110 }
111 
112 void ConcatNestedNamespacesCheck::registerMatchers(
113     ast_matchers::MatchFinder *Finder) {
114   Finder->addMatcher(ast_matchers::namespaceDecl().bind("namespace"), this);
115 }
116 
117 void ConcatNestedNamespacesCheck::reportDiagnostic(
118     const SourceManager &SM, const LangOptions &LangOpts) {
119   DiagnosticBuilder DB =
120       diag(Namespaces.front()->getBeginLoc(),
121            "nested namespaces can be concatenated", DiagnosticIDs::Warning);
122 
123   SmallVector<SourceRange, 6> Fronts;
124   Fronts.reserve(Namespaces.size() - 1U);
125   SmallVector<SourceRange, 6> Backs;
126   Backs.reserve(Namespaces.size());
127 
128   NamespaceDecl const *LastNonNestND = nullptr;
129 
130   for (const NamespaceDecl *ND : Namespaces) {
131     if (ND->isNested())
132       continue;
133     LastNonNestND = ND;
134     std::optional<SourceRange> SR =
135         getCleanedNamespaceFrontRange(ND, SM, LangOpts);
136     if (!SR.has_value())
137       return;
138     Fronts.push_back(SR.value());
139     Backs.push_back(getCleanedNamespaceBackRange(ND, SM, LangOpts));
140   }
141   if (LastNonNestND == nullptr || Fronts.empty() || Backs.empty())
142     return;
143   // the last one should be handled specially
144   Fronts.pop_back();
145   SourceRange LastRBrace = Backs.pop_back_val();
146   NamespaceString ConcatNameSpace = concatNamespaces();
147 
148   for (SourceRange const &Front : Fronts)
149     DB << FixItHint::CreateRemoval(Front);
150   DB << FixItHint::CreateReplacement(
151       SourceRange{LastNonNestND->getBeginLoc(),
152                   Namespaces.back()->getLocation()},
153       ConcatNameSpace);
154   if (LastRBrace !=
155       SourceRange{LastNonNestND->getRBraceLoc(), LastNonNestND->getRBraceLoc()})
156     DB << FixItHint::CreateReplacement(LastRBrace,
157                                        ("} // " + ConcatNameSpace).str());
158   for (SourceRange const &Back : llvm::reverse(Backs))
159     DB << FixItHint::CreateRemoval(Back);
160 }
161 
162 void ConcatNestedNamespacesCheck::check(
163     const ast_matchers::MatchFinder::MatchResult &Result) {
164   const NamespaceDecl &ND = *Result.Nodes.getNodeAs<NamespaceDecl>("namespace");
165   const SourceManager &Sources = *Result.SourceManager;
166 
167   if (!locationsInSameFile(Sources, ND.getBeginLoc(), ND.getRBraceLoc()))
168     return;
169 
170   if (unsupportedNamespace(ND))
171     return;
172 
173   Namespaces.push_back(&ND);
174 
175   if (singleNamedNamespaceChild(ND))
176     return;
177 
178   SourceRange FrontReplacement(Namespaces.front()->getBeginLoc(),
179                                Namespaces.back()->getLocation());
180 
181   if (!alreadyConcatenated(Namespaces.size(), FrontReplacement, Sources,
182                            getLangOpts()))
183     reportDiagnostic(Sources, getLangOpts());
184 
185   Namespaces.clear();
186 }
187 
188 } // namespace clang::tidy::modernize
189