| //===--- ConcatNestedNamespacesCheck.cpp - clang-tidy----------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "ConcatNestedNamespacesCheck.h" |
| #include "../utils/LexerUtils.h" |
| #include "clang/AST/ASTContext.h" |
| #include "clang/AST/Decl.h" |
| #include "clang/ASTMatchers/ASTMatchFinder.h" |
| #include "clang/Basic/SourceLocation.h" |
| #include <algorithm> |
| #include <optional> |
| |
| namespace clang::tidy::modernize { |
| |
| static bool locationsInSameFile(const SourceManager &Sources, |
| SourceLocation Loc1, SourceLocation Loc2) { |
| return Loc1.isFileID() && Loc2.isFileID() && |
| Sources.getFileID(Loc1) == Sources.getFileID(Loc2); |
| } |
| |
| static StringRef getRawStringRef(const SourceRange &Range, |
| const SourceManager &Sources, |
| const LangOptions &LangOpts) { |
| CharSourceRange TextRange = Lexer::getAsCharRange(Range, Sources, LangOpts); |
| return Lexer::getSourceText(TextRange, Sources, LangOpts); |
| } |
| |
| std::optional<SourceRange> |
| NS::getCleanedNamespaceFrontRange(const SourceManager &SM, |
| const LangOptions &LangOpts) const { |
| // Front from namespace tp '{' |
| std::optional<Token> Tok = |
| ::clang::tidy::utils::lexer::findNextTokenSkippingComments( |
| back()->getLocation(), SM, LangOpts); |
| if (!Tok) |
| return std::nullopt; |
| while (Tok->getKind() != tok::TokenKind::l_brace) { |
| Tok = utils::lexer::findNextTokenSkippingComments(Tok->getEndLoc(), SM, |
| LangOpts); |
| if (!Tok) |
| return std::nullopt; |
| } |
| return SourceRange{front()->getBeginLoc(), Tok->getEndLoc()}; |
| } |
| SourceRange NS::getReplacedNamespaceFrontRange() const { |
| return SourceRange{front()->getBeginLoc(), back()->getLocation()}; |
| } |
| |
| SourceRange NS::getDefaultNamespaceBackRange() const { |
| return SourceRange{front()->getRBraceLoc(), front()->getRBraceLoc()}; |
| } |
| SourceRange NS::getNamespaceBackRange(const SourceManager &SM, |
| const LangOptions &LangOpts) const { |
| // Back from '}' to conditional '// namespace xxx' |
| SourceLocation Loc = front()->getRBraceLoc(); |
| std::optional<Token> Tok = |
| utils::lexer::findNextTokenIncludingComments(Loc, SM, LangOpts); |
| if (!Tok) |
| return getDefaultNamespaceBackRange(); |
| if (Tok->getKind() != tok::TokenKind::comment) |
| return getDefaultNamespaceBackRange(); |
| SourceRange TokRange = SourceRange{Tok->getLocation(), Tok->getEndLoc()}; |
| StringRef TokText = getRawStringRef(TokRange, SM, LangOpts); |
| NamespaceName CloseComment{"namespace "}; |
| appendCloseComment(CloseComment); |
| // current fix hint in readability/NamespaceCommentCheck.cpp use single line |
| // comment |
| constexpr size_t L = sizeof("//") - 1U; |
| if (TokText.take_front(L) == "//" && |
| TokText.drop_front(L).trim() != CloseComment) |
| return getDefaultNamespaceBackRange(); |
| return SourceRange{front()->getRBraceLoc(), Tok->getEndLoc()}; |
| } |
| |
| void NS::appendName(NamespaceName &Str) const { |
| for (const NamespaceDecl *ND : *this) { |
| if (ND->isInlineNamespace()) |
| Str.append("inline "); |
| Str.append(ND->getName()); |
| if (ND != back()) |
| Str.append("::"); |
| } |
| } |
| void NS::appendCloseComment(NamespaceName &Str) const { |
| if (size() == 1) |
| Str.append(back()->getName()); |
| else |
| appendName(Str); |
| } |
| |
| bool ConcatNestedNamespacesCheck::unsupportedNamespace(const NamespaceDecl &ND, |
| bool IsChild) const { |
| if (ND.isAnonymousNamespace() || !ND.attrs().empty()) |
| return true; |
| if (getLangOpts().CPlusPlus20) { |
| // C++20 support inline nested namespace |
| bool IsFirstNS = IsChild || !Namespaces.empty(); |
| return ND.isInlineNamespace() && !IsFirstNS; |
| } |
| return ND.isInlineNamespace(); |
| } |
| |
| bool ConcatNestedNamespacesCheck::singleNamedNamespaceChild( |
| const NamespaceDecl &ND) const { |
| NamespaceDecl::decl_range Decls = ND.decls(); |
| if (std::distance(Decls.begin(), Decls.end()) != 1) |
| return false; |
| |
| const auto *ChildNamespace = dyn_cast<const NamespaceDecl>(*Decls.begin()); |
| return ChildNamespace && !unsupportedNamespace(*ChildNamespace, true); |
| } |
| |
| void ConcatNestedNamespacesCheck::registerMatchers( |
| ast_matchers::MatchFinder *Finder) { |
| Finder->addMatcher(ast_matchers::namespaceDecl().bind("namespace"), this); |
| } |
| |
| void ConcatNestedNamespacesCheck::reportDiagnostic( |
| const SourceManager &SM, const LangOptions &LangOpts) { |
| DiagnosticBuilder DB = |
| diag(Namespaces.front().front()->getBeginLoc(), |
| "nested namespaces can be concatenated", DiagnosticIDs::Warning); |
| |
| SmallVector<SourceRange, 6> Fronts; |
| Fronts.reserve(Namespaces.size() - 1U); |
| SmallVector<SourceRange, 6> Backs; |
| Backs.reserve(Namespaces.size()); |
| |
| for (const NS &ND : Namespaces) { |
| std::optional<SourceRange> SR = |
| ND.getCleanedNamespaceFrontRange(SM, LangOpts); |
| if (!SR) |
| return; |
| Fronts.push_back(SR.value()); |
| Backs.push_back(ND.getNamespaceBackRange(SM, LangOpts)); |
| } |
| if (Fronts.empty() || Backs.empty()) |
| return; |
| |
| // the last one should be handled specially |
| Fronts.pop_back(); |
| SourceRange LastRBrace = Backs.pop_back_val(); |
| |
| NamespaceName ConcatNameSpace{"namespace "}; |
| for (const NS &NS : Namespaces) { |
| NS.appendName(ConcatNameSpace); |
| if (&NS != &Namespaces.back()) // compare address directly |
| ConcatNameSpace.append("::"); |
| } |
| |
| for (SourceRange const &Front : Fronts) |
| DB << FixItHint::CreateRemoval(Front); |
| DB << FixItHint::CreateReplacement( |
| Namespaces.back().getReplacedNamespaceFrontRange(), ConcatNameSpace); |
| if (LastRBrace != Namespaces.back().getDefaultNamespaceBackRange()) |
| DB << FixItHint::CreateReplacement(LastRBrace, |
| ("} // " + ConcatNameSpace).str()); |
| for (SourceRange const &Back : llvm::reverse(Backs)) |
| DB << FixItHint::CreateRemoval(Back); |
| } |
| |
| void ConcatNestedNamespacesCheck::check( |
| const ast_matchers::MatchFinder::MatchResult &Result) { |
| const NamespaceDecl &ND = *Result.Nodes.getNodeAs<NamespaceDecl>("namespace"); |
| const SourceManager &Sources = *Result.SourceManager; |
| |
| if (!locationsInSameFile(Sources, ND.getBeginLoc(), ND.getRBraceLoc())) |
| return; |
| |
| if (unsupportedNamespace(ND, false)) |
| return; |
| |
| if (!ND.isNested()) |
| Namespaces.push_back(NS{}); |
| if (!Namespaces.empty()) |
| // Otherwise it will crash with invalid input like `inline namespace |
| // a::b::c`. |
| Namespaces.back().push_back(&ND); |
| |
| if (singleNamedNamespaceChild(ND)) |
| return; |
| |
| if (Namespaces.size() > 1) |
| reportDiagnostic(Sources, getLangOpts()); |
| |
| Namespaces.clear(); |
| } |
| |
| } // namespace clang::tidy::modernize |