[clang-tools-extra] [clangd] Support callHierarchy/outgoingCalls (PR #91191)
via cfe-commits
cfe-commits at lists.llvm.org
Mon May 6 04:47:42 PDT 2024
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-tools-extra
@llvm/pr-subscribers-clangd
Author: Christian Kandeler (ckandeler)
<details>
<summary>Changes</summary>
---
Patch is 23.27 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/91191.diff
7 Files Affected:
- (modified) clang-tools-extra/clangd/ClangdLSPServer.cpp (+7)
- (modified) clang-tools-extra/clangd/ClangdLSPServer.h (+3)
- (modified) clang-tools-extra/clangd/ClangdServer.cpp (+13)
- (modified) clang-tools-extra/clangd/ClangdServer.h (+4)
- (modified) clang-tools-extra/clangd/XRefs.cpp (+71)
- (modified) clang-tools-extra/clangd/XRefs.h (+3)
- (modified) clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp (+192-34)
``````````diff
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]
``````````
</details>
https://github.com/llvm/llvm-project/pull/91191
More information about the cfe-commits
mailing list