| //===-- 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 { |
| |
| llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream, |
| const CallHierarchyItem &Item) { |
| return Stream << Item.name << "@" << Item.selectionRange; |
| } |
| |
| llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream, |
| const CallHierarchyIncomingCall &Call) { |
| Stream << "{ from: " << Call.from << ", ranges: ["; |
| for (const auto &R : Call.fromRanges) { |
| Stream << R; |
| Stream << ", "; |
| } |
| return Stream << "] }"; |
| } |
| |
| namespace { |
| |
| using ::testing::AllOf; |
| using ::testing::ElementsAre; |
| using ::testing::Field; |
| using ::testing::IsEmpty; |
| 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 <class ItemMatcher> |
| ::testing::Matcher<CallHierarchyIncomingCall> From(ItemMatcher M) { |
| return Field(&CallHierarchyIncomingCall::from, M); |
| } |
| template <class... RangeMatchers> |
| ::testing::Matcher<CallHierarchyIncomingCall> FromRanges(RangeMatchers... M) { |
| return Field(&CallHierarchyIncomingCall::fromRanges, |
| UnorderedElementsAre(M...)); |
| } |
| |
| TEST(CallHierarchy, IncomingOneFileCpp) { |
| 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(); |
| |
| std::vector<CallHierarchyItem> Items = |
| 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"))))); |
| 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"))))); |
| |
| auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); |
| ASSERT_THAT(IncomingLevel3, |
| ElementsAre(AllOf(From(WithName("caller3")), |
| FromRanges(Source.range("Caller2"))))); |
| |
| auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); |
| EXPECT_THAT(IncomingLevel4, IsEmpty()); |
| } |
| |
| TEST(CallHierarchy, IncomingOneFileObjC) { |
| Annotations Source(R"objc( |
| @implementation MyClass {} |
| +(void)call^ee {} |
| +(void) caller1 { |
| [MyClass $Callee[[callee]]]; |
| } |
| +(void) caller2 { |
| [MyClass $Caller1A[[caller1]]]; |
| [MyClass $Caller1B[[caller1]]]; |
| } |
| +(void) caller3 { |
| [MyClass $Caller1C[[caller1]]]; |
| [MyClass $Caller2[[caller2]]]; |
| } |
| @end |
| )objc"); |
| TestTU TU = TestTU::withCode(Source.code()); |
| TU.Filename = "TestTU.m"; |
| auto AST = TU.build(); |
| auto Index = TU.index(); |
| std::vector<CallHierarchyItem> Items = |
| 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"))))); |
| 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"))))); |
| |
| auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); |
| ASSERT_THAT(IncomingLevel3, |
| ElementsAre(AllOf(From(WithName("caller3")), |
| FromRanges(Source.range("Caller2"))))); |
| |
| auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); |
| EXPECT_THAT(IncomingLevel4, 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 |
| // 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(); |
| |
| std::vector<CallHierarchyItem> Items = |
| 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"))))); |
| |
| auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get()); |
| EXPECT_THAT(IncomingLevel2, |
| ElementsAre(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(); |
| |
| std::vector<CallHierarchyItem> Items = |
| 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"))))); |
| } |
| |
| TEST(CallHierarchy, IncomingMultiFileCpp) { |
| // 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) { |
| std::vector<CallHierarchyItem> 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())))); |
| |
| 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"))))); |
| |
| auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); |
| ASSERT_THAT(IncomingLevel3, |
| ElementsAre(AllOf(From(WithName("caller3")), |
| FromRanges(Caller3C.range("Caller2"))))); |
| |
| auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); |
| EXPECT_THAT(IncomingLevel4, 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")); |
| |
| // 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")); |
| } |
| |
| TEST(CallHierarchy, IncomingMultiFileObjC) { |
| // The test uses a .mi suffix for header files to get clang |
| // to parse them in ObjC 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"objc( |
| @interface CalleeClass |
| +(void)call^ee; |
| @end |
| )objc"); |
| Annotations CalleeC(R"objc( |
| #import "callee.mi" |
| @implementation CalleeClass {} |
| +(void)call^ee {} |
| @end |
| )objc"); |
| Annotations Caller1H(R"objc( |
| @interface Caller1Class |
| +(void)caller1; |
| @end |
| )objc"); |
| Annotations Caller1C(R"objc( |
| #import "callee.mi" |
| #import "caller1.mi" |
| @implementation Caller1Class {} |
| +(void)caller1 { |
| [CalleeClass [[calle^e]]]; |
| } |
| @end |
| )objc"); |
| Annotations Caller2H(R"objc( |
| @interface Caller2Class |
| +(void)caller2; |
| @end |
| )objc"); |
| Annotations Caller2C(R"objc( |
| #import "caller1.mi" |
| #import "caller2.mi" |
| @implementation Caller2Class {} |
| +(void)caller2 { |
| [Caller1Class $A[[caller1]]]; |
| [Caller1Class $B[[caller1]]]; |
| } |
| @end |
| )objc"); |
| Annotations Caller3C(R"objc( |
| #import "caller1.mi" |
| #import "caller2.mi" |
| @implementation Caller3Class {} |
| +(void)caller3 { |
| [Caller1Class $Caller1[[caller1]]]; |
| [Caller2Class $Caller2[[caller2]]]; |
| } |
| @end |
| )objc"); |
| |
| TestWorkspace Workspace; |
| Workspace.addSource("callee.mi", CalleeH.code()); |
| Workspace.addSource("caller1.mi", Caller1H.code()); |
| Workspace.addSource("caller2.mi", Caller2H.code()); |
| Workspace.addMainFile("callee.m", CalleeC.code()); |
| Workspace.addMainFile("caller1.m", Caller1C.code()); |
| Workspace.addMainFile("caller2.m", Caller2C.code()); |
| Workspace.addMainFile("caller3.m", Caller3C.code()); |
| auto Index = Workspace.index(); |
| |
| auto CheckCallHierarchy = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { |
| std::vector<CallHierarchyItem> 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())))); |
| |
| 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"))))); |
| |
| auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get()); |
| ASSERT_THAT(IncomingLevel3, |
| ElementsAre(AllOf(From(WithName("caller3")), |
| FromRanges(Caller3C.range("Caller2"))))); |
| |
| auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get()); |
| EXPECT_THAT(IncomingLevel4, IsEmpty()); |
| }; |
| |
| // Check that invoking from a call site works. |
| auto AST = Workspace.openFile("caller1.m"); |
| ASSERT_TRUE(bool(AST)); |
| CheckCallHierarchy(*AST, Caller1C.point(), testPath("caller1.m")); |
| |
| // Check that invoking from the declaration site works. |
| AST = Workspace.openFile("callee.mi"); |
| ASSERT_TRUE(bool(AST)); |
| CheckCallHierarchy(*AST, CalleeH.point(), testPath("callee.mi")); |
| |
| // Check that invoking from the definition site works. |
| AST = Workspace.openFile("callee.m"); |
| ASSERT_TRUE(bool(AST)); |
| CheckCallHierarchy(*AST, CalleeC.point(), testPath("callee.m")); |
| } |
| |
| TEST(CallHierarchy, CallInLocalVarDecl) { |
| // Tests that local variable declarations are not treated as callers |
| // (they're not indexed, so they can't be represented as call hierarchy |
| // items); instead, the caller should be the containing function. |
| // However, namespace-scope variable declarations should be treated as |
| // callers because those are indexed and there is no enclosing entity |
| // that would be a useful caller. |
| Annotations Source(R"cpp( |
| int call^ee(); |
| void caller1() { |
| $call1[[callee]](); |
| } |
| void caller2() { |
| int localVar = $call2[[callee]](); |
| } |
| int caller3 = $call3[[callee]](); |
| )cpp"); |
| TestTU TU = TestTU::withCode(Source.code()); |
| auto AST = TU.build(); |
| auto Index = TU.index(); |
| |
| std::vector<CallHierarchyItem> Items = |
| prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename)); |
| 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"))))); |
| } |
| |
| } // namespace |
| } // namespace clangd |
| } // namespace clang |