diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h --- a/clang-tools-extra/clangd/XRefs.h +++ b/clang-tools-extra/clangd/XRefs.h @@ -105,6 +105,17 @@ TypeHierarchyDirection Direction, const SymbolIndex *Index); +/// Get call hierarchy information at \p Pos. +llvm::Optional> +prepareCallHierarchy(ParsedAST &AST, Position Pos, const SymbolIndex *Index, + PathRef TUPath); + +llvm::Optional> +incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index); + +llvm::Optional> +outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index); + /// Returns all decls that are referenced in the \p FD except local symbols. llvm::DenseSet getNonLocalDeclRefs(ParsedAST &AST, const FunctionDecl *FD); diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp --- a/clang-tools-extra/clangd/XRefs.cpp +++ b/clang-tools-extra/clangd/XRefs.cpp @@ -47,6 +47,7 @@ #include "clang/Index/USRGeneration.h" #include "clang/Tooling/Syntax/Tokens.h" #include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/MapVector.h" #include "llvm/ADT/None.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" @@ -1313,9 +1314,8 @@ return THI; } -static Optional -symbolToTypeHierarchyItem(const Symbol &S, const SymbolIndex *Index, - PathRef TUPath) { +static Optional symbolToTypeHierarchyItem(const Symbol &S, + PathRef TUPath) { auto Loc = symbolToLocation(S, TUPath); if (!Loc) { log("Type hierarchy: {0}", Loc.takeError()); @@ -1346,7 +1346,7 @@ Req.Predicate = RelationKind::BaseOf; Index->relations(Req, [&](const SymbolID &Subject, const Symbol &Object) { if (Optional ChildSym = - symbolToTypeHierarchyItem(Object, Index, TUPath)) { + symbolToTypeHierarchyItem(Object, TUPath)) { if (Levels > 1) { ChildSym->children.emplace(); fillSubTypes(Object.ID, *ChildSym->children, Index, Levels - 1, TUPath); @@ -1540,6 +1540,157 @@ } } +static llvm::Optional +declToCallHierarchyItem(ASTContext &Ctx, const NamedDecl &ND) { + auto &SM = Ctx.getSourceManager(); + SourceLocation NameLoc = nameLocation(ND, Ctx.getSourceManager()); + SourceLocation BeginLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getBeginLoc())); + SourceLocation EndLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getEndLoc())); + const auto DeclRange = + toHalfOpenFileRange(SM, Ctx.getLangOpts(), {BeginLoc, EndLoc}); + if (!DeclRange) + return llvm::None; + auto FilePath = + getCanonicalPath(SM.getFileEntryForID(SM.getFileID(NameLoc)), SM); + auto TUPath = getCanonicalPath(SM.getFileEntryForID(SM.getMainFileID()), SM); + if (!FilePath || !TUPath) + return llvm::None; // Not useful without a uri. + + Position NameBegin = sourceLocToPosition(SM, NameLoc); + Position NameEnd = sourceLocToPosition( + SM, Lexer::getLocForEndOfToken(NameLoc, 0, SM, Ctx.getLangOpts())); + + index::SymbolInfo SymInfo = index::getSymbolInfo(&ND); + // FIXME: This is not classifying constructors, destructors and operators + // correctly. + SymbolKind SK = indexSymbolKindToSymbolKind(SymInfo.Kind); + + CallHierarchyItem CHI; + // We need to print the fully qualified name, otherwise we can't recover + // the symbol from the CallHierarchyItem. + CHI.Name = printQualifiedName(ND); + CHI.Kind = SK; + if (ND.isDeprecated()) { + CHI.Tags.push_back(SymbolTag::Deprecated); + } + CHI.Rng = Range{sourceLocToPosition(SM, DeclRange->getBegin()), + sourceLocToPosition(SM, DeclRange->getEnd())}; + CHI.SelectionRange = Range{NameBegin, NameEnd}; + if (!CHI.Rng.contains(CHI.Rng)) { + // 'selectionRange' must be contained in 'range', so in cases where clang + // reports unrelated ranges we need to reconcile somehow. + CHI.Rng = CHI.SelectionRange; + } + + CHI.Uri = URIForFile::canonicalize(*FilePath, *TUPath); + + // Compute the SymbolID and store it in the 'data' field. + // This allows typeHierarchy/resolve to be used to + // resolve children of items returned in a previous request + // for parents. + if (auto ID = getSymbolID(&ND)) { + CHI.Data = ID.str(); + } + + return CHI; +} + +llvm::Optional> +prepareCallHierarchy(ParsedAST &AST, Position Pos, const SymbolIndex *Index, + PathRef TUPath) { + const auto &SM = AST.getSourceManager(); + auto Loc = sourceLocationInMainFile(SM, Pos); + if (!Loc) { + llvm::consumeError(Loc.takeError()); + return llvm::None; + } + std::vector Result; + for (const NamedDecl *Decl : getDeclAtPosition(AST, *Loc, {})) { + if (Decl->isFunctionOrFunctionTemplate()) { + if (auto CHI = declToCallHierarchyItem(AST.getASTContext(), *Decl)) + Result.push_back(*std::move(CHI)); + } + } + return Result; +} + +llvm::Optional symbolToCallHierarchyItem(const Symbol &S, + PathRef TUPath) { + auto Loc = symbolToLocation(S, TUPath); + if (!Loc) { + log("Type hierarchy: {0}", Loc.takeError()); + return llvm::None; + } + CallHierarchyItem CHI; + CHI.Name = std::string(S.Name); + CHI.Kind = indexSymbolKindToSymbolKind(S.SymInfo.Kind); + if (S.Flags & Symbol::Deprecated) + CHI.Tags.push_back(SymbolTag::Deprecated); + CHI.Detail = S.Signature.str(); + CHI.SelectionRange = Loc->range; + // FIXME: Populate 'range' correctly + // (https://github.com/clangd/clangd/issues/59). + CHI.Rng = CHI.SelectionRange; + CHI.Uri = Loc->uri; + // Store the SymbolID in the 'data' field. The client will + // send this back in incomingCalls and outgoingCalls, allowing us to + // continue resolving additional levels of the type hierarchy. + CHI.Data = S.ID.str(); + + return std::move(CHI); +} + +llvm::Optional> +incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) { + if (!Index || !Item.Data) + return llvm::None; + Expected ID = SymbolID::fromStr(*Item.Data); + if (!ID) + return llvm::None; + RefsRequest Request; + Request.IDs.insert(*ID); + // FIXME: Perhaps we should be even more specific and introduce a + // RefKind for calls, and use that? + Request.Filter = RefKind::Reference; + // Initially store the results in a map keyed by SymbolID. + // This allows us to group different calls with the same caller + // into the same CallHierarchyIncomingCall. + llvm::DenseMap ResultMap; + Index->refs(Request, [&](const Ref &R) { + if (auto Loc = indexToLSPLocation(R.Location, Item.Uri.file())) { + LookupRequest Lookup; + Lookup.IDs.insert(R.Container); + Index->lookup(Lookup, [&](const Symbol &Caller) { + // See if we already have a CallHierarchyIncomingCall for this caller. + auto It = ResultMap.find(Caller.ID); + if (It == ResultMap.end()) { + // If not, try to create one. + if (auto CHI = symbolToCallHierarchyItem(Caller, Item.Uri.file())) { + CallHierarchyIncomingCall Call; + Call.From = *CHI; + It = ResultMap.insert({Caller.ID, std::move(Call)}).first; + } + } + if (It != ResultMap.end()) { + It->second.FromRanges.push_back(Loc->range); + } + }); + } + }); + // Flatten the results into a vector. + std::vector Results; + for (auto &&Entry : ResultMap) { + Results.push_back(std::move(Entry.second)); + } + return Results; +} + +llvm::Optional> +outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) { + // TODO: Implement. + return llvm::None; +} + llvm::DenseSet getNonLocalDeclRefs(ParsedAST &AST, const FunctionDecl *FD) { if (!FD->hasBody()) 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 @@ -36,6 +36,7 @@ Annotations.cpp ASTTests.cpp BackgroundIndexTests.cpp + CallHierarchyTests.cpp CanonicalIncludesTests.cpp ClangdTests.cpp ClangdLSPServerTests.cpp diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp @@ -0,0 +1,272 @@ +//===-- CallHierarchyTests.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 "Annotations.h" +#include "Compiler.h" +#include "Matchers.h" +#include "ParsedAST.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestTU.h" +#include "TestWorkspace.h" +#include "XRefs.h" +#include "index/FileIndex.h" +#include "index/SymbolCollector.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/DeclTemplate.h" +#include "clang/Index/IndexingAction.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::Matcher; +using ::testing::UnorderedElementsAre; + +// Helpers for matching call hierarchy data structures. +MATCHER_P(WithName, N, "") { return arg.Name == N; } +MATCHER_P(WithSelectionRange, R, "") { return arg.SelectionRange == R; } + +template +::testing::Matcher From(ItemMatcher M) { + return Field(&CallHierarchyIncomingCall::From, M); +} +template +::testing::Matcher FromRanges(RangeMatchers... M) { + return Field(&CallHierarchyIncomingCall::FromRanges, + UnorderedElementsAre(M...)); +} + +TEST(CallHierarchy, IncomingOneFile) { + Annotations Source(R"cpp( + void call^ee(int); + void caller1() { + $Callee[[callee]](42); + } + void caller2() { + $Caller1A[[caller1]](); + $Caller1B[[caller1]](); + } + void caller3() { + $Caller1C[[caller1]](); + $Caller2[[caller2]](); + } + )cpp"); + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + auto Index = TU.index(); + + llvm::Optional> Items = prepareCallHierarchy( + AST, Source.point(), Index.get(), testPath(TU.Filename)); + ASSERT_TRUE(bool(Items)); + EXPECT_THAT(*Items, ElementsAre(WithName("callee"))); + auto IncomingLevel1 = incomingCalls((*Items)[0], Index.get()); + ASSERT_TRUE(bool(IncomingLevel1)); + EXPECT_THAT(*IncomingLevel1, + ElementsAre(AllOf(From(WithName("caller1")), + FromRanges(Source.range("Callee"))))); + + auto IncomingLevel2 = incomingCalls((*IncomingLevel1)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel2)); + EXPECT_THAT( + *IncomingLevel2, + UnorderedElementsAre( + AllOf(From(WithName("caller2")), + FromRanges(Source.range("Caller1A"), Source.range("Caller1B"))), + AllOf(From(WithName("caller3")), + FromRanges(Source.range("Caller1C"))))); + + auto IncomingLevel3 = incomingCalls((*IncomingLevel2)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel3)); + EXPECT_THAT(*IncomingLevel3, + ElementsAre(AllOf(From(WithName("caller3")), + FromRanges(Source.range("Caller2"))))); + + auto IncomingLevel4 = incomingCalls((*IncomingLevel3)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel4)); + EXPECT_THAT(*IncomingLevel4, ElementsAre()); +} + +TEST(CallHierarchy, MainFileOnlyRef) { + // In addition to testing that we store refs to main-file only symbols, + // this tests that anonymous namespaces do not interfere with the + // symbol re-identification process in callHierarchyItemToSymbo(). + Annotations Source(R"cpp( + void call^ee(int); + namespace { + void caller1() { + $Callee[[callee]](42); + } + } + void caller2() { + $Caller1[[caller1]](); + } + )cpp"); + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + auto Index = TU.index(); + + llvm::Optional> Items = prepareCallHierarchy( + AST, Source.point(), Index.get(), testPath(TU.Filename)); + ASSERT_TRUE(bool(Items)); + EXPECT_THAT(*Items, ElementsAre(WithName("callee"))); + auto IncomingLevel1 = incomingCalls((*Items)[0], Index.get()); + ASSERT_TRUE(bool(IncomingLevel1)); + EXPECT_THAT(*IncomingLevel1, + ElementsAre(AllOf(From(WithName("caller1")), + FromRanges(Source.range("Callee"))))); + + auto IncomingLevel2 = incomingCalls((*IncomingLevel1)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel2)); + EXPECT_THAT(*IncomingLevel2, + UnorderedElementsAre(AllOf(From(WithName("caller2")), + FromRanges(Source.range("Caller1"))))); +} + +TEST(CallHierarchy, IncomingQualified) { + Annotations Source(R"cpp( + namespace ns { + struct Waldo { + void find(); + }; + void Waldo::find() {} + void caller1(Waldo &W) { + W.$Caller1[[f^ind]](); + } + void caller2(Waldo &W) { + W.$Caller2[[find]](); + } + } + )cpp"); + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + auto Index = TU.index(); + + llvm::Optional> Items = prepareCallHierarchy( + AST, Source.point(), Index.get(), testPath(TU.Filename)); + ASSERT_TRUE(bool(Items)); + EXPECT_THAT(*Items, ElementsAre(WithName("ns::Waldo::find"))); + auto Incoming = incomingCalls((*Items)[0], Index.get()); + ASSERT_TRUE(bool(Incoming)); + EXPECT_THAT(*Incoming, + UnorderedElementsAre(AllOf(From(WithName("caller1")), + FromRanges(Source.range("Caller1"))), + AllOf(From(WithName("caller2")), + FromRanges(Source.range("Caller2"))))); +} + +TEST(CallHierarchy, IncomingMultiFile) { + // The test uses a .hh suffix for header files to get clang + // to parse them in C++ mode. .h files are parsed in C mode + // by default, which causes problems because e.g. symbol + // USRs are different in C mode (do not include function signatures). + + Annotations CalleeH(R"cpp( + void calle^e(int); + )cpp"); + Annotations CalleeC(R"cpp( + #include "callee.hh" + void calle^e(int) {} + )cpp"); + Annotations Caller1H(R"cpp( + void caller1(); + )cpp"); + Annotations Caller1C(R"cpp( + #include "callee.hh" + #include "caller1.hh" + void caller1() { + [[calle^e]](42); + } + )cpp"); + Annotations Caller2H(R"cpp( + void caller2(); + )cpp"); + Annotations Caller2C(R"cpp( + #include "caller1.hh" + #include "caller2.hh" + void caller2() { + $A[[caller1]](); + $B[[caller1]](); + } + )cpp"); + Annotations Caller3C(R"cpp( + #include "caller1.hh" + #include "caller2.hh" + void caller3() { + $Caller1[[caller1]](); + $Caller2[[caller2]](); + } + )cpp"); + + TestWorkspace Workspace; + Workspace.addSource("callee.hh", CalleeH.code()); + Workspace.addSource("caller1.hh", Caller1H.code()); + Workspace.addSource("caller2.hh", Caller2H.code()); + Workspace.addMainFile("callee.cc", CalleeC.code()); + Workspace.addMainFile("caller1.cc", Caller1C.code()); + Workspace.addMainFile("caller2.cc", Caller2C.code()); + Workspace.addMainFile("caller3.cc", Caller3C.code()); + + auto Index = Workspace.index(); + + auto CheckCallHierarchy = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { + llvm::Optional> Items = + prepareCallHierarchy(AST, Pos, Index.get(), TUPath); + ASSERT_TRUE(bool(Items)); + EXPECT_THAT(*Items, ElementsAre(WithName("callee"))); + auto IncomingLevel1 = incomingCalls((*Items)[0], Index.get()); + ASSERT_TRUE(bool(IncomingLevel1)); + EXPECT_THAT(*IncomingLevel1, + ElementsAre(AllOf(From(WithName("caller1")), + FromRanges(Caller1C.range())))); + + auto IncomingLevel2 = incomingCalls((*IncomingLevel1)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel2)); + EXPECT_THAT(*IncomingLevel2, + UnorderedElementsAre( + AllOf(From(WithName("caller2")), + FromRanges(Caller2C.range("A"), Caller2C.range("B"))), + AllOf(From(WithName("caller3")), + FromRanges(Caller3C.range("Caller1"))))); + + auto IncomingLevel3 = incomingCalls((*IncomingLevel2)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel3)); + EXPECT_THAT(*IncomingLevel3, + ElementsAre(AllOf(From(WithName("caller3")), + FromRanges(Caller3C.range("Caller2"))))); + + auto IncomingLevel4 = incomingCalls((*IncomingLevel3)[0].From, Index.get()); + ASSERT_TRUE(bool(IncomingLevel4)); + EXPECT_THAT(*IncomingLevel4, ElementsAre()); + }; + + // Check that invoking from a call site works. + auto AST = Workspace.openFile("caller1.cc"); + ASSERT_TRUE(bool(AST)); + CheckCallHierarchy(*AST, Caller1C.point(), testPath("caller1.cc")); + + // Check that invoking from the declaration site works. + AST = Workspace.openFile("callee.hh"); + ASSERT_TRUE(bool(AST)); + CheckCallHierarchy(*AST, CalleeH.point(), testPath("callee.hh")); + + // Check that invoking from the definition site works. + AST = Workspace.openFile("callee.cc"); + ASSERT_TRUE(bool(AST)); + CheckCallHierarchy(*AST, CalleeC.point(), testPath("callee.cc")); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/TestTU.cpp b/clang-tools-extra/clangd/unittests/TestTU.cpp --- a/clang-tools-extra/clangd/unittests/TestTU.cpp +++ b/clang-tools-extra/clangd/unittests/TestTU.cpp @@ -156,7 +156,8 @@ std::unique_ptr TestTU::index() const { auto AST = build(); - auto Idx = std::make_unique(/*UseDex=*/true); + auto Idx = std::make_unique(/*UseDex=*/true, + /*CollectMainFileRefs=*/true); Idx->updatePreamble(testPath(Filename), /*Version=*/"null", AST.getASTContext(), AST.getPreprocessorPtr(), AST.getCanonicalIncludes());