nridge updated this revision to Diff 305400. nridge marked 12 inline comments as done. nridge added a comment.
Address review comments Repository: rG LLVM Github Monorepo CHANGES SINCE LAST ACTION https://reviews.llvm.org/D91122/new/ https://reviews.llvm.org/D91122 Files: clang-tools-extra/clangd/XRefs.cpp clang-tools-extra/clangd/XRefs.h clang-tools-extra/clangd/unittests/CMakeLists.txt clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp clang-tools-extra/clangd/unittests/TestTU.cpp
Index: clang-tools-extra/clangd/unittests/TestTU.cpp =================================================================== --- clang-tools-extra/clangd/unittests/TestTU.cpp +++ clang-tools-extra/clangd/unittests/TestTU.cpp @@ -156,7 +156,8 @@ std::unique_ptr<SymbolIndex> TestTU::index() const { auto AST = build(); - auto Idx = std::make_unique<FileIndex>(/*UseDex=*/true); + auto Idx = std::make_unique<FileIndex>(/*UseDex=*/true, + /*CollectMainFileRefs=*/true); Idx->updatePreamble(testPath(Filename), /*Version=*/"null", AST.getASTContext(), AST.getPreprocessorPtr(), AST.getCanonicalIncludes()); Index: clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp =================================================================== --- /dev/null +++ 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 <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, 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<std::vector<CallHierarchyItem>> Items = + prepareCallHierarchy(AST, Source.point(), 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<std::vector<CallHierarchyItem>> Items = + prepareCallHierarchy(AST, Source.point(), 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<std::vector<CallHierarchyItem>> Items = + prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename)); + ASSERT_TRUE(bool(Items)); + EXPECT_THAT(*Items, ElementsAre(WithName("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<std::vector<CallHierarchyItem>> Items = + prepareCallHierarchy(AST, Pos, 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 Index: clang-tools-extra/clangd/unittests/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/unittests/CMakeLists.txt +++ 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 Index: clang-tools-extra/clangd/XRefs.h =================================================================== --- clang-tools-extra/clangd/XRefs.h +++ clang-tools-extra/clangd/XRefs.h @@ -105,6 +105,16 @@ TypeHierarchyDirection Direction, const SymbolIndex *Index); +/// Get call hierarchy information at \p Pos. +llvm::Optional<std::vector<CallHierarchyItem>> +prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath); + +llvm::Optional<std::vector<CallHierarchyIncomingCall>> +incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index); + +llvm::Optional<std::vector<CallHierarchyOutgoingCall>> +outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index); + /// Returns all decls that are referenced in the \p FD except local symbols. llvm::DenseSet<const Decl *> getNonLocalDeclRefs(ParsedAST &AST, const FunctionDecl *FD); Index: clang-tools-extra/clangd/XRefs.cpp =================================================================== --- clang-tools-extra/clangd/XRefs.cpp +++ 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" @@ -1287,9 +1288,9 @@ return OS; } -// FIXME(nridge): Reduce duplication between this function and declToSym(). -static llvm::Optional<TypeHierarchyItem> -declToTypeHierarchyItem(ASTContext &Ctx, const NamedDecl &ND) { +template <typename HierarchyItem> +static llvm::Optional<HierarchyItem> declToHierarchyItem(const NamedDecl &ND) { + ASTContext &Ctx = ND.getASTContext(); auto &SM = Ctx.getSourceManager(); SourceLocation NameLoc = nameLocation(ND, Ctx.getSourceManager()); SourceLocation BeginLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getBeginLoc())); @@ -1313,54 +1314,92 @@ // correctly. SymbolKind SK = indexSymbolKindToSymbolKind(SymInfo.Kind); - TypeHierarchyItem THI; - THI.name = printName(Ctx, ND); - THI.kind = SK; - THI.deprecated = ND.isDeprecated(); - THI.range = Range{sourceLocToPosition(SM, DeclRange->getBegin()), - sourceLocToPosition(SM, DeclRange->getEnd())}; - THI.selectionRange = Range{NameBegin, NameEnd}; - if (!THI.range.contains(THI.selectionRange)) { + HierarchyItem HI; + HI.name = printName(Ctx, ND); + HI.kind = SK; + // HI.deprecated = ND.isDeprecated(); + HI.range = Range{sourceLocToPosition(SM, DeclRange->getBegin()), + sourceLocToPosition(SM, DeclRange->getEnd())}; + HI.selectionRange = Range{NameBegin, NameEnd}; + if (!HI.range.contains(HI.selectionRange)) { // 'selectionRange' must be contained in 'range', so in cases where clang // reports unrelated ranges we need to reconcile somehow. - THI.range = THI.selectionRange; + HI.range = HI.selectionRange; } - THI.uri = URIForFile::canonicalize(*FilePath, *TUPath); + HI.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)) - THI.data = ID.str(); + HI.data = ID.str(); + + return HI; +} - return THI; +static llvm::Optional<TypeHierarchyItem> +declToTypeHierarchyItem(const NamedDecl &ND) { + auto Result = declToHierarchyItem<TypeHierarchyItem>(ND); + if (Result) { + Result->deprecated = ND.isDeprecated(); + } + return Result; } -static Optional<TypeHierarchyItem> -symbolToTypeHierarchyItem(const Symbol &S, const SymbolIndex *Index, - PathRef TUPath) { +static llvm::Optional<CallHierarchyItem> +declToCallHierarchyItem(const NamedDecl &ND) { + auto Result = declToHierarchyItem<CallHierarchyItem>(ND); + if (Result) { + if (ND.isDeprecated()) + Result->tags.push_back(SymbolTag::Deprecated); + } + return Result; +} + +template <typename HierarchyItem> +static Optional<HierarchyItem> symbolToHierarchyItem(const Symbol &S, + PathRef TUPath) { auto Loc = symbolToLocation(S, TUPath); if (!Loc) { - log("Type hierarchy: {0}", Loc.takeError()); + elog("Failed to convert symbol to hierarchy item: {0}", Loc.takeError()); return llvm::None; } - TypeHierarchyItem THI; - THI.name = std::string(S.Name); - THI.kind = indexSymbolKindToSymbolKind(S.SymInfo.Kind); - THI.deprecated = (S.Flags & Symbol::Deprecated); - THI.selectionRange = Loc->range; + HierarchyItem HI; + HI.name = std::string(S.Name); + HI.kind = indexSymbolKindToSymbolKind(S.SymInfo.Kind); + HI.selectionRange = Loc->range; // FIXME: Populate 'range' correctly // (https://github.com/clangd/clangd/issues/59). - THI.range = THI.selectionRange; - THI.uri = Loc->uri; + HI.range = HI.selectionRange; + HI.uri = Loc->uri; // Store the SymbolID in the 'data' field. The client will - // send this back in typeHierarchy/resolve, allowing us to - // continue resolving additional levels of the type hierarchy. - THI.data = S.ID.str(); + // send this back in requests to resolve additional levels + // of the hierarchy. + HI.data = S.ID.str(); + + return std::move(HI); +} - return std::move(THI); +static Optional<TypeHierarchyItem> symbolToTypeHierarchyItem(const Symbol &S, + PathRef TUPath) { + auto Result = symbolToHierarchyItem<TypeHierarchyItem>(S, TUPath); + if (Result) { + Result->deprecated = (S.Flags & Symbol::Deprecated); + } + return Result; +} + +static Optional<CallHierarchyItem> symbolToCallHierarchyItem(const Symbol &S, + PathRef TUPath) { + auto Result = symbolToHierarchyItem<CallHierarchyItem>(S, TUPath); + if (Result) { + if (S.Flags & Symbol::Deprecated) { + Result->tags.push_back(SymbolTag::Deprecated); + } + } + return Result; } static void fillSubTypes(const SymbolID &ID, @@ -1371,7 +1410,7 @@ Req.Predicate = RelationKind::BaseOf; Index->relations(Req, [&](const SymbolID &Subject, const Symbol &Object) { if (Optional<TypeHierarchyItem> ChildSym = - symbolToTypeHierarchyItem(Object, Index, TUPath)) { + symbolToTypeHierarchyItem(Object, TUPath)) { if (Levels > 1) { ChildSym->children.emplace(); fillSubTypes(Object.ID, *ChildSym->children, Index, Levels - 1, TUPath); @@ -1400,7 +1439,7 @@ for (const CXXRecordDecl *ParentDecl : typeParents(&CXXRD)) { if (Optional<TypeHierarchyItem> ParentSym = - declToTypeHierarchyItem(ASTCtx, *ParentDecl)) { + declToTypeHierarchyItem(*ParentDecl)) { ParentSym->parents.emplace(); fillSuperTypes(*ParentDecl, ASTCtx, *ParentSym->parents, RPSet); SuperTypes.emplace_back(std::move(*ParentSym)); @@ -1522,8 +1561,7 @@ CXXRD = CTSD->getTemplateInstantiationPattern(); } - Optional<TypeHierarchyItem> Result = - declToTypeHierarchyItem(AST.getASTContext(), *CXXRD); + Optional<TypeHierarchyItem> Result = declToTypeHierarchyItem(*CXXRD); if (!Result) return Result; @@ -1565,6 +1603,91 @@ } } +llvm::Optional<std::vector<CallHierarchyItem>> +prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath) { + const auto &SM = AST.getSourceManager(); + auto Loc = sourceLocationInMainFile(SM, Pos); + if (!Loc) { + elog("prepareCallHierarchy failed to convert position to source location: " + "{0}", + Loc.takeError()); + return llvm::None; + } + std::vector<CallHierarchyItem> Result; + for (const NamedDecl *Decl : getDeclAtPosition(AST, *Loc, {})) { + if (!Decl->isFunctionOrFunctionTemplate()) + continue; + if (auto CHI = declToCallHierarchyItem(*Decl)) + Result.push_back(*std::move(CHI)); + } + return Result; +} + +llvm::Optional<std::vector<CallHierarchyIncomingCall>> +incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) { + if (!Index || Item.data.empty()) + return llvm::None; + Expected<SymbolID> ID = SymbolID::fromStr(Item.data); + if (!ID) { + elog("incomingCalls failed to find symbol: {0}", ID.takeError()); + return llvm::None; + } + // In this function, we find incoming calls based on the index only. + // In principle, the AST could have more up-to-date information about + // occurrences within the current file. However, going from a SymbolID + // to an AST node isn't cheap, particularly when the declaration isn't + // in the main file. + // FIXME: Consider also using AST information when feasible. + RefsRequest Request; + Request.IDs.insert(*ID); + // We could restrict more specifically to calls by introducing a new RefKind, + // but non-call references (such as address-of-function) can still be + // interesting as they can indicate indirect calls. + 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<SymbolID, CallHierarchyIncomingCall> ResultMap; + // We can populate the keys of ResultMap, and the FromRanges fields of + // its values, based on a refs request only. As we do so, we also accumulate + // the container IDs into a lookup request. + LookupRequest ContainerLookup; + Index->refs(Request, [&](const Ref &R) { + auto Loc = indexToLSPLocation(R.Location, Item.uri.file()); + if (!Loc) { + elog("incomingCalls failed to convert location: {0}", Loc.takeError()); + return; + } + auto It = ResultMap.find(R.Container); + if (It == ResultMap.end()) + It = ResultMap.insert({R.Container, CallHierarchyIncomingCall{}}).first; + It->second.FromRanges.push_back(Loc->range); + + ContainerLookup.IDs.insert(R.Container); + }); + // Perform the lookup request and use its result to fill the From fields + // of the values in ResultMap. + Index->lookup(ContainerLookup, [&](const Symbol &Caller) { + auto It = ResultMap.find(Caller.ID); + if (It != ResultMap.end()) { + if (auto CHI = symbolToCallHierarchyItem(Caller, Item.uri.file())) + It->second.From = std::move(*CHI); + } + }); + // Finally, flatten the results into a vector. + std::vector<CallHierarchyIncomingCall> Results; + for (auto &&Entry : ResultMap) { + Results.push_back(std::move(Entry.second)); + } + return Results; +} + +llvm::Optional<std::vector<CallHierarchyOutgoingCall>> +outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) { + // TODO: Implement. + return llvm::None; +} + llvm::DenseSet<const Decl *> getNonLocalDeclRefs(ParsedAST &AST, const FunctionDecl *FD) { if (!FD->hasBody())
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits