diff --git a/clang-tools-extra/clangd/AST.h b/clang-tools-extra/clangd/AST.h --- a/clang-tools-extra/clangd/AST.h +++ b/clang-tools-extra/clangd/AST.h @@ -15,8 +15,10 @@ #include "index/SymbolID.h" #include "clang/AST/Decl.h" +#include "clang/AST/NestedNameSpecifier.h" #include "clang/Basic/SourceLocation.h" #include "clang/Lex/MacroInfo.h" +#include "llvm/ADT/DenseSet.h" namespace clang { class SourceManager; @@ -110,6 +112,20 @@ /// void foo() -> returns null NestedNameSpecifierLoc getQualifierLoc(const NamedDecl &ND); +/// Gets the nested name specifier necessary for spelling \p ND in \p +/// DestContext, at \p InsertionPoint. It selects the shortest suffix of \p ND +/// such that it is visible in \p DestContext, considering all the using +/// directives before \p InsertionPoint. +/// Returns null if no qualification is necessary. For example, if you want to +/// qualify clang::clangd::bar::foo in clang::clangd::x, this function will +/// return bar. Also if you have `using namespace clang::clangd::bar` before \p +/// InsertionPoint this function will return null, since no qualification is +/// necessary in that case. +NestedNameSpecifier *getQualification(ASTContext &Context, + const DeclContext *DestContext, + SourceLocation InsertionPoint, + const NamedDecl *ND); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/AST.cpp b/clang-tools-extra/clangd/AST.cpp --- a/clang-tools-extra/clangd/AST.cpp +++ b/clang-tools-extra/clangd/AST.cpp @@ -244,6 +244,59 @@ printNamespaceScope(Context) ); } +NestedNameSpecifier *getQualification(ASTContext &Context, + const DeclContext *DestContext, + SourceLocation InsertionPoint, + const NamedDecl *ND) { + const auto &SM = Context.getSourceManager(); + // Store all UsingDirectiveDecls in parent contexts of DestContext, that were + // introduced before InsertionPoint. + llvm::DenseSet VisibleNamespaceDecls; + for (const auto *DC = DestContext; DC; DC = DC->getLookupParent()) { + for (const auto *D : DC->decls()) { + if (!SM.isWrittenInSameFile(D->getLocation(), InsertionPoint) || + !SM.isBeforeInTranslationUnit(D->getLocation(), InsertionPoint)) + continue; + if (auto *UDD = llvm::dyn_cast(D)) { + VisibleNamespaceDecls.insert( + UDD->getNominatedNamespace()->getCanonicalDecl()); + } + } + } + + // Goes over all parents of ND until we find a comman ancestor for DestContext + // and ND. Any qualifier including and above common ancestor is redundant, + // therefore we stop at lowest common ancestor. + std::vector SourceParents; + for (const DeclContext *Context = ND->getLexicalDeclContext(); Context; + Context = Context->getLookupParent()) { + // Stop once we reach a common ancestor. + if (Context->Encloses(DestContext)) + break; + // Inline namespace names are not spelled while qualifying a name, so skip + // those. + if (Context->isInlineNamespace()) + continue; + + auto *NSD = llvm::dyn_cast(Context); + assert(NSD && "Non-namespace decl context found."); + // Stop if this namespace is already visible at DestContext. + if (VisibleNamespaceDecls.count(NSD->getCanonicalDecl())) + break; + // Again, ananoymous namespaces are not spelled while qualifying a name. + if (NSD->isAnonymousNamespace()) + continue; + + SourceParents.push_back(NSD); + } + + // Go over name-specifiers in reverse order to create necessary qualification, + // since we stored inner-most parent first. + NestedNameSpecifier *Result = nullptr; + for (const auto *Parent : llvm::reverse(SourceParents)) + Result = NestedNameSpecifier::Create(Context, Result, Parent); + return Result; +} } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/ASTTests.cpp b/clang-tools-extra/clangd/unittests/ASTTests.cpp --- a/clang-tools-extra/clangd/unittests/ASTTests.cpp +++ b/clang-tools-extra/clangd/unittests/ASTTests.cpp @@ -7,7 +7,16 @@ //===----------------------------------------------------------------------===// #include "AST.h" +#include "Annotations.h" +#include "ParsedAST.h" +#include "TestTU.h" +#include "clang/AST/Decl.h" +#include "clang/AST/DeclBase.h" +#include "llvm/ADT/StringRef.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" +#include +#include namespace clang { namespace clangd { @@ -36,6 +45,66 @@ "testns1::TestClass", "testns1")); } +TEST(ClangdAST, GetQualification) { + struct { + llvm::StringRef Test; + std::vector Qualifications; + } Cases[] = { + { + R"cpp( + namespace ns1 { namespace ns2 { class Foo {}; } } + void insert(); // ns1::ns2::Foo + namespace ns1 { + void insert(); // ns2::Foo + namespace ns2 { + void insert(); // Foo + } + using namespace ns2; + void insert(); // Foo + } + using namespace ns1; + void insert(); // ns2::Foo + using namespace ns2; + void insert(); // Foo + )cpp", + {"ns1::ns2::", "ns2::", "", "", "ns2::", ""}, + }, + }; + for (const auto &Case : Cases) { + Annotations Test(Case.Test); + TestTU TU = TestTU::withCode(Test.code()); + ParsedAST AST = TU.build(); + std::vector InsertionPoints; + const NamedDecl *TargetDecl; + findDecl(AST, [&](const NamedDecl &ND) { + if (ND.getNameAsString() == "Foo") { + TargetDecl = &ND; + return true; + } + + if (ND.getNameAsString() == "insert") + InsertionPoints.push_back(&ND); + return false; + }); + + ASSERT_EQ(InsertionPoints.size(), Case.Qualifications.size()); + for (size_t I = 0, E = InsertionPoints.size(); I != E; ++I) { + const Decl *D = InsertionPoints[I]; + auto Qual = + getQualification(AST.getASTContext(), D->getLexicalDeclContext(), + D->getBeginLoc(), TargetDecl); + if (!Qual) + EXPECT_TRUE(Case.Qualifications[I].empty()); + else { + std::string Qualifier; + llvm::raw_string_ostream OS(Qualifier); + Qual->print(OS, AST.getASTContext().getPrintingPolicy(), true); + EXPECT_EQ(OS.str(), Case.Qualifications[I]); + } + } + } +} + } // namespace } // namespace clangd } // namespace clang