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