diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -131,6 +131,15 @@ Callback>); void onResolveTypeHierarchy(const ResolveTypeHierarchyItemParams &, Callback>); + void onPrepareCallHierarchy( + const CallHierarchyPrepareParams &, + Callback>>); + void onCallHierarchyIncomingCalls( + const CallHierarchyIncomingCallsParams &, + Callback>>); + void onCallHierarchyOutgoingCalls( + const CallHierarchyOutgoingCallsParams &, + Callback>>); void onChangeConfiguration(const DidChangeConfigurationParams &); void onSymbolInfo(const TextDocumentPositionParams &, Callback>); diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -617,6 +617,7 @@ ExecuteCommandParams::CLANGD_APPLY_TWEAK}}, }}, {"typeHierarchyProvider", true}, + {"callHierarchyProvider", true}, }}}}; if (Opts.Encoding) Result["offsetEncoding"] = *Opts.Encoding; @@ -1208,6 +1209,25 @@ std::move(Reply)); } +void ClangdLSPServer::onPrepareCallHierarchy( + const CallHierarchyPrepareParams &Params, + Callback>> Reply) { + Server->prepareCallHierarchy(Params.textDocument.uri.file(), Params.position, + std::move(Reply)); +} + +void ClangdLSPServer::onCallHierarchyIncomingCalls( + const CallHierarchyIncomingCallsParams &Params, + Callback>> Reply) { + Server->incomingCalls(Params.Item, std::move(Reply)); +} + +void ClangdLSPServer::onCallHierarchyOutgoingCalls( + const CallHierarchyOutgoingCallsParams &Params, + Callback>> Reply) { + Server->outgoingCalls(Params.Item, std::move(Reply)); +} + void ClangdLSPServer::applyConfiguration( const ConfigurationSettings &Settings) { // Per-file update to the compilation database. @@ -1421,6 +1441,9 @@ MsgHandler->bind("textDocument/symbolInfo", &ClangdLSPServer::onSymbolInfo); MsgHandler->bind("textDocument/typeHierarchy", &ClangdLSPServer::onTypeHierarchy); MsgHandler->bind("typeHierarchy/resolve", &ClangdLSPServer::onResolveTypeHierarchy); + MsgHandler->bind("textDocument/prepareCallHierarchy", &ClangdLSPServer::onPrepareCallHierarchy); + MsgHandler->bind("callHierarchy/incomingCalls", &ClangdLSPServer::onCallHierarchyIncomingCalls); + MsgHandler->bind("callHierarchy/outgoingCalls", &ClangdLSPServer::onCallHierarchyOutgoingCalls); MsgHandler->bind("textDocument/selectionRange", &ClangdLSPServer::onSelectionRange); MsgHandler->bind("textDocument/documentLink", &ClangdLSPServer::onDocumentLink); MsgHandler->bind("textDocument/semanticTokens/full", &ClangdLSPServer::onSemanticTokens); diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -242,6 +242,21 @@ TypeHierarchyDirection Direction, Callback> CB); + /// Get information about call hierarchy for a given position. + void prepareCallHierarchy( + PathRef File, Position Pos, + Callback>> CB); + + /// Resolve incoming calls for a given call hierarchy item. + void incomingCalls( + const CallHierarchyItem &Item, + Callback>>); + + /// Resolve outgoing calls for a given call hierarchy item. + void outgoingCalls( + const CallHierarchyItem &Item, + Callback>>); + /// Retrieve the top symbols from the workspace matching a query. void workspaceSymbols(StringRef Query, int Limit, Callback> CB); 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 @@ -677,6 +677,30 @@ CB(Item); } +void ClangdServer::prepareCallHierarchy( + PathRef File, Position Pos, + Callback>> CB) { + auto Action = [File = File.str(), Pos, CB = std::move(CB), + this](Expected InpAST) mutable { + if (!InpAST) + return CB(InpAST.takeError()); + CB(clangd::prepareCallHierarchy(InpAST->AST, Pos, Index, File)); + }; + WorkScheduler.runWithAST("Call Hierarchy", File, std::move(Action)); +} + +void ClangdServer::incomingCalls( + const CallHierarchyItem &Item, + Callback>> CB) { + CB(clangd::incomingCalls(Item, Index)); +} + +void ClangdServer::outgoingCalls( + const CallHierarchyItem &Item, + Callback>> CB) { + CB(clangd::outgoingCalls(Item, Index)); +} + void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) { // FIXME: Do nothing for now. This will be used for indexing and potentially // invalidating other caches. diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -1363,7 +1363,7 @@ /// descendants. If not defined, the children have not been resolved. llvm::Optional> children; - /// An optional 'data' filed, which can be used to identify a type hierarchy + /// An optional 'data' field, which can be used to identify a type hierarchy /// item in a resolve request. llvm::Optional data; }; @@ -1385,6 +1385,83 @@ bool fromJSON(const llvm::json::Value &, ResolveTypeHierarchyItemParams &, llvm::json::Path); +enum class SymbolTag { Deprecated = 1 }; +llvm::json::Value toJSON(SymbolTag); + +/// Represents programming constructs like functions or constructors +/// in the context of call hierarchy. +struct CallHierarchyItem { + /// The name of this item. + std::string Name; + + /// The kind of this item. + SymbolKind Kind; + + /// Tags for this item. + std::vector Tags; + + /// More detaill for this item, e.g. the signature of a function. + std::string Detail; + + /// The resource identifier of this item. + URIForFile Uri; + + /// The range enclosing this symbol not including leading / trailing + /// whitespace but everything else, e.g. comments and code. + Range Rng; + + /// The range that should be selected and revealed when this symbol + /// is being picked, e.g. the name of a function. + /// Must be contained by `Rng`. + Range SelectionRange; + + /// An optional 'data' field, which can be used to identify a call + /// hierarchy item in an incomingCalls or outgoingCalls request. + llvm::Optional Data; +}; +llvm::json::Value toJSON(const CallHierarchyItem &); +bool fromJSON(const llvm::json::Value &, CallHierarchyItem &, llvm::json::Path); + +/// Represents an incoming call, e.g. a caller of a method or constructor. +struct CallHierarchyIncomingCall { + /// The item that makes the call. + CallHierarchyItem From; + + /// The range at which the calls appear. + /// This is relative to the caller denoted by `From`. + std::vector FromRanges; +}; +llvm::json::Value toJSON(const CallHierarchyIncomingCall &); + +/// Represents an outgoing call, e.g. calling a getter from a method or +/// a method from a constructor etc. +struct CallHierarchyOutgoingCall { + /// The item that is called. + CallHierarchyItem To; + + /// The range at which this item is called. + /// This is the range relative to the caller, and not `To`. + std::vector FromRanges; +}; +llvm::json::Value toJSON(const CallHierarchyOutgoingCall &); + +/// The parameter of a `textDocument/prepareCallHierarchy` request. +struct CallHierarchyPrepareParams : public TextDocumentPositionParams {}; + +/// The parameter of a `callHierarchy/incomingCalls` request. +struct CallHierarchyIncomingCallsParams { + CallHierarchyItem Item; +}; +bool fromJSON(const llvm::json::Value &, CallHierarchyIncomingCallsParams &, + llvm::json::Path); + +/// The parameter of a `callHierarchy/outgoingCalls` request. +struct CallHierarchyOutgoingCallsParams { + CallHierarchyItem Item; +}; +bool fromJSON(const llvm::json::Value &, CallHierarchyOutgoingCallsParams &, + llvm::json::Path); + struct ReferenceParams : public TextDocumentPositionParams { // For now, no options like context.includeDeclaration are supported. }; diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -1203,6 +1203,54 @@ return fromJSON(Params, Base, P); } +llvm::json::Value toJSON(SymbolTag Tag) { + return llvm::json::Value{static_cast(Tag)}; +} + +llvm::json::Value toJSON(const CallHierarchyItem &I) { + llvm::json::Object Result{ + {"name", I.Name}, {"kind", static_cast(I.Kind)}, + {"tags", I.Tags}, {"detail", I.Detail}, + {"range", I.Rng}, {"selectionRange", I.SelectionRange}, + {"uri", I.Uri}}; + if (I.Data) + Result["data"] = I.Data; + return std::move(Result); +} + +bool fromJSON(const llvm::json::Value &Params, CallHierarchyItem &I, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + + // Populate the required fields only. We don't care about the + // optional fields `Tags` and `Detail` for the purpose of + // client --> server communication. + return O && O.map("name", I.Name) && O.map("kind", I.Kind) && + O.map("uri", I.Uri) && O.map("range", I.Rng) && + O.map("selectionRange", I.SelectionRange) && + O.mapOptional("data", I.Data); +} + +llvm::json::Value toJSON(const CallHierarchyIncomingCall &C) { + return llvm::json::Object{{"from", C.From}, {"fromRanges", C.FromRanges}}; +} + +llvm::json::Value toJSON(const CallHierarchyOutgoingCall &C) { + return llvm::json::Object{{"from", C.To}, {"fromRanges", C.FromRanges}}; +} + +bool fromJSON(const llvm::json::Value &Params, + CallHierarchyIncomingCallsParams &C, llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O.map("item", C.Item); +} + +bool fromJSON(const llvm::json::Value &Params, + CallHierarchyOutgoingCallsParams &C, llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + return O.map("item", C.Item); +} + static const char *toString(OffsetEncoding OE) { switch (OE) { case OffsetEncoding::UTF8: 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" @@ -1314,9 +1315,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()); @@ -1347,7 +1347,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); @@ -1541,6 +1541,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 (they're all "methods"). + 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/test/initialize-params.test b/clang-tools-extra/clangd/test/initialize-params.test --- a/clang-tools-extra/clangd/test/initialize-params.test +++ b/clang-tools-extra/clangd/test/initialize-params.test @@ -5,6 +5,7 @@ # CHECK-NEXT: "jsonrpc": "2.0", # CHECK-NEXT: "result": { # CHECK-NEXT: "capabilities": { +# CHECK-NEXT: "callHierarchyProvider": true, # CHECK-NEXT: "codeActionProvider": true, # CHECK-NEXT: "completionProvider": { # CHECK-NEXT: "allCommitCharacters": [ 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()); diff --git a/llvm/utils/gn/secondary/clang-tools-extra/clangd/unittests/BUILD.gn b/llvm/utils/gn/secondary/clang-tools-extra/clangd/unittests/BUILD.gn --- a/llvm/utils/gn/secondary/clang-tools-extra/clangd/unittests/BUILD.gn +++ b/llvm/utils/gn/secondary/clang-tools-extra/clangd/unittests/BUILD.gn @@ -44,6 +44,7 @@ "ASTTests.cpp", "Annotations.cpp", "BackgroundIndexTests.cpp", + "CallHierarchyTests.cpp", "CanonicalIncludesTests.cpp", "ClangdLSPServerTests.cpp", "ClangdTests.cpp",