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 @@ -153,6 +153,9 @@ void onCallHierarchyIncomingCalls( const CallHierarchyIncomingCallsParams &, Callback>); + void onCallHierarchyOutgoingCalls( + const CallHierarchyOutgoingCallsParams &, + Callback>); void onClangdInlayHints(const InlayHintsParams &, Callback); void onInlayHint(const InlayHintsParams &, 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 @@ -1373,6 +1373,12 @@ 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. @@ -1654,6 +1660,7 @@ Bind.method("typeHierarchy/subtypes", this, &ClangdLSPServer::onSubTypes); Bind.method("textDocument/prepareCallHierarchy", this, &ClangdLSPServer::onPrepareCallHierarchy); Bind.method("callHierarchy/incomingCalls", this, &ClangdLSPServer::onCallHierarchyIncomingCalls); + Bind.method("callHierarchy/outgoingCalls", this, &ClangdLSPServer::onCallHierarchyOutgoingCalls); Bind.method("textDocument/selectionRange", this, &ClangdLSPServer::onSelectionRange); Bind.method("textDocument/documentLink", this, &ClangdLSPServer::onDocumentLink); Bind.method("textDocument/semanticTokens/full", this, &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 @@ -288,6 +288,10 @@ void incomingCalls(const CallHierarchyItem &Item, Callback>); + /// Resolve outgoing calls for a given call hierarchy item. + void outgoingCalls(const CallHierarchyItem &Item, + Callback>); + /// Resolve inlay hints for a given document. void inlayHints(PathRef File, std::optional RestrictRange, Callback>); 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 @@ -880,6 +880,15 @@ WorkScheduler->runWithAST("InlayHints", File, std::move(Action), Transient); } +void ClangdServer::outgoingCalls( + const CallHierarchyItem &Item, + Callback> CB) { + WorkScheduler->run("Outgoing Calls", "", + [CB = std::move(CB), Item, this]() mutable { + 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/XRefs.h b/clang-tools-extra/clangd/XRefs.h --- a/clang-tools-extra/clangd/XRefs.h +++ b/clang-tools-extra/clangd/XRefs.h @@ -150,6 +150,9 @@ std::vector incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index); +std::vector +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 @@ -1700,6 +1700,7 @@ HierarchyItem HI; HI.name = printName(Ctx, ND); + // FIXME: Populate HI.detail the way we do in symbolToHierarchyItem? HI.kind = SK; HI.range = Range{sourceLocToPosition(SM, DeclRange->getBegin()), sourceLocToPosition(SM, DeclRange->getEnd())}; @@ -1751,6 +1752,7 @@ } HierarchyItem HI; HI.name = std::string(S.Name); + HI.detail = (S.Scope + S.Name).str(); HI.kind = indexSymbolKindToSymbolKind(S.SymInfo.Kind); HI.selectionRange = Loc->range; // FIXME: Populate 'range' correctly @@ -2303,6 +2305,63 @@ return Results; } +std::vector +outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) { + std::vector Results; + if (!Index || Item.data.empty()) + return Results; + auto ID = SymbolID::fromStr(Item.data); + if (!ID) { + elog("outgoingCalls failed to find symbol: {0}", ID.takeError()); + return Results; + } + // In this function, we find outgoing calls based on the index only. + ContainedRefsRequest Request; + Request.ID = *ID; + // Initially store the ranges in a map keyed by SymbolID of the callee. + // This allows us to group different calls to the same function + // into the same CallHierarchyOutgoingCall. + llvm::DenseMap> CallsOut; + // We can populate the ranges based on a refs request only. As we do so, we + // also accumulate the callee IDs into a lookup request. + LookupRequest CallsOutLookup; + Index->containedRefs(Request, [&](const auto &R) { + auto Loc = indexToLSPLocation(R.Location, Item.uri.file()); + if (!Loc) { + elog("outgoingCalls failed to convert location: {0}", Loc.takeError()); + return; + } + auto It = CallsOut.try_emplace(R.Symbol, std::vector{}).first; + It->second.push_back(Loc->range); + + CallsOutLookup.IDs.insert(R.Symbol); + }); + // Perform the lookup request and combine its results with CallsOut to + // get complete CallHierarchyOutgoingCall objects. + Index->lookup(CallsOutLookup, [&](const Symbol &Callee) { + // Filter references to only keep function calls + using SK = index::SymbolKind; + auto Kind = Callee.SymInfo.Kind; + bool NotCall = (Kind != SK::Function && Kind != SK::InstanceMethod && + Kind != SK::ClassMethod && Kind != SK::StaticMethod && + Kind != SK::Constructor && Kind != SK::Destructor && + Kind != SK::ConversionFunction); + assert(!NotCall); + + auto It = CallsOut.find(Callee.ID); + assert(It != CallsOut.end()); + if (auto CHI = symbolToCallHierarchyItem(Callee, Item.uri.file())) + Results.push_back( + CallHierarchyOutgoingCall{std::move(*CHI), std::move(It->second)}); + }); + // Sort results by name of the callee. + llvm::sort(Results, [](const CallHierarchyOutgoingCall &A, + const CallHierarchyOutgoingCall &B) { + return A.to.name < B.to.name; + }); + return Results; +} + llvm::DenseSet getNonLocalDeclRefs(ParsedAST &AST, const FunctionDecl *FD) { if (!FD->hasBody()) diff --git a/clang-tools-extra/clangd/index/Index.h b/clang-tools-extra/clangd/index/Index.h --- a/clang-tools-extra/clangd/index/Index.h +++ b/clang-tools-extra/clangd/index/Index.h @@ -77,6 +77,19 @@ bool WantContainer = false; }; +struct ContainedRefsRequest { + /// Note that RefKind::Call just restricts the matched SymbolKind to + /// functions, not the form of the reference (e.g. address-of-function, + /// which can indicate an indirect call, should still be caught). + static const RefKind SupportedRefKinds = RefKind::Call; + + SymbolID ID; + /// If set, limit the number of refers returned from the index. The index may + /// choose to return less than this, e.g. it tries to avoid returning stale + /// results. + std::optional Limit; +}; + struct RelationsRequest { llvm::DenseSet Subjects; RelationKind Predicate; @@ -84,6 +97,14 @@ std::optional Limit; }; +struct ContainedRefsResult { + /// The source location where the symbol is named. + SymbolLocation Location; + RefKind Kind = RefKind::Unknown; + /// The ID of the symbol which is referred to + SymbolID Symbol; +}; + /// Describes what data is covered by an index. /// /// Indexes may contain symbols but not references from a file, etc. @@ -141,6 +162,17 @@ virtual bool refs(const RefsRequest &Req, llvm::function_ref Callback) const = 0; + /// Find all symbols that are referenced by a symbol and apply + /// \p Callback on each result. + /// + /// Results should be returned in arbitrary order. + /// The returned result must be deep-copied if it's used outside Callback. + /// + /// Returns true if there will be more results (limited by Req.Limit); + virtual bool containedRefs( + const ContainedRefsRequest &Req, + llvm::function_ref Callback) const = 0; + /// Finds all relations (S, P, O) stored in the index such that S is among /// Req.Subjects and P is Req.Predicate, and invokes \p Callback for (S, O) in /// each. @@ -175,6 +207,9 @@ llvm::function_ref) const override; bool refs(const RefsRequest &, llvm::function_ref) const override; + bool containedRefs( + const ContainedRefsRequest &, + llvm::function_ref) const override; void relations(const RelationsRequest &, llvm::function_ref) const override; diff --git a/clang-tools-extra/clangd/index/Index.cpp b/clang-tools-extra/clangd/index/Index.cpp --- a/clang-tools-extra/clangd/index/Index.cpp +++ b/clang-tools-extra/clangd/index/Index.cpp @@ -66,6 +66,11 @@ llvm::function_ref CB) const { return snapshot()->refs(R, CB); } +bool SwapIndex::containedRefs( + const ContainedRefsRequest &R, + llvm::function_ref CB) const { + return snapshot()->containedRefs(R, CB); +} void SwapIndex::relations( const RelationsRequest &R, llvm::function_ref CB) const { diff --git a/clang-tools-extra/clangd/index/MemIndex.h b/clang-tools-extra/clangd/index/MemIndex.h --- a/clang-tools-extra/clangd/index/MemIndex.h +++ b/clang-tools-extra/clangd/index/MemIndex.h @@ -72,6 +72,10 @@ bool refs(const RefsRequest &Req, llvm::function_ref Callback) const override; + bool containedRefs(const ContainedRefsRequest &Req, + llvm::function_ref + Callback) const override; + void relations(const RelationsRequest &Req, llvm::function_ref Callback) const override; diff --git a/clang-tools-extra/clangd/index/MemIndex.cpp b/clang-tools-extra/clangd/index/MemIndex.cpp --- a/clang-tools-extra/clangd/index/MemIndex.cpp +++ b/clang-tools-extra/clangd/index/MemIndex.cpp @@ -9,6 +9,7 @@ #include "MemIndex.h" #include "FuzzyMatch.h" #include "Quality.h" +#include "index/Index.h" #include "support/Trace.h" namespace clang { @@ -85,6 +86,25 @@ return false; // We reported all refs. } +bool MemIndex::containedRefs( + const ContainedRefsRequest &Req, + llvm::function_ref Callback) const { + trace::Span Tracer("MemIndex refersTo"); + uint32_t Remaining = Req.Limit.value_or(std::numeric_limits::max()); + for (const auto &Pair : Refs) { + for (const auto &R : Pair.second) { + if (!static_cast(ContainedRefsRequest::SupportedRefKinds & R.Kind) || + Req.ID != R.Container) + continue; + if (Remaining == 0) + return true; // More refs were available. + --Remaining; + Callback({R.Location, R.Kind, Pair.first}); + } + } + return false; // We reported all refs. +} + void MemIndex::relations( const RelationsRequest &Req, llvm::function_ref Callback) const { diff --git a/clang-tools-extra/clangd/index/Merge.h b/clang-tools-extra/clangd/index/Merge.h --- a/clang-tools-extra/clangd/index/Merge.h +++ b/clang-tools-extra/clangd/index/Merge.h @@ -38,6 +38,9 @@ llvm::function_ref) const override; bool refs(const RefsRequest &, llvm::function_ref) const override; + bool containedRefs( + const ContainedRefsRequest &, + llvm::function_ref) const override; void relations(const RelationsRequest &, llvm::function_ref) const override; diff --git a/clang-tools-extra/clangd/index/Merge.cpp b/clang-tools-extra/clangd/index/Merge.cpp --- a/clang-tools-extra/clangd/index/Merge.cpp +++ b/clang-tools-extra/clangd/index/Merge.cpp @@ -155,6 +155,40 @@ return More || StaticHadMore; } +bool MergedIndex::containedRefs( + const ContainedRefsRequest &Req, + llvm::function_ref Callback) const { + trace::Span Tracer("MergedIndex refersTo"); + bool More = false; + uint32_t Remaining = Req.Limit.value_or(std::numeric_limits::max()); + // We don't want duplicated refs from the static/dynamic indexes, + // and we can't reliably deduplicate them because offsets may differ slightly. + // We consider the dynamic index authoritative and report all its refs, + // and only report static index refs from other files. + More |= Dynamic->containedRefs(Req, [&](const auto &O) { + Callback(O); + assert(Remaining != 0); + --Remaining; + }); + if (Remaining == 0 && More) + return More; + auto DynamicContainsFile = Dynamic->indexedFiles(); + // We return less than Req.Limit if static index returns more refs for dirty + // files. + bool StaticHadMore = Static->containedRefs(Req, [&](const auto &O) { + if ((DynamicContainsFile(O.Location.FileURI) & IndexContents::References) != + IndexContents::None) + return; // ignore refs that have been seen from dynamic index. + if (Remaining == 0) { + More = true; + return; + } + --Remaining; + Callback(O); + }); + return More || StaticHadMore; +} + llvm::unique_function MergedIndex::indexedFiles() const { return [DynamicContainsFile{Dynamic->indexedFiles()}, diff --git a/clang-tools-extra/clangd/index/ProjectAware.cpp b/clang-tools-extra/clangd/index/ProjectAware.cpp --- a/clang-tools-extra/clangd/index/ProjectAware.cpp +++ b/clang-tools-extra/clangd/index/ProjectAware.cpp @@ -35,6 +35,10 @@ /// Query all indexes while prioritizing the associated one (if any). bool refs(const RefsRequest &Req, llvm::function_ref Callback) const override; + /// Query all indexes while prioritizing the associated one (if any). + bool containedRefs(const ContainedRefsRequest &Req, + llvm::function_ref + Callback) const override; /// Queries only the associates index when Req.RestrictForCodeCompletion is /// set, otherwise queries all. @@ -94,6 +98,15 @@ return false; } +bool ProjectAwareIndex::containedRefs( + const ContainedRefsRequest &Req, + llvm::function_ref Callback) const { + trace::Span Tracer("ProjectAwareIndex::refersTo"); + if (auto *Idx = getIndex()) + return Idx->containedRefs(Req, Callback); + return false; +} + bool ProjectAwareIndex::fuzzyFind( const FuzzyFindRequest &Req, llvm::function_ref Callback) const { diff --git a/clang-tools-extra/clangd/index/Ref.h b/clang-tools-extra/clangd/index/Ref.h --- a/clang-tools-extra/clangd/index/Ref.h +++ b/clang-tools-extra/clangd/index/Ref.h @@ -63,6 +63,9 @@ // ^ this references Foo, but does not explicitly spell out its name // }; Spelled = 1 << 3, + // A reference which is a call. Used as a filter for which references + // to store in data structures used for computing outgoing calls. + Call = 1 << 4, All = Declaration | Definition | Reference | Spelled, }; diff --git a/clang-tools-extra/clangd/index/SymbolCollector.h b/clang-tools-extra/clangd/index/SymbolCollector.h --- a/clang-tools-extra/clangd/index/SymbolCollector.h +++ b/clang-tools-extra/clangd/index/SymbolCollector.h @@ -201,6 +201,7 @@ SourceLocation Loc; FileID FID; index::SymbolRoleSet Roles; + index::SymbolKind Kind; const Decl *Container; bool Spelled; }; diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp --- a/clang-tools-extra/clangd/index/SymbolCollector.cpp +++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp @@ -18,6 +18,7 @@ #include "clang-include-cleaner/Record.h" #include "clang-include-cleaner/Types.h" #include "index/CanonicalIncludes.h" +#include "index/Ref.h" #include "index/Relation.h" #include "index/Symbol.h" #include "index/SymbolID.h" @@ -612,7 +613,7 @@ auto FileLoc = SM.getFileLoc(Loc); auto FID = SM.getFileID(FileLoc); if (Opts.RefsInHeaders || FID == SM.getMainFileID()) { - addRef(ID, SymbolRef{FileLoc, FID, Roles, + addRef(ID, SymbolRef{FileLoc, FID, Roles, index::getSymbolInfo(ND).Kind, getRefContainer(ASTNode.Parent, Opts), isSpelled(FileLoc, *ND)}); } @@ -722,8 +723,10 @@ // FIXME: Populate container information for macro references. // FIXME: All MacroRefs are marked as Spelled now, but this should be // checked. - addRef(ID, SymbolRef{Loc, SM.getFileID(Loc), Roles, /*Container=*/nullptr, - /*Spelled=*/true}); + addRef(ID, + SymbolRef{Loc, SM.getFileID(Loc), Roles, index::SymbolKind::Macro, + /*Container=*/nullptr, + /*Spelled=*/true}); } // Collect symbols. @@ -1077,6 +1080,14 @@ return I.first->second; } +static bool refIsCall(index::SymbolKind Kind) { + using SK = index::SymbolKind; + return Kind == SK::Function || Kind == SK::InstanceMethod || + Kind == SK::ClassMethod || Kind == SK::StaticMethod || + Kind == SK::Constructor || Kind == SK::Destructor || + Kind == SK::ConversionFunction; +} + void SymbolCollector::addRef(SymbolID ID, const SymbolRef &SR) { const auto &SM = ASTCtx->getSourceManager(); // FIXME: use the result to filter out references. @@ -1088,6 +1099,9 @@ R.Location.End = Range.second; R.Location.FileURI = HeaderFileURIs->toURI(*FE).c_str(); R.Kind = toRefKind(SR.Roles, SR.Spelled); + if (refIsCall(SR.Kind)) { + R.Kind |= RefKind::Call; + } R.Container = getSymbolIDCached(SR.Container); Refs.insert(ID, R); } diff --git a/clang-tools-extra/clangd/index/dex/Dex.h b/clang-tools-extra/clangd/index/dex/Dex.h --- a/clang-tools-extra/clangd/index/dex/Dex.h +++ b/clang-tools-extra/clangd/index/dex/Dex.h @@ -85,6 +85,10 @@ bool refs(const RefsRequest &Req, llvm::function_ref Callback) const override; + bool containedRefs(const ContainedRefsRequest &Req, + llvm::function_ref + Callback) const override; + void relations(const RelationsRequest &Req, llvm::function_ref Callback) const override; @@ -95,7 +99,22 @@ size_t estimateMemoryUsage() const override; private: + class RevRef { + const Ref *Reference; + SymbolID Target; + + public: + RevRef(const Ref &Reference, SymbolID Target) + : Reference(&Reference), Target(Target) {} + const Ref &ref() const { return *Reference; } + ContainedRefsResult containedRefsResult() const { + return {ref().Location, ref().Kind, Target}; + } + }; + void buildIndex(); + llvm::iterator_range::const_iterator> + lookupRevRefs(const SymbolID &Container) const; std::unique_ptr iterator(const Token &Tok) const; std::unique_ptr createFileProximityIterator(llvm::ArrayRef ProximityPaths) const; @@ -116,6 +135,7 @@ llvm::DenseMap InvertedIndex; dex::Corpus Corpus; llvm::DenseMap> Refs; + std::vector RevRefs; // sorted by container ID static_assert(sizeof(RelationKind) == sizeof(uint8_t), "RelationKind should be of same size as a uint8_t"); llvm::DenseMap, std::vector> Relations; diff --git a/clang-tools-extra/clangd/index/dex/Dex.cpp b/clang-tools-extra/clangd/index/dex/Dex.cpp --- a/clang-tools-extra/clangd/index/dex/Dex.cpp +++ b/clang-tools-extra/clangd/index/dex/Dex.cpp @@ -147,6 +147,17 @@ for (DocID SymbolRank = 0; SymbolRank < Symbols.size(); ++SymbolRank) Builder.add(*Symbols[SymbolRank], SymbolRank); InvertedIndex = std::move(Builder).build(); + + // Build RevRefs + for (const auto &[ID, RefList] : Refs) + for (const auto &R : RefList) + if ((R.Kind & ContainedRefsRequest::SupportedRefKinds) != + RefKind::Unknown) + RevRefs.emplace_back(R, ID); + // Sort by container ID so we can use binary search for lookup. + llvm::sort(RevRefs, [](const RevRef &A, const RevRef &B) { + return A.ref().Container < B.ref().Container; + }); } std::unique_ptr Dex::iterator(const Token &Tok) const { @@ -314,6 +325,36 @@ return false; // We reported all refs. } +llvm::iterator_range::const_iterator> +Dex::lookupRevRefs(const SymbolID &Container) const { + // equal_range() requires an element of the same type as the elements of the + // range, so construct a dummy RevRef with the container of interest. + Ref QueryRef; + QueryRef.Container = Container; + RevRef Query(QueryRef, SymbolID{}); + + auto ItPair = std::equal_range(RevRefs.cbegin(), RevRefs.cend(), Query, + [](const RevRef &A, const RevRef &B) { + return A.ref().Container < B.ref().Container; + }); + return {ItPair.first, ItPair.second}; +} + +bool Dex::containedRefs( + const ContainedRefsRequest &Req, + llvm::function_ref Callback) const { + trace::Span Tracer("Dex reversed refs"); + uint32_t Remaining = Req.Limit.value_or(std::numeric_limits::max()); + for (const auto &Rev : lookupRevRefs(Req.ID)) { + // RevRefs are already filtered to ContainedRefsRequest::SupportedRefKinds + if (Remaining == 0) + return true; // More refs were available. + --Remaining; + Callback(Rev.containedRefsResult()); + } + return false; // We reported all refs. +} + void Dex::relations( const RelationsRequest &Req, llvm::function_ref Callback) const { @@ -350,6 +391,7 @@ for (const auto &TokenToPostingList : InvertedIndex) Bytes += TokenToPostingList.second.bytes(); Bytes += Refs.getMemorySize(); + Bytes += RevRefs.size() * sizeof(RevRef); Bytes += Relations.getMemorySize(); return Bytes + BackingDataSize; } diff --git a/clang-tools-extra/clangd/test/type-hierarchy-ext.test b/clang-tools-extra/clangd/test/type-hierarchy-ext.test --- a/clang-tools-extra/clangd/test/type-hierarchy-ext.test +++ b/clang-tools-extra/clangd/test/type-hierarchy-ext.test @@ -12,6 +12,7 @@ # CHECK-NEXT: "data": { # CHECK-NEXT: "symbolID": "A6576FE083F2949A" # CHECK-NEXT: }, +# CHECK-NEXT: "detail": "Child3", # CHECK-NEXT: "kind": 23, # CHECK-NEXT: "name": "Child3", # CHECK-NEXT: "range": { @@ -153,6 +154,7 @@ # CHECK-NEXT: "data": { # CHECK-NEXT: "symbolID": "5705B382DFC77CBC" # CHECK-NEXT: }, +# CHECK-NEXT: "detail": "Child4", # CHECK-NEXT: "kind": 23, # CHECK-NEXT: "name": "Child4", # CHECK-NEXT: "range": { diff --git a/clang-tools-extra/clangd/test/type-hierarchy.test b/clang-tools-extra/clangd/test/type-hierarchy.test --- a/clang-tools-extra/clangd/test/type-hierarchy.test +++ b/clang-tools-extra/clangd/test/type-hierarchy.test @@ -62,6 +62,7 @@ # CHECK-NEXT: ], # CHECK-NEXT: "symbolID": "ECDC0C46D75120F4" # CHECK-NEXT: }, +# CHECK-NEXT: "detail": "Child1", # CHECK-NEXT: "kind": 23, # CHECK-NEXT: "name": "Child1", # CHECK-NEXT: "range": { @@ -88,56 +89,6 @@ # CHECK-NEXT: } # CHECK-NEXT: ] --- -{"jsonrpc":"2.0","id":2,"method":"typeHierarchy/subtypes","params":{"item":{"uri":"test:///main.cpp","data":{"parents":[{"parents":[{"parents":[],"symbolID":"FE546E7B648D69A7"}],"symbolID":"ECDC0C46D75120F4"}],"symbolID":"8A991335E4E67D08"},"name":"Child2","kind":23,"range":{"end":{"character":13,"line":3},"start":{"character":7,"line":3}},"selectionRange":{"end":{"character":13,"line":3},"start":{"character":7,"line":3}}}}} -# CHECK: "id": 2 -# CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "data": { -# CHECK-NEXT: "parents": [ -# CHECK-NEXT: { -# CHECK-NEXT: "parents": [ -# CHECK-NEXT: { -# CHECK-NEXT: "parents": [ -# CHECK-NEXT: { -# CHECK-NEXT: "parents": [], -# CHECK-NEXT: "symbolID": "FE546E7B648D69A7" -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "symbolID": "ECDC0C46D75120F4" -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "symbolID": "8A991335E4E67D08" -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "symbolID": "A6576FE083F2949A" -# CHECK-NEXT: }, -# CHECK-NEXT: "kind": 23, -# CHECK-NEXT: "name": "Child3", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 13, -# CHECK-NEXT: "line": 3 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 7, -# CHECK-NEXT: "line": 3 -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: "selectionRange": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 13, -# CHECK-NEXT: "line": 3 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 7, -# CHECK-NEXT: "line": 3 -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: "uri": "file://{{.*}}/clangd-test/main.cpp" -# CHECK-NEXT: } -# CHECK-NEXT: ] ---- {"jsonrpc":"2.0","id":3,"method":"shutdown"} --- {"jsonrpc":"2.0","method":"exit"} diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp --- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp +++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp @@ -44,17 +44,27 @@ // Helpers for matching call hierarchy data structures. MATCHER_P(withName, N, "") { return arg.name == N; } +MATCHER_P(withDetail, N, "") { return arg.detail == N; } MATCHER_P(withSelectionRange, R, "") { return arg.selectionRange == R; } template ::testing::Matcher from(ItemMatcher M) { return Field(&CallHierarchyIncomingCall::from, M); } +template +::testing::Matcher to(ItemMatcher M) { + return Field(&CallHierarchyOutgoingCall::to, M); +} template -::testing::Matcher fromRanges(RangeMatchers... M) { +::testing::Matcher iFromRanges(RangeMatchers... M) { return Field(&CallHierarchyIncomingCall::fromRanges, UnorderedElementsAre(M...)); } +template +::testing::Matcher oFromRanges(RangeMatchers... M) { + return Field(&CallHierarchyOutgoingCall::fromRanges, + UnorderedElementsAre(M...)); +} TEST(CallHierarchy, IncomingOneFileCpp) { Annotations Source(R"cpp( @@ -79,21 +89,24 @@ prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename)); ASSERT_THAT(Items, ElementsAre(withName("callee"))); auto IncomingLevel1 = incomingCalls(Items[0], Index.get()); - ASSERT_THAT(IncomingLevel1, - ElementsAre(AllOf(from(withName("caller1")), - fromRanges(Source.range("Callee"))))); + ASSERT_THAT( + IncomingLevel1, + ElementsAre(AllOf(from(AllOf(withName("caller1"), withDetail("caller1"))), + iFromRanges(Source.range("Callee"))))); auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get()); - ASSERT_THAT(IncomingLevel2, - ElementsAre(AllOf(from(withName("caller2")), - fromRanges(Source.range("Caller1A"), - Source.range("Caller1B"))), - AllOf(from(withName("caller3")), - fromRanges(Source.range("Caller1C"))))); + ASSERT_THAT( + IncomingLevel2, + ElementsAre(AllOf(from(AllOf(withName("caller2"), withDetail("caller2"))), + iFromRanges(Source.range("Caller1A"), + Source.range("Caller1B"))), + AllOf(from(AllOf(withName("caller3"), withDetail("caller3"))), + iFromRanges(Source.range("Caller1C"))))); auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); - ASSERT_THAT(IncomingLevel3, - ElementsAre(AllOf(from(withName("caller3")), - fromRanges(Source.range("Caller2"))))); + ASSERT_THAT( + IncomingLevel3, + ElementsAre(AllOf(from(AllOf(withName("caller3"), withDetail("caller3"))), + iFromRanges(Source.range("Caller2"))))); auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); EXPECT_THAT(IncomingLevel4, IsEmpty()); @@ -125,20 +138,24 @@ ASSERT_THAT(Items, ElementsAre(withName("callee"))); auto IncomingLevel1 = incomingCalls(Items[0], Index.get()); ASSERT_THAT(IncomingLevel1, - ElementsAre(AllOf(from(withName("caller1")), - fromRanges(Source.range("Callee"))))); + ElementsAre(AllOf(from(AllOf(withName("caller1"), + withDetail("MyClass::caller1"))), + iFromRanges(Source.range("Callee"))))); auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get()); ASSERT_THAT(IncomingLevel2, - ElementsAre(AllOf(from(withName("caller2")), - fromRanges(Source.range("Caller1A"), - Source.range("Caller1B"))), - AllOf(from(withName("caller3")), - fromRanges(Source.range("Caller1C"))))); + ElementsAre(AllOf(from(AllOf(withName("caller2"), + withDetail("MyClass::caller2"))), + iFromRanges(Source.range("Caller1A"), + Source.range("Caller1B"))), + AllOf(from(AllOf(withName("caller3"), + withDetail("MyClass::caller3"))), + iFromRanges(Source.range("Caller1C"))))); auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); ASSERT_THAT(IncomingLevel3, - ElementsAre(AllOf(from(withName("caller3")), - fromRanges(Source.range("Caller2"))))); + ElementsAre(AllOf(from(AllOf(withName("caller3"), + withDetail("MyClass::caller3"))), + iFromRanges(Source.range("Caller2"))))); auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); EXPECT_THAT(IncomingLevel4, IsEmpty()); @@ -167,14 +184,16 @@ prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename)); ASSERT_THAT(Items, ElementsAre(withName("callee"))); auto IncomingLevel1 = incomingCalls(Items[0], Index.get()); - ASSERT_THAT(IncomingLevel1, - ElementsAre(AllOf(from(withName("caller1")), - fromRanges(Source.range("Callee"))))); + ASSERT_THAT( + IncomingLevel1, + ElementsAre(AllOf(from(AllOf(withName("caller1"), withDetail("caller1"))), + iFromRanges(Source.range("Callee"))))); auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get()); - EXPECT_THAT(IncomingLevel2, - ElementsAre(AllOf(from(withName("caller2")), - fromRanges(Source.range("Caller1"))))); + EXPECT_THAT( + IncomingLevel2, + ElementsAre(AllOf(from(AllOf(withName("caller2"), withDetail("caller2"))), + iFromRanges(Source.range("Caller1"))))); } TEST(CallHierarchy, IncomingQualified) { @@ -200,14 +219,72 @@ prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename)); ASSERT_THAT(Items, ElementsAre(withName("Waldo::find"))); auto Incoming = incomingCalls(Items[0], Index.get()); - EXPECT_THAT(Incoming, - ElementsAre(AllOf(from(withName("caller1")), - fromRanges(Source.range("Caller1"))), - AllOf(from(withName("caller2")), - fromRanges(Source.range("Caller2"))))); + EXPECT_THAT( + Incoming, + ElementsAre( + AllOf(from(AllOf(withName("caller1"), withDetail("ns::caller1"))), + iFromRanges(Source.range("Caller1"))), + AllOf(from(AllOf(withName("caller2"), withDetail("ns::caller2"))), + iFromRanges(Source.range("Caller2"))))); +} + +TEST(CallHierarchy, OutgoingOneFile) { + // Test outgoing call on the main file, with namespaces and methods + Annotations Source(R"cpp( + void callee(int); + namespace ns { + struct Foo { + void caller1(); + }; + void Foo::caller1() { + $Callee[[callee]](42); + } + } + namespace { + void caller2(ns::Foo& F) { + F.$Caller1A[[caller1]](); + F.$Caller1B[[caller1]](); + } + } + void call^er3(ns::Foo& F) { + F.$Caller1C[[caller1]](); + $Caller2[[caller2]](F); + } + )cpp"); + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + auto Index = TU.index(); + + std::vector Items = + prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename)); + ASSERT_THAT(Items, ElementsAre(withName("caller3"))); + auto OugoingLevel1 = outgoingCalls(Items[0], Index.get()); + ASSERT_THAT( + OugoingLevel1, + ElementsAre( + AllOf(to(AllOf(withName("caller1"), withDetail("ns::Foo::caller1"))), + oFromRanges(Source.range("Caller1C"))), + AllOf(to(AllOf(withName("caller2"), withDetail("caller2"))), + oFromRanges(Source.range("Caller2"))))); + + auto OutgoingLevel2 = outgoingCalls(OugoingLevel1[1].to, Index.get()); + ASSERT_THAT( + OutgoingLevel2, + ElementsAre(AllOf( + to(AllOf(withName("caller1"), withDetail("ns::Foo::caller1"))), + oFromRanges(Source.range("Caller1A"), Source.range("Caller1B"))))); + + auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get()); + ASSERT_THAT( + OutgoingLevel3, + ElementsAre(AllOf(to(AllOf(withName("callee"), withDetail("callee"))), + oFromRanges(Source.range("Callee"))))); + + auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get()); + EXPECT_THAT(OutgoingLevel4, IsEmpty()); } -TEST(CallHierarchy, IncomingMultiFileCpp) { +TEST(CallHierarchy, MultiFileCpp) { // 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 @@ -221,32 +298,47 @@ void calle^e(int) {} )cpp"); Annotations Caller1H(R"cpp( - void caller1(); + namespace nsa { + void caller1(); + } )cpp"); Annotations Caller1C(R"cpp( #include "callee.hh" #include "caller1.hh" - void caller1() { - [[calle^e]](42); + namespace nsa { + void caller1() { + [[calle^e]](42); + } } )cpp"); Annotations Caller2H(R"cpp( - void caller2(); + namespace nsb { + void caller2(); + } )cpp"); Annotations Caller2C(R"cpp( #include "caller1.hh" #include "caller2.hh" - void caller2() { - $A[[caller1]](); - $B[[caller1]](); + namespace nsb { + void caller2() { + nsa::$A[[caller1]](); + nsa::$B[[caller1]](); + } + } + )cpp"); + Annotations Caller3H(R"cpp( + namespace nsa { + void call^er3(); } )cpp"); Annotations Caller3C(R"cpp( #include "caller1.hh" #include "caller2.hh" - void caller3() { - $Caller1[[caller1]](); - $Caller2[[caller2]](); + namespace nsa { + void call^er3() { + $Caller1[[caller1]](); + nsb::$Caller2[[caller2]](); + } } )cpp"); @@ -254,6 +346,7 @@ Workspace.addSource("callee.hh", CalleeH.code()); Workspace.addSource("caller1.hh", Caller1H.code()); Workspace.addSource("caller2.hh", Caller2H.code()); + Workspace.addSource("caller3.hh", Caller3H.code()); Workspace.addMainFile("callee.cc", CalleeC.code()); Workspace.addMainFile("caller1.cc", Caller1C.code()); Workspace.addMainFile("caller2.cc", Caller2C.code()); @@ -261,46 +354,84 @@ auto Index = Workspace.index(); - auto CheckCallHierarchy = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { + auto CheckIncomingCalls = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { std::vector Items = prepareCallHierarchy(AST, Pos, TUPath); ASSERT_THAT(Items, ElementsAre(withName("callee"))); auto IncomingLevel1 = incomingCalls(Items[0], Index.get()); ASSERT_THAT(IncomingLevel1, - ElementsAre(AllOf(from(withName("caller1")), - fromRanges(Caller1C.range())))); + ElementsAre(AllOf(from(AllOf(withName("caller1"), + withDetail("nsa::caller1"))), + iFromRanges(Caller1C.range())))); auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get()); ASSERT_THAT( IncomingLevel2, - ElementsAre(AllOf(from(withName("caller2")), - fromRanges(Caller2C.range("A"), Caller2C.range("B"))), - AllOf(from(withName("caller3")), - fromRanges(Caller3C.range("Caller1"))))); + ElementsAre( + AllOf(from(AllOf(withName("caller2"), withDetail("nsb::caller2"))), + iFromRanges(Caller2C.range("A"), Caller2C.range("B"))), + AllOf(from(AllOf(withName("caller3"), withDetail("nsa::caller3"))), + iFromRanges(Caller3C.range("Caller1"))))); auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); ASSERT_THAT(IncomingLevel3, - ElementsAre(AllOf(from(withName("caller3")), - fromRanges(Caller3C.range("Caller2"))))); + ElementsAre(AllOf(from(AllOf(withName("caller3"), + withDetail("nsa::caller3"))), + iFromRanges(Caller3C.range("Caller2"))))); auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); EXPECT_THAT(IncomingLevel4, IsEmpty()); }; + auto CheckOutgoingCalls = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { + std::vector Items = + prepareCallHierarchy(AST, Pos, TUPath); + ASSERT_THAT(Items, ElementsAre(withName("caller3"))); + auto OutgoingLevel1 = outgoingCalls(Items[0], Index.get()); + ASSERT_THAT( + OutgoingLevel1, + ElementsAre( + AllOf(to(AllOf(withName("caller1"), withDetail("nsa::caller1"))), + oFromRanges(Caller3C.range("Caller1"))), + AllOf(to(AllOf(withName("caller2"), withDetail("nsb::caller2"))), + oFromRanges(Caller3C.range("Caller2"))))); + + auto OutgoingLevel2 = outgoingCalls(OutgoingLevel1[1].to, Index.get()); + ASSERT_THAT(OutgoingLevel2, + ElementsAre(AllOf( + to(AllOf(withName("caller1"), withDetail("nsa::caller1"))), + oFromRanges(Caller2C.range("A"), Caller2C.range("B"))))); + + auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get()); + ASSERT_THAT( + OutgoingLevel3, + ElementsAre(AllOf(to(AllOf(withName("callee"), withDetail("callee"))), + oFromRanges(Caller1C.range())))); + + auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get()); + EXPECT_THAT(OutgoingLevel4, IsEmpty()); + }; + // 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")); + CheckIncomingCalls(*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")); + CheckIncomingCalls(*AST, CalleeH.point(), testPath("callee.hh")); + AST = Workspace.openFile("caller3.hh"); + ASSERT_TRUE(bool(AST)); + CheckOutgoingCalls(*AST, Caller3H.point(), testPath("caller3.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")); + CheckIncomingCalls(*AST, CalleeC.point(), testPath("callee.cc")); + AST = Workspace.openFile("caller3.cc"); + ASSERT_TRUE(bool(AST)); + CheckOutgoingCalls(*AST, Caller3C.point(), testPath("caller3.cc")); } TEST(CallHierarchy, IncomingMultiFileObjC) { @@ -377,20 +508,20 @@ auto IncomingLevel1 = incomingCalls(Items[0], Index.get()); ASSERT_THAT(IncomingLevel1, ElementsAre(AllOf(from(withName("caller1")), - fromRanges(Caller1C.range())))); + iFromRanges(Caller1C.range())))); auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get()); - ASSERT_THAT( - IncomingLevel2, - ElementsAre(AllOf(from(withName("caller2")), - fromRanges(Caller2C.range("A"), Caller2C.range("B"))), - AllOf(from(withName("caller3")), - fromRanges(Caller3C.range("Caller1"))))); + ASSERT_THAT(IncomingLevel2, + ElementsAre(AllOf(from(withName("caller2")), + iFromRanges(Caller2C.range("A"), + Caller2C.range("B"))), + AllOf(from(withName("caller3")), + iFromRanges(Caller3C.range("Caller1"))))); auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); ASSERT_THAT(IncomingLevel3, ElementsAre(AllOf(from(withName("caller3")), - fromRanges(Caller3C.range("Caller2"))))); + iFromRanges(Caller3C.range("Caller2"))))); auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); EXPECT_THAT(IncomingLevel4, IsEmpty()); @@ -438,12 +569,12 @@ ASSERT_THAT(Items, ElementsAre(withName("callee"))); auto Incoming = incomingCalls(Items[0], Index.get()); - ASSERT_THAT( - Incoming, - ElementsAre( - AllOf(from(withName("caller1")), fromRanges(Source.range("call1"))), - AllOf(from(withName("caller2")), fromRanges(Source.range("call2"))), - AllOf(from(withName("caller3")), fromRanges(Source.range("call3"))))); + ASSERT_THAT(Incoming, ElementsAre(AllOf(from(withName("caller1")), + iFromRanges(Source.range("call1"))), + AllOf(from(withName("caller2")), + iFromRanges(Source.range("call2"))), + AllOf(from(withName("caller3")), + iFromRanges(Source.range("call3"))))); } } // namespace diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp --- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp @@ -1644,6 +1644,12 @@ return false; } + bool containedRefs( + const ContainedRefsRequest &, + llvm::function_ref) const override { + return false; + } + void relations(const RelationsRequest &, llvm::function_ref) const override {} diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp --- a/clang-tools-extra/clangd/unittests/RenameTests.cpp +++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp @@ -1417,6 +1417,12 @@ return true; // has more references } + bool containedRefs(const ContainedRefsRequest &Req, + llvm::function_ref + Callback) const override { + return false; + } + bool fuzzyFind( const FuzzyFindRequest &Req, llvm::function_ref Callback) const override { @@ -1468,6 +1474,12 @@ return false; } + bool containedRefs(const ContainedRefsRequest &Req, + llvm::function_ref + Callback) const override { + return false; + } + bool fuzzyFind(const FuzzyFindRequest &, llvm::function_ref) const override { return false;