-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[clangd] Support callHierarchy/outgoingCalls #91191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[clangd] Support callHierarchy/outgoingCalls #91191
Conversation
@llvm/pr-subscribers-clang-tools-extra @llvm/pr-subscribers-clangd Author: Christian Kandeler (ckandeler) ChangesPatch is 23.27 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/91191.diff 7 Files Affected:
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 7fd599d4e1a0b0..5820a644088e3e 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -1368,6 +1368,12 @@ void ClangdLSPServer::onCallHierarchyIncomingCalls(
Server->incomingCalls(Params.item, std::move(Reply));
}
+void ClangdLSPServer::onCallHierarchyOutgoingCalls(
+ const CallHierarchyOutgoingCallsParams &Params,
+ Callback<std::vector<CallHierarchyOutgoingCall>> Reply) {
+ Server->outgoingCalls(Params.item, std::move(Reply));
+}
+
void ClangdLSPServer::onClangdInlayHints(const InlayHintsParams &Params,
Callback<llvm::json::Value> Reply) {
// Our extension has a different representation on the wire than the standard.
@@ -1688,6 +1694,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
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/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 8bcb29522509b7..4981027372cb57 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -153,6 +153,9 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
void onCallHierarchyIncomingCalls(
const CallHierarchyIncomingCallsParams &,
Callback<std::vector<CallHierarchyIncomingCall>>);
+ void onCallHierarchyOutgoingCalls(
+ const CallHierarchyOutgoingCallsParams &,
+ Callback<std::vector<CallHierarchyOutgoingCall>>);
void onClangdInlayHints(const InlayHintsParams &,
Callback<llvm::json::Value>);
void onInlayHint(const InlayHintsParams &, Callback<std::vector<InlayHint>>);
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 1c4c2a79b5c051..19d01dfbd873e2 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -898,6 +898,19 @@ void ClangdServer::incomingCalls(
});
}
+void ClangdServer::outgoingCalls(
+ const CallHierarchyItem &Item,
+ Callback<std::vector<CallHierarchyOutgoingCall>> CB) {
+ auto Action = [Item,
+ CB = std::move(CB)](Expected<InputsAndAST> InpAST) mutable {
+ if (!InpAST)
+ return CB(InpAST.takeError());
+ CB(clangd::outgoingCalls(InpAST->AST, Item));
+ };
+ WorkScheduler->runWithAST("Outgoing Calls", Item.uri.file(),
+ std::move(Action));
+}
+
void ClangdServer::inlayHints(PathRef File, std::optional<Range> RestrictRange,
Callback<std::vector<InlayHint>> CB) {
auto Action = [RestrictRange(std::move(RestrictRange)),
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 1661028be88b4e..4caef917c1ec16 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -288,6 +288,10 @@ class ClangdServer {
void incomingCalls(const CallHierarchyItem &Item,
Callback<std::vector<CallHierarchyIncomingCall>>);
+ /// Resolve outgoing calls for a given call hierarchy item.
+ void outgoingCalls(const CallHierarchyItem &Item,
+ Callback<std::vector<CallHierarchyOutgoingCall>>);
+
/// Resolve inlay hints for a given document.
void inlayHints(PathRef File, std::optional<Range> RestrictRange,
Callback<std::vector<InlayHint>>);
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index cd909266489a85..b9bf944a7bba98 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -42,6 +42,7 @@
#include "clang/AST/StmtCXX.h"
#include "clang/AST/StmtVisitor.h"
#include "clang/AST/Type.h"
+#include "clang/Analysis/CallGraph.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceLocation.h"
@@ -2303,6 +2304,76 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
return Results;
}
+std::vector<CallHierarchyOutgoingCall>
+outgoingCalls(ParsedAST &AST, const CallHierarchyItem &Item) {
+ if (AST.tuPath() != Item.uri.file())
+ return {};
+
+ const auto &SM = AST.getSourceManager();
+ auto Loc = sourceLocationInMainFile(SM, Item.selectionRange.start);
+ if (!Loc) {
+ elog("outgoingCalls failed to convert position to source location: "
+ "{0}",
+ Loc.takeError());
+ return {};
+ }
+
+ // For user convenience, we allow cursors on declarations, in which case
+ // we will try to find the definition.
+ std::optional<std::pair<const NamedDecl *, const NamedDecl *>> DeclAndDef;
+ for (const NamedDecl *Decl : getDeclAtPosition(AST, *Loc, {})) {
+ if (getSymbolID(Decl).str() != Item.data)
+ continue;
+ DeclAndDef = std::make_pair(Decl, getDefinition(Decl));
+ break;
+ }
+ if (!DeclAndDef || !DeclAndDef->second)
+ return {};
+
+ // Collect calls.
+ CallGraph callGraph;
+ callGraph.addToCallGraph(const_cast<NamedDecl *>(DeclAndDef->second));
+ llvm::DenseMap<NamedDecl *, std::vector<Range>> CallsOut;
+ for (const CallGraphNode::CallRecord &CallRecord :
+ callGraph.getRoot()->callees()) {
+ if (CallRecord.Callee->getDecl() != DeclAndDef->first)
+ continue;
+ for (const CallGraphNode::CallRecord &CallRecord :
+ CallRecord.Callee->callees()) {
+ SourceLocation BeginLoc = CallRecord.CallExpr->getBeginLoc();
+ if (auto *M = dyn_cast_if_present<ObjCMessageExpr>(CallRecord.CallExpr)) {
+ BeginLoc = M->getSelectorStartLoc();
+ } else if (auto *M = dyn_cast_if_present<CXXMemberCallExpr>(
+ CallRecord.CallExpr)) {
+ if (auto ME = dyn_cast_if_present<MemberExpr>(M->getCallee()))
+ BeginLoc = ME->getMemberLoc();
+ }
+ BeginLoc = SM.getFileLoc(BeginLoc);
+ Position NameBegin = sourceLocToPosition(SM, BeginLoc);
+ Position NameEnd = sourceLocToPosition(
+ SM, Lexer::getLocForEndOfToken(BeginLoc, 0, SM,
+ AST.getASTContext().getLangOpts()));
+ CallsOut[static_cast<NamedDecl *>(CallRecord.Callee->getDecl())]
+ .push_back(Range{NameBegin, NameEnd});
+ }
+ break;
+ }
+
+ // Create and sort items.
+ std::vector<CallHierarchyOutgoingCall> Results;
+ for (auto It = CallsOut.begin(); It != CallsOut.end(); ++It) {
+ if (auto CHI = declToCallHierarchyItem(*It->first, Item.uri.file())) {
+ Results.push_back(CallHierarchyOutgoingCall{std::move(*CHI), It->second});
+ }
+ }
+ llvm::sort(Results, [](const CallHierarchyOutgoingCall &A,
+ const CallHierarchyOutgoingCall &B) {
+ return A.to.name < B.to.name;
+ });
+
+ return Results;
+}
+
llvm::DenseSet<const Decl *> getNonLocalDeclRefs(ParsedAST &AST,
const FunctionDecl *FD) {
if (!FD->hasBody())
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index df91dd15303c18..73c22ab7673ad0 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -150,6 +150,9 @@ prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath);
std::vector<CallHierarchyIncomingCall>
incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index);
+std::vector<CallHierarchyOutgoingCall>
+outgoingCalls(ParsedAST &AST, const CallHierarchyItem &Item);
+
/// Returns all decls that are referenced in the \p FD except local symbols.
llvm::DenseSet<const Decl *> getNonLocalDeclRefs(ParsedAST &AST,
const FunctionDecl *FD);
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 6fa76aa6094bf2..0ec96599ccc43f 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -50,11 +50,21 @@ template <class ItemMatcher>
::testing::Matcher<CallHierarchyIncomingCall> from(ItemMatcher M) {
return Field(&CallHierarchyIncomingCall::from, M);
}
+template <class ItemMatcher>
+::testing::Matcher<CallHierarchyOutgoingCall> to(ItemMatcher M) {
+ return Field(&CallHierarchyOutgoingCall::to, M);
+}
template <class... RangeMatchers>
-::testing::Matcher<CallHierarchyIncomingCall> fromRanges(RangeMatchers... M) {
+::testing::Matcher<CallHierarchyIncomingCall> fromRangesIn(RangeMatchers... M) {
return Field(&CallHierarchyIncomingCall::fromRanges,
UnorderedElementsAre(M...));
}
+template <class... RangeMatchers>
+::testing::Matcher<CallHierarchyOutgoingCall>
+fromRangesOut(RangeMatchers... M) {
+ return Field(&CallHierarchyOutgoingCall::fromRanges,
+ UnorderedElementsAre(M...));
+}
TEST(CallHierarchy, IncomingOneFileCpp) {
Annotations Source(R"cpp(
@@ -81,24 +91,61 @@ TEST(CallHierarchy, IncomingOneFileCpp) {
auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
ASSERT_THAT(IncomingLevel1,
ElementsAre(AllOf(from(withName("caller1")),
- fromRanges(Source.range("Callee")))));
+ fromRangesIn(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"))),
+ fromRangesIn(Source.range("Caller1A"),
+ Source.range("Caller1B"))),
AllOf(from(withName("caller3")),
- fromRanges(Source.range("Caller1C")))));
+ fromRangesIn(Source.range("Caller1C")))));
auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
ASSERT_THAT(IncomingLevel3,
ElementsAre(AllOf(from(withName("caller3")),
- fromRanges(Source.range("Caller2")))));
+ fromRangesIn(Source.range("Caller2")))));
auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
EXPECT_THAT(IncomingLevel4, IsEmpty());
}
+TEST(CallHierarchy, OutgoingOneFileCpp) {
+ Annotations Source(R"cpp(
+ void calle^r1();
+ void terminator1() {}
+ void terminator2() { struct S { S() { terminator1(); } }; }
+ void caller2() {
+ $Terminator1A[[terminator1]]();
+ $Terminator1B[[terminator1]]();
+ $Terminator2[[terminator2]]();
+ }
+ void caller1() {
+ $Caller2[[caller2]]();
+ }
+ )cpp");
+ TestTU TU = TestTU::withCode(Source.code());
+ auto AST = TU.build();
+
+ std::vector<CallHierarchyItem> Items =
+ prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
+ ASSERT_THAT(Items, ElementsAre(withName("caller1")));
+ auto OutgoingLevel1 = outgoingCalls(AST, Items[0]);
+ ASSERT_THAT(OutgoingLevel1,
+ ElementsAre(AllOf(to(withName("caller2")),
+ fromRangesOut(Source.range("Caller2")))));
+ auto OutgoingLevel2 = outgoingCalls(AST, OutgoingLevel1[0].to);
+ ASSERT_THAT(OutgoingLevel2,
+ ElementsAre(AllOf(to(withName("terminator1")),
+ fromRangesOut(Source.range("Terminator1A"),
+ Source.range("Terminator1B"))),
+ AllOf(to(withName("terminator2")),
+ fromRangesOut(Source.range("Terminator2")))));
+ auto OutgoingLevel3a = outgoingCalls(AST, OutgoingLevel2[0].to);
+ EXPECT_THAT(OutgoingLevel3a, IsEmpty());
+ auto OutgoingLevel3b = outgoingCalls(AST, OutgoingLevel2[1].to);
+ EXPECT_THAT(OutgoingLevel3b, IsEmpty());
+}
+
TEST(CallHierarchy, IncomingOneFileObjC) {
Annotations Source(R"objc(
@implementation MyClass {}
@@ -126,24 +173,66 @@ TEST(CallHierarchy, IncomingOneFileObjC) {
auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
ASSERT_THAT(IncomingLevel1,
ElementsAre(AllOf(from(withName("caller1")),
- fromRanges(Source.range("Callee")))));
+ fromRangesIn(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"))),
+ fromRangesIn(Source.range("Caller1A"),
+ Source.range("Caller1B"))),
AllOf(from(withName("caller3")),
- fromRanges(Source.range("Caller1C")))));
+ fromRangesIn(Source.range("Caller1C")))));
auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
ASSERT_THAT(IncomingLevel3,
ElementsAre(AllOf(from(withName("caller3")),
- fromRanges(Source.range("Caller2")))));
+ fromRangesIn(Source.range("Caller2")))));
auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
EXPECT_THAT(IncomingLevel4, IsEmpty());
}
+TEST(CallHierarchy, OutgoingOneFileObjC) {
+ Annotations Source(R"objc(
+ @implementation MyClass {}
+ +(void)callee {}
+ +(void) caller1 {
+ [MyClass $Callee[[callee]]];
+ }
+ +(void) caller2 {
+ [MyClass $Caller1A[[caller1]]];
+ [MyClass $Caller1B[[caller1]]];
+ }
+ +(void) calle^r3 {
+ [MyClass $Caller1C[[caller1]]];
+ [MyClass $Caller2[[caller2]]];
+ }
+ @end
+ )objc");
+ TestTU TU = TestTU::withCode(Source.code());
+ TU.Filename = "TestTU.m";
+ auto AST = TU.build();
+ std::vector<CallHierarchyItem> Items =
+ prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
+ ASSERT_THAT(Items, ElementsAre(withName("caller3")));
+ auto OutgoingLevel1 = outgoingCalls(AST, Items[0]);
+ ASSERT_THAT(OutgoingLevel1,
+ ElementsAre(AllOf(to(withName("caller1")),
+ fromRangesOut(Source.range("Caller1C"))),
+ AllOf(to(withName("caller2")),
+ fromRangesOut(Source.range("Caller2")))));
+ auto OutgoingLevel2a = outgoingCalls(AST, OutgoingLevel1[0].to);
+ ASSERT_THAT(OutgoingLevel2a,
+ ElementsAre(AllOf(to(withName("callee")),
+ fromRangesOut(Source.range("Callee")))));
+ auto OutgoingLevel2b = outgoingCalls(AST, OutgoingLevel1[1].to);
+ ASSERT_THAT(OutgoingLevel2b,
+ ElementsAre(AllOf(to(withName("caller1")),
+ fromRangesOut(Source.range("Caller1A"),
+ Source.range("Caller1B")))));
+ auto OutgoingLevel3 = outgoingCalls(AST, OutgoingLevel2a[0].to);
+ EXPECT_THAT(OutgoingLevel3, IsEmpty());
+}
+
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
@@ -169,12 +258,12 @@ TEST(CallHierarchy, MainFileOnlyRef) {
auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
ASSERT_THAT(IncomingLevel1,
ElementsAre(AllOf(from(withName("caller1")),
- fromRanges(Source.range("Callee")))));
+ fromRangesIn(Source.range("Callee")))));
auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
EXPECT_THAT(IncomingLevel2,
ElementsAre(AllOf(from(withName("caller2")),
- fromRanges(Source.range("Caller1")))));
+ fromRangesIn(Source.range("Caller1")))));
}
TEST(CallHierarchy, IncomingQualified) {
@@ -202,9 +291,39 @@ TEST(CallHierarchy, IncomingQualified) {
auto Incoming = incomingCalls(Items[0], Index.get());
EXPECT_THAT(Incoming,
ElementsAre(AllOf(from(withName("caller1")),
- fromRanges(Source.range("Caller1"))),
+ fromRangesIn(Source.range("Caller1"))),
AllOf(from(withName("caller2")),
- fromRanges(Source.range("Caller2")))));
+ fromRangesIn(Source.range("Caller2")))));
+}
+
+TEST(CallHierarchy, OutgoingQualified) {
+ Annotations Source(R"cpp(
+ namespace ns {
+ class Waldo {
+ public:
+ void find() { $Find[[locate]](); }
+ private:
+ void locate() {}
+ };
+ void c^aller(Waldo &W) {
+ W.$Caller[[find]]();
+ }
+ }
+ )cpp");
+ TestTU TU = TestTU::withCode(Source.code());
+ auto AST = TU.build();
+
+ std::vector<CallHierarchyItem> Items =
+ prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
+ ASSERT_THAT(Items, ElementsAre(withName("caller")));
+ auto OutgoingLevel1 = outgoingCalls(AST, Items[0]);
+ EXPECT_THAT(OutgoingLevel1,
+ ElementsAre(AllOf(to(withName("find")),
+ fromRangesOut(Source.range("Caller")))));
+ auto OutgoingLevel2 = outgoingCalls(AST, OutgoingLevel1[0].to);
+ EXPECT_THAT(OutgoingLevel2,
+ ElementsAre(AllOf(to(withName("locate")),
+ fromRangesOut(Source.range("Find")))));
}
TEST(CallHierarchy, IncomingMultiFileCpp) {
@@ -268,20 +387,20 @@ TEST(CallHierarchy, IncomingMultiFileCpp) {
auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
ASSERT_THAT(IncomingLevel1,
ElementsAre(AllOf(from(withName("caller1")),
- fromRanges(Caller1C.range()))));
+ fromRangesIn(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")),
+ fromRangesIn(Caller2C.range("A"),
+ Caller2C.range("B"))),
+ AllOf(from(withName("caller3")),
+ fromRangesIn(Caller3C.range("Caller1")))));
auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
ASSERT_THAT(IncomingLevel3,
ElementsAre(AllOf(from(withName("caller3")),
- fromRanges(Caller3C.range("Caller2")))));
+ fromRangesIn(Caller3C.range("Caller2")))));
auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
EXPECT_THAT(IncomingLevel4, IsEmpty());
@@ -303,6 +422,44 @@ TEST(CallHierarchy, IncomingMultiFileCpp) {
CheckCallHierarchy(*AST, CalleeC.point(), testPath("callee.cc"));
}
+TEST(CallHierarchy, OutgoingMultiFileCpp) {
+ Annotations CalleeH(R"cpp(
+ void calleeFwd() { $CalleeFwd[[callee]](0); }
+ void callee(int);
+ )cpp");
+ Annotations CalleeC(R"cpp(
+ #include "callee.hh"
+ void callee(int) {}
+ )cpp");
+ Annotations Caller(R"cpp(
+ #include "callee.hh"
+ ...
[truncated]
|
I remembered @HighCommander4 had filed a similar PR at #77556. Is this one separate, or are they actually the same (i.e. both are salvaged from https://reviews.llvm.org/D93829)? |
I didn't know about that one. Seems nothing gets reviewed anymore these days. |
Yeah, and unfortunately folks who are qualified/versed in clangd indices don't have enough bandwidths recently, nor does the author @HighCommander4. |
If I'm understanding correctly, the implementation approach in this PR only finds callees in the current translation unit. The approach in #77556 uses the project's index to find callees across translation unit boundaries. Regarding reviews: yes, it seems quite unfortunate that the original developers seem to have largely moved on to other things. I will do my best to make some progress of the project's review backlog (including in particular #86629 and #67802) as time permits. |
Right, that's obviously nicer.
Your work is highly appreciated, but I don't think it's reasonable to expect a single unpaid contributor to maintain the entire project, as appears to be the case right now. |
No description provided.