diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -56,6 +56,7 @@ FuzzyMatch.cpp GlobalCompilationDatabase.cpp Headers.cpp + HeaderSourceSwitch.cpp IncludeFixer.cpp JSONTransport.cpp Logger.cpp diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -449,7 +449,7 @@ } llvm::Optional ClangdServer::switchSourceHeader(PathRef Path) { - + // FIXME: move the file heuristic to HeaderSourceSwitch.h. llvm::StringRef SourceExtensions[] = {".cpp", ".c", ".cc", ".cxx", ".c++", ".m", ".mm"}; llvm::StringRef HeaderExtensions[] = {".h", ".hh", ".hpp", ".hxx", ".inc"}; diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.h b/clang-tools-extra/clangd/HeaderSourceSwitch.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.h @@ -0,0 +1,26 @@ +//===--- HeaderSourceSwitch.h - ----------------------------------*- C++-*-===// +// +// 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 "ParsedAST.h" +#include "llvm/ADT/Optional.h" + +namespace clang { +namespace clangd { + +/// Given a header file, returns a best matching source file, and vice visa. +/// The heuristics incorporate with the AST and the index. +llvm::Optional getCorrespondingHeaderOrSource(const Path &OriginalFile, + ParsedAST &AST, + const SymbolIndex *Index); + +/// Returns all indexable decls that are present in the main file of the AST. +/// Exposed for unittests. +std::vector getIndexableLocalDecls(ParsedAST &AST); + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -0,0 +1,128 @@ +//===--- HeaderSourceSwitch.cpp - --------------------------------*- C++-*-===// +// +// 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 "HeaderSourceSwitch.h" +#include "AST.h" +#include "Logger.h" +#include "index/SymbolCollector.h" +#include "clang/AST/Decl.h" + +namespace clang { +namespace clangd { +namespace { + +// Traverses the ParsedAST directly to collect all decls present in the main +// file. +class CollectIndexableLocalDecls { +public: + CollectIndexableLocalDecls(ParsedAST &AST) : AST(AST) {} + + std::vector collect() { + std::vector Results; + for (auto &TopLevel : AST.getLocalTopLevelDecls()) + traverseDecl(TopLevel, Results); + return Results; + } + +private: + void traverseDecl(Decl *D, std::vector &Results) { + auto *ND = llvm::dyn_cast(D); + if (!ND || ND->isImplicit()) + return; + if (!SymbolCollector::shouldCollectSymbol(*ND, D->getASTContext(), {}, + /*IsMainFileSymbol=*/false)) + return; + if (!shouldSkipChildren(ND)) { + if (auto *Scope = llvm::dyn_cast(ND)) { + for (auto *D : Scope->decls()) + traverseDecl(D, Results); + } + } + if (llvm::isa(D)) + return; // namespace is indexable, but we're not interested. + Results.push_back(D); + } + bool shouldSkipChildren(NamedDecl *D) { + // We're not interested in the function body. + if (llvm::isa(D)) + return true; + // For others e.g. namespace decl, class decl, we visit their children. + return false; + } + + ParsedAST * +}; + +llvm::Optional resolveURI(const char *FileURI, + llvm::StringRef HintPath) { + auto Uri = URI::parse(FileURI); + if (!Uri) { + elog("Could no parse URI {0}: {1}", FileURI, Uri.takeError()); + return None; + } + auto Path = URI::resolve(*Uri, HintPath); + if (!Path) { + elog("Could no resolve URI URI {0}: {1}", FileURI, Path.takeError()); + return None; + } + return *Path; +}; + +} // namespace + +llvm::Optional getCorrespondingHeaderOrSource(const Path &OriginalFile, + ParsedAST &AST, + const SymbolIndex *Index) { + if (!Index) { + elog("no index provided, no result returned"); + return None; + } + LookupRequest Request; + // Find all symbols present in the original file. + for (const auto *D : getIndexableLocalDecls(AST)) { + if (auto ID = getSymbolID(D)) + Request.IDs.insert(*ID); + } + llvm::StringMap Candidates; // Target path => score. + auto AwardTarget = [&](const char *TargetURI) { + if (auto TargetPath = resolveURI(TargetURI, OriginalFile)) { + if (*TargetPath != OriginalFile) // exclude the original file. + ++Candidates[*TargetPath]; + }; + }; + // If we switch from a header, we are looking for the implementation + // file, so we use the definition loc; otherwise we look for the header file, + // we use the decl loc; + // + // For each symbol in the original file, we get its target location (decl or + // def) from the index, then award that target file. + bool IsHeader = AST.getASTContext().getLangOpts().IsHeaderFile; + Index->lookup(Request, [&](const Symbol &Sym) { + if (IsHeader) + AwardTarget(Sym.Definition.FileURI); + else + AwardTarget(Sym.CanonicalDeclaration.FileURI); + }); + if (Candidates.empty()) + return None; + // Pickup the winner, who contains most of symbols. + // FIXME: should we use other signals (file proximity) to help score? + auto Best = Candidates.begin(); + for (auto It = Candidates.begin(); It != Candidates.end(); ++It) { + if (It->second > Best->second) + Best = It; + } + return Path(Best->first()); +} + +std::vector getIndexableLocalDecls(ParsedAST &AST) { + return CollectIndexableLocalDecls(AST).collect(); +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -46,6 +46,7 @@ FuzzyMatchTests.cpp GlobalCompilationDatabaseTests.cpp HeadersTests.cpp + HeaderSourceSwitchTests.cpp IndexActionTests.cpp IndexTests.cpp JSONTransportTests.cpp diff --git a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp @@ -0,0 +1,174 @@ +//===--- HeaderSourceSwitchTests.cpp - ---------------------------*- C++-*-===// +// +// 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 "HeaderSourceSwitch.h" + +#include "TestFS.h" +#include "TestTU.h" +#include "index/MemIndex.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +MATCHER_P(DeclNamed, Name, "") { + if (const NamedDecl *ND = dyn_cast(arg)) + if (ND->getQualifiedNameAsString() == Name) + return true; + return false; +} + +TEST(HeaderSourceSwitchTest, GetLocalDecls) { + TestTU TU; + TU.HeaderCode = R"cpp( + void HeaderOnly(); + )cpp"; + TU.Code = R"cpp( + void MainF1(); + class Foo {}; + namespace ns { + class Foo { + void method(); + int field; + }; + } // namespace ns + + // Non-indexable symbols + namespace { + void Ignore1() {} + } + + )cpp"; + + auto AST = TU.build(); + EXPECT_THAT(getIndexableLocalDecls(AST), + testing::UnorderedElementsAre( + DeclNamed("MainF1"), DeclNamed("Foo"), DeclNamed("ns::Foo"), + DeclNamed("ns::Foo::method"), DeclNamed("ns::Foo::field"))); +} + +TEST(HeaderSourceSwitchTest, FromHeaderToSource) { + // build a proper index, which contains symbols: + // A_Sym1, declared in TestTU.h, defined in a.cpp + // B_Sym[1-2], declared in TestTU.h, defined in b.cpp + SymbolSlab::Builder AllSymbols; + TestTU Testing; + Testing.HeaderFilename = "TestTU.h"; + Testing.HeaderCode = "void A_Sym1();"; + Testing.Filename = "a.cpp"; + Testing.Code = "void A_Sym1() {};"; + for (auto &Sym : Testing.headerSymbols()) + AllSymbols.insert(Sym); + + Testing.HeaderCode = R"cpp( + void B_Sym1(); + void B_Sym2(); + )cpp"; + Testing.Filename = "b.cpp"; + Testing.Code = R"cpp( + void B_Sym1() {} + void B_Sym2() {} + )cpp"; + for (auto &Sym : Testing.headerSymbols()) + AllSymbols.insert(Sym); + auto Index = MemIndex::build(std::move(AllSymbols).build(), {}, {}); + + // Test for swtich from .h header to .cc source + struct { + llvm::StringRef HeaderCode; + llvm::Optional ExpectedSource; + } TestCases[] = {{R"cpp(// empty, no header found)cpp", llvm::None}, + {R"cpp( + // no definition found in the index. + void NonDefinition(); + )cpp", + llvm::None}, + {R"cpp( + void A_Sym1(); + )cpp", + testPath("a.cpp")}, + {R"cpp( + // b.cpp wins. + void A_Sym1(); + void B_Sym1(); + void B_Sym2(); + )cpp", + testPath("b.cpp")}}; + const std::string &TestFilePath = testPath("TestTU.h"); + for (const auto &Case : TestCases) { + TestTU TU = TestTU::withCode(Case.HeaderCode); + TU.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header. + auto HeaderAST = TU.build(); + EXPECT_EQ(Case.ExpectedSource, getCorrespondingHeaderOrSource( + TestFilePath, HeaderAST, Index.get())); + } +} + +TEST(HeaderSourceSwitchTest, FromSourceToHeader) { + // build a proper index, which contains symbols: + // A_Sym1, declared in a.h, defined in TestTU.cpp + // B_Sym[1-2], declared in b.h, defined in TestTU.cpp + TestTU TUForIndex = TestTU::withCode(R"cpp( + #include "a.h" + #include "b.h" + + void A_Sym1() {} + + void B_Sym1() {} + void B_Sym2() {} + )cpp"); + TUForIndex.AdditionalFiles["a.h"] = R"cpp( + void A_Sym1(); + )cpp"; + TUForIndex.AdditionalFiles["b.h"] = R"cpp( + void B_Sym1(); + void B_Sym2(); + )cpp"; + TUForIndex.Filename = "TestTU.cpp"; + auto Index = TUForIndex.index(); + + // Test for switching from .cc source file to .h header. + struct { + llvm::StringRef SourceCode; + llvm::Optional ExpectedResult; + } TestCases[] = { + {R"cpp(// empty, no header found)cpp", llvm::None}, + {R"cpp( + // symbol not in index, no header found + void Local() {} + )cpp", + llvm::None}, + + {R"cpp( + // a.h wins. + void A_Sym1() {} + )cpp", + testPath("a.h")}, + + {R"cpp( + // b.h wins. + void A_Sym1() {} + void B_Sym1() {} + void B_Sym2() {} + )cpp", + testPath("b.h")}, + }; + const std::string &TestFilePath = testPath("TestTU.cpp"); + for (const auto &Case : TestCases) { + TestTU TU = TestTU::withCode(Case.SourceCode); + auto AST = TU.build(); + EXPECT_EQ(Case.ExpectedResult, + getCorrespondingHeaderOrSource(TestFilePath, AST, Index.get())); + } +} + +} // namespace +} // namespace clangd +} // namespace clang