diff options
Diffstat (limited to 'clangd/unittests')
55 files changed, 17195 insertions, 0 deletions
diff --git a/clangd/unittests/Annotations.cpp b/clangd/unittests/Annotations.cpp new file mode 100644 index 00000000..edb0ea9a --- /dev/null +++ b/clangd/unittests/Annotations.cpp @@ -0,0 +1,53 @@ +//===--- Annotations.cpp - Annotated source code for unit tests --*- 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 "SourceCode.h" + +namespace clang { +namespace clangd { + +Position Annotations::point(llvm::StringRef Name) const { + return offsetToPosition(code(), Base::point(Name)); +} + +std::vector<Position> Annotations::points(llvm::StringRef Name) const { + auto Offsets = Base::points(Name); + + std::vector<Position> Ps; + Ps.reserve(Offsets.size()); + for (size_t O : Offsets) + Ps.push_back(offsetToPosition(code(), O)); + + return Ps; +} + +static clangd::Range toLSPRange(llvm::StringRef Code, Annotations::Range R) { + clangd::Range LSPRange; + LSPRange.start = offsetToPosition(Code, R.Begin); + LSPRange.end = offsetToPosition(Code, R.End); + return LSPRange; +} + +clangd::Range Annotations::range(llvm::StringRef Name) const { + return toLSPRange(code(), Base::range(Name)); +} + +std::vector<clangd::Range> Annotations::ranges(llvm::StringRef Name) const { + auto OffsetRanges = Base::ranges(Name); + + std::vector<clangd::Range> Rs; + Rs.reserve(OffsetRanges.size()); + for (Annotations::Range R : OffsetRanges) + Rs.push_back(toLSPRange(code(), R)); + + return Rs; +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/Annotations.h b/clangd/unittests/Annotations.h new file mode 100644 index 00000000..846c36a5 --- /dev/null +++ b/clangd/unittests/Annotations.h @@ -0,0 +1,39 @@ +//===--- Annotations.h - Annotated source code for tests ---------*- 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 +// +//===----------------------------------------------------------------------===// +// A clangd-specific version of llvm/Testing/Support/Annotations.h, replaces +// offsets and offset-based ranges with types from the LSP protocol. +//===---------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H + +#include "Protocol.h" +#include "llvm/Testing/Support/Annotations.h" + +namespace clang { +namespace clangd { + +/// Same as llvm::Annotations, but adjusts functions to LSP-specific types for +/// positions and ranges. +class Annotations : public llvm::Annotations { + using Base = llvm::Annotations; + +public: + using llvm::Annotations::Annotations; + + Position point(llvm::StringRef Name = "") const; + std::vector<Position> points(llvm::StringRef Name = "") const; + + clangd::Range range(llvm::StringRef Name = "") const; + std::vector<clangd::Range> ranges(llvm::StringRef Name = "") const; +}; + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_ANNOTATIONS_H diff --git a/clangd/unittests/BackgroundIndexTests.cpp b/clangd/unittests/BackgroundIndexTests.cpp new file mode 100644 index 00000000..86f87009 --- /dev/null +++ b/clangd/unittests/BackgroundIndexTests.cpp @@ -0,0 +1,465 @@ +#include "SyncAPI.h" +#include "TestFS.h" +#include "index/Background.h" +#include "llvm/Support/ScopedPrinter.h" +#include "llvm/Support/Threading.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <thread> + +using ::testing::_; +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::Not; +using ::testing::UnorderedElementsAre; + +namespace clang { +namespace clangd { + +MATCHER_P(Named, N, "") { return arg.Name == N; } +MATCHER(Declared, "") { + return !StringRef(arg.CanonicalDeclaration.FileURI).empty(); +} +MATCHER(Defined, "") { return !StringRef(arg.Definition.FileURI).empty(); } +MATCHER_P(FileURI, F, "") { return StringRef(arg.Location.FileURI) == F; } +::testing::Matcher<const RefSlab &> +RefsAre(std::vector<::testing::Matcher<Ref>> Matchers) { + return ElementsAre(::testing::Pair(_, UnorderedElementsAreArray(Matchers))); +} +// URI cannot be empty since it references keys in the IncludeGraph. +MATCHER(EmptyIncludeNode, "") { + return !arg.IsTU && !arg.URI.empty() && arg.Digest == FileDigest{{0}} && + arg.DirectIncludes.empty(); +} + +class MemoryShardStorage : public BackgroundIndexStorage { + mutable std::mutex StorageMu; + llvm::StringMap<std::string> &Storage; + size_t &CacheHits; + +public: + MemoryShardStorage(llvm::StringMap<std::string> &Storage, size_t &CacheHits) + : Storage(Storage), CacheHits(CacheHits) {} + llvm::Error storeShard(llvm::StringRef ShardIdentifier, + IndexFileOut Shard) const override { + std::lock_guard<std::mutex> Lock(StorageMu); + AccessedPaths.insert(ShardIdentifier); + Storage[ShardIdentifier] = llvm::to_string(Shard); + return llvm::Error::success(); + } + std::unique_ptr<IndexFileIn> + loadShard(llvm::StringRef ShardIdentifier) const override { + std::lock_guard<std::mutex> Lock(StorageMu); + AccessedPaths.insert(ShardIdentifier); + if (Storage.find(ShardIdentifier) == Storage.end()) { + return nullptr; + } + auto IndexFile = readIndexFile(Storage[ShardIdentifier]); + if (!IndexFile) { + ADD_FAILURE() << "Error while reading " << ShardIdentifier << ':' + << IndexFile.takeError(); + return nullptr; + } + CacheHits++; + return llvm::make_unique<IndexFileIn>(std::move(*IndexFile)); + } + + mutable llvm::StringSet<> AccessedPaths; +}; + +class BackgroundIndexTest : public ::testing::Test { +protected: + BackgroundIndexTest() { BackgroundIndex::preventThreadStarvationInTests(); } +}; + +TEST_F(BackgroundIndexTest, NoCrashOnErrorFile) { + MockFSProvider FS; + FS.Files[testPath("root/A.cc")] = "error file"; + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); +} + +TEST_F(BackgroundIndexTest, IndexTwoFiles) { + MockFSProvider FS; + // a.h yields different symbols when included by A.cc vs B.cc. + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + #if A + class A_CC {}; + #else + class B_CC{}; + #endif + )cpp"; + FS.Files[testPath("root/A.cc")] = + "#include \"A.h\"\nvoid g() { (void)common; }"; + FS.Files[testPath("root/B.cc")] = + R"cpp( + #define A 0 + #include "A.h" + void f_b() { + (void)common; + })cpp"; + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + EXPECT_THAT( + runFuzzyFind(Idx, ""), + UnorderedElementsAre(Named("common"), Named("A_CC"), Named("g"), + AllOf(Named("f_b"), Declared(), Not(Defined())))); + + Cmd.Filename = testPath("root/B.cc"); + Cmd.CommandLine = {"clang++", Cmd.Filename}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + // B_CC is dropped as we don't collect symbols from A.h in this compilation. + EXPECT_THAT(runFuzzyFind(Idx, ""), + UnorderedElementsAre(Named("common"), Named("A_CC"), Named("g"), + AllOf(Named("f_b"), Declared(), Defined()))); + + auto Syms = runFuzzyFind(Idx, "common"); + EXPECT_THAT(Syms, UnorderedElementsAre(Named("common"))); + auto Common = *Syms.begin(); + EXPECT_THAT(getRefs(Idx, Common.ID), + RefsAre({FileURI("unittest:///root/A.h"), + FileURI("unittest:///root/A.cc"), + FileURI("unittest:///root/B.cc")})); +} + +TEST_F(BackgroundIndexTest, ShardStorageTest) { + MockFSProvider FS; + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + )cpp"; + std::string A_CC = "#include \"A.h\"\nvoid g() { (void)common; }"; + FS.Files[testPath("root/A.cc")] = A_CC; + + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + // Check nothing is loaded from Storage, but A.cc and A.h has been stored. + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 0U); + EXPECT_EQ(Storage.size(), 2U); + + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache. + EXPECT_EQ(Storage.size(), 2U); + + auto ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT( + *ShardHeader->Symbols, + UnorderedElementsAre(Named("common"), Named("A_CC"), + AllOf(Named("f_b"), Declared(), Not(Defined())))); + for (const auto &Ref : *ShardHeader->Refs) + EXPECT_THAT(Ref.second, + UnorderedElementsAre(FileURI("unittest:///root/A.h"))); + + auto ShardSource = MSS.loadShard(testPath("root/A.cc")); + EXPECT_NE(ShardSource, nullptr); + EXPECT_THAT(*ShardSource->Symbols, UnorderedElementsAre(Named("g"))); + EXPECT_THAT(*ShardSource->Refs, RefsAre({FileURI("unittest:///root/A.cc")})); +} + +TEST_F(BackgroundIndexTest, DirectIncludesTest) { + MockFSProvider FS; + FS.Files[testPath("root/B.h")] = ""; + FS.Files[testPath("root/A.h")] = R"cpp( + #include "B.h" + void common(); + void f_b(); + class A_CC {}; + )cpp"; + std::string A_CC = "#include \"A.h\"\nvoid g() { (void)common; }"; + FS.Files[testPath("root/A.cc")] = A_CC; + + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + + auto ShardSource = MSS.loadShard(testPath("root/A.cc")); + EXPECT_TRUE(ShardSource->Sources); + EXPECT_EQ(ShardSource->Sources->size(), 2U); // A.cc, A.h + EXPECT_THAT( + ShardSource->Sources->lookup("unittest:///root/A.cc").DirectIncludes, + UnorderedElementsAre("unittest:///root/A.h")); + EXPECT_NE(ShardSource->Sources->lookup("unittest:///root/A.cc").Digest, + FileDigest{{0}}); + EXPECT_THAT(ShardSource->Sources->lookup("unittest:///root/A.h"), + EmptyIncludeNode()); + + auto ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_TRUE(ShardHeader->Sources); + EXPECT_EQ(ShardHeader->Sources->size(), 2U); // A.h, B.h + EXPECT_THAT( + ShardHeader->Sources->lookup("unittest:///root/A.h").DirectIncludes, + UnorderedElementsAre("unittest:///root/B.h")); + EXPECT_NE(ShardHeader->Sources->lookup("unittest:///root/A.h").Digest, + FileDigest{{0}}); + EXPECT_THAT(ShardHeader->Sources->lookup("unittest:///root/B.h"), + EmptyIncludeNode()); +} + +// FIXME: figure out the right timeouts or rewrite to not use the timeouts and +// re-enable. +TEST_F(BackgroundIndexTest, DISABLED_PeriodicalIndex) { + MockFSProvider FS; + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx( + Context::empty(), FS, CDB, [&](llvm::StringRef) { return &MSS; }, + /*BuildIndexPeriodMs=*/500); + + FS.Files[testPath("root/A.cc")] = "#include \"A.h\""; + + tooling::CompileCommand Cmd; + FS.Files[testPath("root/A.h")] = "class X {};"; + Cmd.Filename = testPath("root/A.cc"); + Cmd.CommandLine = {"clang++", Cmd.Filename}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre()); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre(Named("X"))); + + FS.Files[testPath("root/A.h")] = "class Y {};"; + FS.Files[testPath("root/A.cc")] += " "; // Force reindex the file. + Cmd.CommandLine = {"clang++", "-DA=1", testPath("root/A.cc")}; + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre(Named("X"))); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + EXPECT_THAT(runFuzzyFind(Idx, ""), ElementsAre(Named("Y"))); +} + +TEST_F(BackgroundIndexTest, ShardStorageLoad) { + MockFSProvider FS; + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + )cpp"; + FS.Files[testPath("root/A.cc")] = + "#include \"A.h\"\nvoid g() { (void)common; }"; + + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + // Check nothing is loaded from Storage, but A.cc and A.h has been stored. + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + + // Change header. + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + class A_CCnew {}; + )cpp"; + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache. + + // Check if the new symbol has arrived. + auto ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT(*ShardHeader->Symbols, Contains(Named("A_CCnew"))); + + // Change source. + FS.Files[testPath("root/A.cc")] = + "#include \"A.h\"\nvoid g() { (void)common; }\nvoid f_b() {}"; + { + CacheHits = 0; + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 2U); // Check both A.cc and A.h loaded from cache. + + // Check if the new symbol has arrived. + ShardHeader = MSS.loadShard(testPath("root/A.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT(*ShardHeader->Symbols, Contains(Named("A_CCnew"))); + auto ShardSource = MSS.loadShard(testPath("root/A.cc")); + EXPECT_NE(ShardSource, nullptr); + EXPECT_THAT(*ShardSource->Symbols, + Contains(AllOf(Named("f_b"), Declared(), Defined()))); +} + +TEST_F(BackgroundIndexTest, ShardStorageEmptyFile) { + MockFSProvider FS; + FS.Files[testPath("root/A.h")] = R"cpp( + void common(); + void f_b(); + class A_CC {}; + )cpp"; + FS.Files[testPath("root/B.h")] = R"cpp( + #include "A.h" + )cpp"; + FS.Files[testPath("root/A.cc")] = + "#include \"B.h\"\nvoid g() { (void)common; }"; + + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + + tooling::CompileCommand Cmd; + Cmd.Filename = testPath("root/A.cc"); + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", testPath("root/A.cc")}; + // Check that A.cc, A.h and B.h has been stored. + { + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_THAT(Storage.keys(), + UnorderedElementsAre(testPath("root/A.cc"), testPath("root/A.h"), + testPath("root/B.h"))); + auto ShardHeader = MSS.loadShard(testPath("root/B.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_TRUE(ShardHeader->Symbols->empty()); + + // Check that A.cc, A.h and B.h has been loaded. + { + CacheHits = 0; + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 3U); + + // Update B.h to contain some symbols. + FS.Files[testPath("root/B.h")] = R"cpp( + #include "A.h" + void new_func(); + )cpp"; + // Check that B.h has been stored with new contents. + { + CacheHits = 0; + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + CDB.setCompileCommand(testPath("root/A.cc"), Cmd); + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + } + EXPECT_EQ(CacheHits, 3U); + ShardHeader = MSS.loadShard(testPath("root/B.h")); + EXPECT_NE(ShardHeader, nullptr); + EXPECT_THAT(*ShardHeader->Symbols, + Contains(AllOf(Named("new_func"), Declared(), Not(Defined())))); +} + +TEST_F(BackgroundIndexTest, NoDotsInAbsPath) { + MockFSProvider FS; + llvm::StringMap<std::string> Storage; + size_t CacheHits = 0; + MemoryShardStorage MSS(Storage, CacheHits); + OverlayCDB CDB(/*Base=*/nullptr); + BackgroundIndex Idx(Context::empty(), FS, CDB, + [&](llvm::StringRef) { return &MSS; }); + + tooling::CompileCommand Cmd; + FS.Files[testPath("root/A.cc")] = ""; + Cmd.Filename = "../A.cc"; + Cmd.Directory = testPath("root/build"); + Cmd.CommandLine = {"clang++", "../A.cc"}; + CDB.setCompileCommand(testPath("root/build/../A.cc"), Cmd); + + FS.Files[testPath("root/B.cc")] = ""; + Cmd.Filename = "./B.cc"; + Cmd.Directory = testPath("root"); + Cmd.CommandLine = {"clang++", "./B.cc"}; + CDB.setCompileCommand(testPath("root/./B.cc"), Cmd); + + ASSERT_TRUE(Idx.blockUntilIdleForTest()); + for (llvm::StringRef AbsPath : MSS.AccessedPaths.keys()) { + EXPECT_FALSE(AbsPath.contains("./")) << AbsPath; + EXPECT_FALSE(AbsPath.contains("../")) << AbsPath; + } +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/CMakeLists.txt b/clangd/unittests/CMakeLists.txt new file mode 100644 index 00000000..96b1d360 --- /dev/null +++ b/clangd/unittests/CMakeLists.txt @@ -0,0 +1,97 @@ +set(LLVM_LINK_COMPONENTS + support + ) + +get_filename_component(CLANGD_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/../../clangd REALPATH) +get_filename_component(CLANGD_BINARY_DIR + ${CMAKE_CURRENT_BINARY_DIR}/../../clangd REALPATH) +include_directories( + ${CLANGD_SOURCE_DIR} + ${CLANGD_BINARY_DIR} + ) + +if(CLANG_BUILT_STANDALONE) + # LLVMTestingSupport library is needed for clangd tests. + if (EXISTS ${LLVM_MAIN_SRC_DIR}/lib/Testing/Support + AND NOT TARGET LLVMTestingSupport) + add_subdirectory(${LLVM_MAIN_SRC_DIR}/lib/Testing/Support + lib/Testing/Support) + endif() +endif() + +add_custom_target(ClangdUnitTests) +add_unittest(ClangdUnitTests ClangdTests + Annotations.cpp + BackgroundIndexTests.cpp + CancellationTests.cpp + CanonicalIncludesTests.cpp + ClangdTests.cpp + ClangdUnitTests.cpp + CodeCompleteTests.cpp + CodeCompletionStringsTests.cpp + ContextTests.cpp + DexTests.cpp + DiagnosticsTests.cpp + DraftStoreTests.cpp + ExpectedTypeTest.cpp + FileDistanceTests.cpp + FileIndexTests.cpp + FindSymbolsTests.cpp + FSTests.cpp + FunctionTests.cpp + FuzzyMatchTests.cpp + GlobalCompilationDatabaseTests.cpp + HeadersTests.cpp + IndexActionTests.cpp + IndexTests.cpp + JSONTransportTests.cpp + PrintASTTests.cpp + QualityTests.cpp + RIFFTests.cpp + SelectionTests.cpp + SerializationTests.cpp + SourceCodeTests.cpp + SymbolCollectorTests.cpp + SymbolInfoTests.cpp + SyncAPI.cpp + TUSchedulerTests.cpp + TestFS.cpp + TestIndex.cpp + TestTU.cpp + ThreadingTests.cpp + TraceTests.cpp + TypeHierarchyTests.cpp + TweakTests.cpp + URITests.cpp + XRefsTests.cpp + + $<TARGET_OBJECTS:obj.clangDaemonTweaks> + ) + +target_link_libraries(ClangdTests + PRIVATE + clangAST + clangBasic + clangDaemon + clangFormat + clangFrontend + clangIndex + clangLex + clangSema + clangSerialization + clangTidy + clangTooling + clangToolingCore + clangToolingInclusions + LLVMSupport + LLVMTestingSupport + ) + +if (CLANGD_BUILD_XPC) + add_subdirectory(xpc) +endif () + +configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in + ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py) diff --git a/clangd/unittests/CancellationTests.cpp b/clangd/unittests/CancellationTests.cpp new file mode 100644 index 00000000..611ce07d --- /dev/null +++ b/clangd/unittests/CancellationTests.cpp @@ -0,0 +1,65 @@ +#include "Cancellation.h" +#include "Context.h" +#include "Threading.h" +#include "llvm/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <atomic> +#include <memory> +#include <thread> + +namespace clang { +namespace clangd { +namespace { + +TEST(CancellationTest, CancellationTest) { + auto Task = cancelableTask(); + WithContext ContextWithCancellation(std::move(Task.first)); + EXPECT_FALSE(isCancelled()); + Task.second(); + EXPECT_TRUE(isCancelled()); +} + +TEST(CancellationTest, CancelerDiesContextLives) { + llvm::Optional<WithContext> ContextWithCancellation; + { + auto Task = cancelableTask(); + ContextWithCancellation.emplace(std::move(Task.first)); + EXPECT_FALSE(isCancelled()); + Task.second(); + EXPECT_TRUE(isCancelled()); + } + EXPECT_TRUE(isCancelled()); +} + +TEST(CancellationTest, TaskContextDiesHandleLives) { + auto Task = cancelableTask(); + { + WithContext ContextWithCancellation(std::move(Task.first)); + EXPECT_FALSE(isCancelled()); + Task.second(); + EXPECT_TRUE(isCancelled()); + } + // Still should be able to cancel without any problems. + Task.second(); +} + +TEST(CancellationTest, AsynCancellationTest) { + std::atomic<bool> HasCancelled(false); + Notification Cancelled; + auto TaskToBeCancelled = [&](Context Ctx) { + WithContext ContextGuard(std::move(Ctx)); + Cancelled.wait(); + HasCancelled = isCancelled(); + }; + auto Task = cancelableTask(); + std::thread AsyncTask(TaskToBeCancelled, std::move(Task.first)); + Task.second(); + Cancelled.notify(); + AsyncTask.join(); + + EXPECT_TRUE(HasCancelled); +} +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/CanonicalIncludesTests.cpp b/clangd/unittests/CanonicalIncludesTests.cpp new file mode 100644 index 00000000..9fd78841 --- /dev/null +++ b/clangd/unittests/CanonicalIncludesTests.cpp @@ -0,0 +1,62 @@ +//===-- CanonicalIncludesTests.cpp - --------------------------------------===// +// +// 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 "index/CanonicalIncludes.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(CanonicalIncludesTest, CXXStandardLibrary) { + CanonicalIncludes CI; + addSystemHeadersMapping(&CI); + + // Usual standard library symbols are mapped correctly. + EXPECT_EQ("<vector>", CI.mapHeader("path/vector.h", "std::vector")); + // std::move is ambiguous, currently mapped only based on path + EXPECT_EQ("<utility>", CI.mapHeader("libstdc++/bits/move.h", "std::move")); + EXPECT_EQ("path/utility.h", CI.mapHeader("path/utility.h", "std::move")); + // Unknown std symbols aren't mapped. + EXPECT_EQ("foo/bar.h", CI.mapHeader("foo/bar.h", "std::notathing")); + // iosfwd declares some symbols it doesn't own. + EXPECT_EQ("<ostream>", CI.mapHeader("iosfwd", "std::ostream")); + // And (for now) we assume it owns the others. + EXPECT_EQ("<iosfwd>", CI.mapHeader("iosfwd", "std::notwathing")); +} + +TEST(CanonicalIncludesTest, PathMapping) { + // As used for IWYU pragmas. + CanonicalIncludes CI; + CI.addMapping("foo/bar", "<baz>"); + + EXPECT_EQ("<baz>", CI.mapHeader("foo/bar", "some::symbol")); + EXPECT_EQ("bar/bar", CI.mapHeader("bar/bar", "some::symbol")); +} + +TEST(CanonicalIncludesTest, SymbolMapping) { + // As used for standard library. + CanonicalIncludes CI; + CI.addSymbolMapping("some::symbol", "<baz>"); + + EXPECT_EQ("<baz>", CI.mapHeader("foo/bar", "some::symbol")); + EXPECT_EQ("foo/bar", CI.mapHeader("foo/bar", "other::symbol")); +} + +TEST(CanonicalIncludesTest, Precedence) { + CanonicalIncludes CI; + CI.addMapping("some/path", "<path>"); + CI.addSymbolMapping("some::symbol", "<symbol>"); + + // Symbol mapping beats path mapping. + EXPECT_EQ("<symbol>", CI.mapHeader("some/path", "some::symbol")); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/ClangdTests.cpp b/clangd/unittests/ClangdTests.cpp new file mode 100644 index 00000000..5d98bdc2 --- /dev/null +++ b/clangd/unittests/ClangdTests.cpp @@ -0,0 +1,1162 @@ +//===-- ClangdTests.cpp - Clangd unit tests ---------------------*- 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 "ClangdLSPServer.h" +#include "ClangdServer.h" +#include "GlobalCompilationDatabase.h" +#include "Matchers.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "Threading.h" +#include "URI.h" +#include "clang/Config/config.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Regex.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <algorithm> +#include <chrono> +#include <iostream> +#include <random> +#include <string> +#include <thread> +#include <vector> + +namespace clang { +namespace clangd { + +namespace { + +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::Gt; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +MATCHER_P2(DeclAt, File, Range, "") { + return arg.PreferredDeclaration == + Location{URIForFile::canonicalize(File, testRoot()), Range}; +} + +bool diagsContainErrors(const std::vector<Diag> &Diagnostics) { + for (auto D : Diagnostics) { + if (D.Severity == DiagnosticsEngine::Error || + D.Severity == DiagnosticsEngine::Fatal) + return true; + } + return false; +} + +class ErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + bool HadError = diagsContainErrors(Diagnostics); + std::lock_guard<std::mutex> Lock(Mutex); + HadErrorInLastDiags = HadError; + } + + bool hadErrorInLastDiags() { + std::lock_guard<std::mutex> Lock(Mutex); + return HadErrorInLastDiags; + } + +private: + std::mutex Mutex; + bool HadErrorInLastDiags = false; +}; + +/// For each file, record whether the last published diagnostics contained at +/// least one error. +class MultipleErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + bool HadError = diagsContainErrors(Diagnostics); + + std::lock_guard<std::mutex> Lock(Mutex); + LastDiagsHadError[File] = HadError; + } + + /// Exposes all files consumed by onDiagnosticsReady in an unspecified order. + /// For each file, a bool value indicates whether the last diagnostics + /// contained an error. + std::vector<std::pair<Path, bool>> filesWithDiags() const { + std::vector<std::pair<Path, bool>> Result; + std::lock_guard<std::mutex> Lock(Mutex); + + for (const auto &it : LastDiagsHadError) { + Result.emplace_back(it.first(), it.second); + } + + return Result; + } + + void clear() { + std::lock_guard<std::mutex> Lock(Mutex); + LastDiagsHadError.clear(); + } + +private: + mutable std::mutex Mutex; + llvm::StringMap<bool> LastDiagsHadError; +}; + +/// Replaces all patterns of the form 0x123abc with spaces +std::string replacePtrsInDump(std::string const &Dump) { + llvm::Regex RE("0x[0-9a-fA-F]+"); + llvm::SmallVector<llvm::StringRef, 1> Matches; + llvm::StringRef Pending = Dump; + + std::string Result; + while (RE.match(Pending, &Matches)) { + assert(Matches.size() == 1 && "Exactly one match expected"); + auto MatchPos = Matches[0].data() - Pending.data(); + + Result += Pending.take_front(MatchPos); + Pending = Pending.drop_front(MatchPos + Matches[0].size()); + } + Result += Pending; + + return Result; +} + +std::string dumpASTWithoutMemoryLocs(ClangdServer &Server, PathRef File) { + auto DumpWithMemLocs = runDumpAST(Server, File); + return replacePtrsInDump(DumpWithMemLocs); +} + +class ClangdVFSTest : public ::testing::Test { +protected: + std::string parseSourceAndDumpAST( + PathRef SourceFileRelPath, llvm::StringRef SourceContents, + std::vector<std::pair<PathRef, llvm::StringRef>> ExtraFiles = {}, + bool ExpectErrors = false) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + for (const auto &FileWithContents : ExtraFiles) + FS.Files[testPath(FileWithContents.first)] = FileWithContents.second; + + auto SourceFilename = testPath(SourceFileRelPath); + Server.addDocument(SourceFilename, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, SourceFilename); + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_EQ(ExpectErrors, DiagConsumer.hadErrorInLastDiags()); + return Result; + } +}; + +TEST_F(ClangdVFSTest, Parse) { + // FIXME: figure out a stable format for AST dumps, so that we can check the + // output of the dump itself is equal to the expected one, not just that it's + // different. + auto Empty = parseSourceAndDumpAST("foo.cpp", "", {}); + auto OneDecl = parseSourceAndDumpAST("foo.cpp", "int a;", {}); + auto SomeDecls = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;", {}); + EXPECT_NE(Empty, OneDecl); + EXPECT_NE(Empty, SomeDecls); + EXPECT_NE(SomeDecls, OneDecl); + + auto Empty2 = parseSourceAndDumpAST("foo.cpp", ""); + auto OneDecl2 = parseSourceAndDumpAST("foo.cpp", "int a;"); + auto SomeDecls2 = parseSourceAndDumpAST("foo.cpp", "int a; int b; int c;"); + EXPECT_EQ(Empty, Empty2); + EXPECT_EQ(OneDecl, OneDecl2); + EXPECT_EQ(SomeDecls, SomeDecls2); +} + +TEST_F(ClangdVFSTest, ParseWithHeader) { + parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {}, + /*ExpectErrors=*/true); + parseSourceAndDumpAST("foo.cpp", "#include \"foo.h\"", {{"foo.h", ""}}, + /*ExpectErrors=*/false); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", ""}}, + /*ExpectErrors=*/true); + parseSourceAndDumpAST("foo.cpp", SourceContents, {{"foo.h", "int a;"}}, + /*ExpectErrors=*/false); +} + +TEST_F(ClangdVFSTest, Reparse) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + + FS.Files[testPath("foo.h")] = "int a;"; + FS.Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + Server.addDocument(FooCpp, ""); + auto DumpParseEmpty = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseEmpty); +} + +TEST_F(ClangdVFSTest, ReparseOnHeaderChange) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + + FS.Files[FooH] = "int a;"; + FS.Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + FS.Files[FooH] = ""; + Server.addDocument(FooCpp, SourceContents); + auto DumpParseDifferent = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + FS.Files[FooH] = "int a;"; + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseDifferent); +} + +TEST_F(ClangdVFSTest, PropagatesContexts) { + static Key<int> Secret; + struct FSProvider : public FileSystemProvider { + IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override { + Got = Context::current().getExisting(Secret); + return buildTestFS({}); + } + mutable int Got; + } FS; + struct DiagConsumer : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + Got = Context::current().getExisting(Secret); + } + int Got; + } DiagConsumer; + MockCompilationDatabase CDB; + + // Verify that the context is plumbed to the FS provider and diagnostics. + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + { + WithContextValue Entrypoint(Secret, 42); + Server.addDocument(testPath("foo.cpp"), "void main(){}"); + } + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_EQ(FS.Got, 42); + EXPECT_EQ(DiagConsumer.Got, 42); +} + +// Only enable this test on Unix +#ifdef LLVM_ON_UNIX +TEST_F(ClangdVFSTest, SearchLibDir) { + // Checks that searches for GCC installation is done through vfs. + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + CDB.ExtraClangFlags.insert(CDB.ExtraClangFlags.end(), + {"-xc++", "-target", "x86_64-linux-unknown", + "-m64", "--gcc-toolchain=/randomusr", + "-stdlib=libstdc++"}); + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Just a random gcc version string + SmallString<8> Version("4.9.3"); + + // A lib dir for gcc installation + SmallString<64> LibDir("/randomusr/lib/gcc/x86_64-linux-gnu"); + llvm::sys::path::append(LibDir, Version); + + // Put crtbegin.o into LibDir/64 to trick clang into thinking there's a gcc + // installation there. + SmallString<64> DummyLibFile; + llvm::sys::path::append(DummyLibFile, LibDir, "64", "crtbegin.o"); + FS.Files[DummyLibFile] = ""; + + SmallString<64> IncludeDir("/randomusr/include/c++"); + llvm::sys::path::append(IncludeDir, Version); + + SmallString<64> StringPath; + llvm::sys::path::append(StringPath, IncludeDir, "string"); + FS.Files[StringPath] = "class mock_string {};"; + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +#include <string> +mock_string x; +)cpp"; + FS.Files[FooCpp] = SourceContents; + + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + + const auto SourceContentsWithError = R"cpp( +#include <string> +std::string x; +)cpp"; + runAddDocument(Server, FooCpp, SourceContentsWithError); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); +} +#endif // LLVM_ON_UNIX + +TEST_F(ClangdVFSTest, ForceReparseCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents1 = R"cpp( +template <class T> +struct foo { T x; }; +)cpp"; + const auto SourceContents2 = R"cpp( +template <class T> +struct bar { T x; }; +)cpp"; + + FS.Files[FooCpp] = ""; + + // First parse files in C mode and check they produce errors. + CDB.ExtraClangFlags = {"-xc"}; + runAddDocument(Server, FooCpp, SourceContents1); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + runAddDocument(Server, FooCpp, SourceContents2); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + // Now switch to C++ mode. + CDB.ExtraClangFlags = {"-xc++"}; + runAddDocument(Server, FooCpp, SourceContents2, WantDiagnostics::Auto); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + // Subsequent addDocument calls should finish without errors too. + runAddDocument(Server, FooCpp, SourceContents1); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + runAddDocument(Server, FooCpp, SourceContents2); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); +} + +TEST_F(ClangdVFSTest, ForceReparseCompileCommandDefines) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +#ifdef WITH_ERROR +this +#endif + +int main() { return 0; } +)cpp"; + FS.Files[FooCpp] = ""; + + // Parse with define, we expect to see the errors. + CDB.ExtraClangFlags = {"-DWITH_ERROR"}; + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_TRUE(DiagConsumer.hadErrorInLastDiags()); + + // Parse without the define, no errors should be produced. + CDB.ExtraClangFlags = {}; + runAddDocument(Server, FooCpp, SourceContents, WantDiagnostics::Auto); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); + // Subsequent addDocument call should finish without errors too. + runAddDocument(Server, FooCpp, SourceContents); + EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); +} + +// Test ClangdServer.reparseOpenedFiles. +TEST_F(ClangdVFSTest, ReparseOpenedFiles) { + Annotations FooSource(R"cpp( +#ifdef MACRO +static void $one[[bob]]() {} +#else +static void $two[[bob]]() {} +#endif + +int main () { bo^b (); return 0; } +)cpp"); + + Annotations BarSource(R"cpp( +#ifdef MACRO +this is an error +#endif +)cpp"); + + Annotations BazSource(R"cpp( +int hello; +)cpp"); + + MockFSProvider FS; + MockCompilationDatabase CDB; + MultipleErrorCheckingDiagConsumer DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + auto BarCpp = testPath("bar.cpp"); + auto BazCpp = testPath("baz.cpp"); + + FS.Files[FooCpp] = ""; + FS.Files[BarCpp] = ""; + FS.Files[BazCpp] = ""; + + CDB.ExtraClangFlags = {"-DMACRO=1"}; + Server.addDocument(FooCpp, FooSource.code()); + Server.addDocument(BarCpp, BarSource.code()); + Server.addDocument(BazCpp, BazSource.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(DiagConsumer.filesWithDiags(), + UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, true), + Pair(BazCpp, false))); + + auto Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point()); + EXPECT_TRUE(bool(Locations)); + EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("one")))); + + // Undefine MACRO, close baz.cpp. + CDB.ExtraClangFlags.clear(); + DiagConsumer.clear(); + Server.removeDocument(BazCpp); + Server.addDocument(FooCpp, FooSource.code(), WantDiagnostics::Auto); + Server.addDocument(BarCpp, BarSource.code(), WantDiagnostics::Auto); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(DiagConsumer.filesWithDiags(), + UnorderedElementsAre(Pair(FooCpp, false), Pair(BarCpp, false))); + + Locations = runLocateSymbolAt(Server, FooCpp, FooSource.point()); + EXPECT_TRUE(bool(Locations)); + EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("two")))); +} + +TEST_F(ClangdVFSTest, MemoryUsage) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Path FooCpp = testPath("foo.cpp"); + const auto SourceContents = R"cpp( +struct Something { + int method(); +}; +)cpp"; + Path BarCpp = testPath("bar.cpp"); + + FS.Files[FooCpp] = ""; + FS.Files[BarCpp] = ""; + + EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty()); + + Server.addDocument(FooCpp, SourceContents); + Server.addDocument(BarCpp, SourceContents); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(Server.getUsedBytesPerFile(), + UnorderedElementsAre(Pair(FooCpp, Gt(0u)), Pair(BarCpp, Gt(0u)))); + + Server.removeDocument(FooCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(Server.getUsedBytesPerFile(), ElementsAre(Pair(BarCpp, Gt(0u)))); + + Server.removeDocument(BarCpp); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty()); +} + +TEST_F(ClangdVFSTest, InvalidCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + // clang cannot create CompilerInvocation if we pass two files in the + // CompileCommand. We pass the file in ExtraFlags once and CDB adds another + // one in getCompileCommand(). + CDB.ExtraClangFlags.push_back(FooCpp); + + // Clang can't parse command args in that case, but we shouldn't crash. + runAddDocument(Server, FooCpp, "int main() {}"); + + EXPECT_EQ(runDumpAST(Server, FooCpp), "<no-ast>"); + EXPECT_ERROR(runLocateSymbolAt(Server, FooCpp, Position())); + EXPECT_ERROR(runFindDocumentHighlights(Server, FooCpp, Position())); + EXPECT_ERROR(runRename(Server, FooCpp, Position(), "new_name")); + // Identifier-based fallback completion. + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Position(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(Field(&CodeCompletion::Name, "int"), + Field(&CodeCompletion::Name, "main"))); + auto SigHelp = runSignatureHelp(Server, FooCpp, Position()); + ASSERT_TRUE(bool(SigHelp)) << "signatureHelp returned an error"; + EXPECT_THAT(SigHelp->signatures, IsEmpty()); +} + +class ClangdThreadingTest : public ClangdVFSTest {}; + +TEST_F(ClangdThreadingTest, StressTest) { + // Without 'static' clang gives an error for a usage inside TestDiagConsumer. + static const unsigned FilesCount = 5; + const unsigned RequestsCount = 500; + // Blocking requests wait for the parsing to complete, they slow down the test + // dramatically, so they are issued rarely. Each + // BlockingRequestInterval-request will be a blocking one. + const unsigned BlockingRequestInterval = 40; + + const auto SourceContentsWithoutErrors = R"cpp( +int a; +int b; +int c; +int d; +)cpp"; + + const auto SourceContentsWithErrors = R"cpp( +int a = x; +int b; +int c; +int d; +)cpp"; + + // Giving invalid line and column number should not crash ClangdServer, but + // just to make sure we're sometimes hitting the bounds inside the file we + // limit the intervals of line and column number that are generated. + unsigned MaxLineForFileRequests = 7; + unsigned MaxColumnForFileRequests = 10; + + std::vector<std::string> FilePaths; + MockFSProvider FS; + for (unsigned I = 0; I < FilesCount; ++I) { + std::string Name = std::string("Foo") + std::to_string(I) + ".cpp"; + FS.Files[Name] = ""; + FilePaths.push_back(testPath(Name)); + } + + struct FileStat { + unsigned HitsWithoutErrors = 0; + unsigned HitsWithErrors = 0; + bool HadErrorsInLastDiags = false; + }; + + class TestDiagConsumer : public DiagnosticsConsumer { + public: + TestDiagConsumer() : Stats(FilesCount, FileStat()) {} + + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override { + StringRef FileIndexStr = llvm::sys::path::stem(File); + ASSERT_TRUE(FileIndexStr.consume_front("Foo")); + + unsigned long FileIndex = std::stoul(FileIndexStr.str()); + + bool HadError = diagsContainErrors(Diagnostics); + + std::lock_guard<std::mutex> Lock(Mutex); + if (HadError) + Stats[FileIndex].HitsWithErrors++; + else + Stats[FileIndex].HitsWithoutErrors++; + Stats[FileIndex].HadErrorsInLastDiags = HadError; + } + + std::vector<FileStat> takeFileStats() { + std::lock_guard<std::mutex> Lock(Mutex); + return std::move(Stats); + } + + private: + std::mutex Mutex; + std::vector<FileStat> Stats; + }; + + struct RequestStats { + unsigned RequestsWithoutErrors = 0; + unsigned RequestsWithErrors = 0; + bool LastContentsHadErrors = false; + bool FileIsRemoved = true; + }; + + std::vector<RequestStats> ReqStats; + ReqStats.reserve(FilesCount); + for (unsigned FileIndex = 0; FileIndex < FilesCount; ++FileIndex) + ReqStats.emplace_back(); + + TestDiagConsumer DiagConsumer; + { + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Prepare some random distributions for the test. + std::random_device RandGen; + + std::uniform_int_distribution<unsigned> FileIndexDist(0, FilesCount - 1); + // Pass a text that contains compiler errors to addDocument in about 20% of + // all requests. + std::bernoulli_distribution ShouldHaveErrorsDist(0.2); + // Line and Column numbers for requests that need them. + std::uniform_int_distribution<int> LineDist(0, MaxLineForFileRequests); + std::uniform_int_distribution<int> ColumnDist(0, MaxColumnForFileRequests); + + // Some helpers. + auto UpdateStatsOnAddDocument = [&](unsigned FileIndex, bool HadErrors) { + auto &Stats = ReqStats[FileIndex]; + + if (HadErrors) + ++Stats.RequestsWithErrors; + else + ++Stats.RequestsWithoutErrors; + Stats.LastContentsHadErrors = HadErrors; + Stats.FileIsRemoved = false; + }; + + auto UpdateStatsOnRemoveDocument = [&](unsigned FileIndex) { + auto &Stats = ReqStats[FileIndex]; + + Stats.FileIsRemoved = true; + }; + + auto AddDocument = [&](unsigned FileIndex, bool SkipCache) { + bool ShouldHaveErrors = ShouldHaveErrorsDist(RandGen); + Server.addDocument(FilePaths[FileIndex], + ShouldHaveErrors ? SourceContentsWithErrors + : SourceContentsWithoutErrors, + WantDiagnostics::Auto); + UpdateStatsOnAddDocument(FileIndex, ShouldHaveErrors); + }; + + // Various requests that we would randomly run. + auto AddDocumentRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + AddDocument(FileIndex, /*SkipCache=*/false); + }; + + auto ForceReparseRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + AddDocument(FileIndex, /*SkipCache=*/true); + }; + + auto RemoveDocumentRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Server.removeDocument(FilePaths[FileIndex]); + UpdateStatsOnRemoveDocument(FileIndex); + }; + + auto CodeCompletionRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Position Pos; + Pos.line = LineDist(RandGen); + Pos.character = ColumnDist(RandGen); + // FIXME(ibiryukov): Also test async completion requests. + // Simply putting CodeCompletion into async requests now would make + // tests slow, since there's no way to cancel previous completion + // requests as opposed to AddDocument/RemoveDocument, which are implicitly + // cancelled by any subsequent AddDocument/RemoveDocument request to the + // same file. + cantFail(runCodeComplete(Server, FilePaths[FileIndex], Pos, + clangd::CodeCompleteOptions())); + }; + + auto LocateSymbolRequest = [&]() { + unsigned FileIndex = FileIndexDist(RandGen); + // Make sure we don't violate the ClangdServer's contract. + if (ReqStats[FileIndex].FileIsRemoved) + AddDocument(FileIndex, /*SkipCache=*/false); + + Position Pos; + Pos.line = LineDist(RandGen); + Pos.character = ColumnDist(RandGen); + + ASSERT_TRUE(!!runLocateSymbolAt(Server, FilePaths[FileIndex], Pos)); + }; + + std::vector<std::function<void()>> AsyncRequests = { + AddDocumentRequest, ForceReparseRequest, RemoveDocumentRequest}; + std::vector<std::function<void()>> BlockingRequests = { + CodeCompletionRequest, LocateSymbolRequest}; + + // Bash requests to ClangdServer in a loop. + std::uniform_int_distribution<int> AsyncRequestIndexDist( + 0, AsyncRequests.size() - 1); + std::uniform_int_distribution<int> BlockingRequestIndexDist( + 0, BlockingRequests.size() - 1); + for (unsigned I = 1; I <= RequestsCount; ++I) { + if (I % BlockingRequestInterval != 0) { + // Issue an async request most of the time. It should be fast. + unsigned RequestIndex = AsyncRequestIndexDist(RandGen); + AsyncRequests[RequestIndex](); + } else { + // Issue a blocking request once in a while. + auto RequestIndex = BlockingRequestIndexDist(RandGen); + BlockingRequests[RequestIndex](); + } + } + ASSERT_TRUE(Server.blockUntilIdleForTest()); + } + + // Check some invariants about the state of the program. + std::vector<FileStat> Stats = DiagConsumer.takeFileStats(); + for (unsigned I = 0; I < FilesCount; ++I) { + if (!ReqStats[I].FileIsRemoved) { + ASSERT_EQ(Stats[I].HadErrorsInLastDiags, + ReqStats[I].LastContentsHadErrors); + } + + ASSERT_LE(Stats[I].HitsWithErrors, ReqStats[I].RequestsWithErrors); + ASSERT_LE(Stats[I].HitsWithoutErrors, ReqStats[I].RequestsWithoutErrors); + } +} + +TEST_F(ClangdVFSTest, CheckSourceHeaderSwitch) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourceContents = R"cpp( + #include "foo.h" + int b = a; + )cpp"; + + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + auto Invalid = testPath("main.cpp"); + + FS.Files[FooCpp] = SourceContents; + FS.Files[FooH] = "int a;"; + FS.Files[Invalid] = "int main() { \n return 0; \n }"; + + Optional<Path> PathResult = Server.switchSourceHeader(FooCpp); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooH); + + PathResult = Server.switchSourceHeader(FooH); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooCpp); + + SourceContents = R"c( + #include "foo.HH" + int b = a; + )c"; + + // Test with header file in capital letters and different extension, source + // file with different extension + auto FooC = testPath("bar.c"); + auto FooHH = testPath("bar.HH"); + + FS.Files[FooC] = SourceContents; + FS.Files[FooHH] = "int a;"; + + PathResult = Server.switchSourceHeader(FooC); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooHH); + + // Test with both capital letters + auto Foo2C = testPath("foo2.C"); + auto Foo2HH = testPath("foo2.HH"); + FS.Files[Foo2C] = SourceContents; + FS.Files[Foo2HH] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo2C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo2HH); + + // Test with source file as capital letter and .hxx header file + auto Foo3C = testPath("foo3.C"); + auto Foo3HXX = testPath("foo3.hxx"); + + SourceContents = R"c( + #include "foo3.hxx" + int b = a; + )c"; + + FS.Files[Foo3C] = SourceContents; + FS.Files[Foo3HXX] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo3C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo3HXX); + + // Test if asking for a corresponding file that doesn't exist returns an empty + // string. + PathResult = Server.switchSourceHeader(Invalid); + EXPECT_FALSE(PathResult.hasValue()); +} + +TEST_F(ClangdThreadingTest, NoConcurrentDiagnostics) { + class NoConcurrentAccessDiagConsumer : public DiagnosticsConsumer { + public: + std::atomic<int> Count = {0}; + + NoConcurrentAccessDiagConsumer(std::promise<void> StartSecondReparse) + : StartSecondReparse(std::move(StartSecondReparse)) {} + + void onDiagnosticsReady(PathRef, std::vector<Diag>) override { + ++Count; + std::unique_lock<std::mutex> Lock(Mutex, std::try_to_lock_t()); + ASSERT_TRUE(Lock.owns_lock()) + << "Detected concurrent onDiagnosticsReady calls for the same file."; + + // If we started the second parse immediately, it might cancel the first. + // So we don't allow it to start until the first has delivered diags... + if (FirstRequest) { + FirstRequest = false; + StartSecondReparse.set_value(); + // ... but then we wait long enough that the callbacks would overlap. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + + private: + std::mutex Mutex; + bool FirstRequest = true; + std::promise<void> StartSecondReparse; + }; + + const auto SourceContentsWithoutErrors = R"cpp( +int a; +int b; +int c; +int d; +)cpp"; + + const auto SourceContentsWithErrors = R"cpp( +int a = x; +int b; +int c; +int d; +)cpp"; + + auto FooCpp = testPath("foo.cpp"); + MockFSProvider FS; + FS.Files[FooCpp] = ""; + + std::promise<void> StartSecondPromise; + std::future<void> StartSecond = StartSecondPromise.get_future(); + + NoConcurrentAccessDiagConsumer DiagConsumer(std::move(StartSecondPromise)); + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + Server.addDocument(FooCpp, SourceContentsWithErrors); + StartSecond.wait(); + Server.addDocument(FooCpp, SourceContentsWithoutErrors); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + ASSERT_EQ(DiagConsumer.Count, 2); // Sanity check - we actually ran both? +} + +TEST_F(ClangdVFSTest, FormatCode) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto Path = testPath("foo.cpp"); + std::string Code = R"cpp( +#include "x.h" +#include "y.h" + +void f( ) {} +)cpp"; + std::string Expected = R"cpp( +#include "x.h" +#include "y.h" + +void f() {} +)cpp"; + FS.Files[Path] = Code; + runAddDocument(Server, Path, Code); + + auto Replaces = Server.formatFile(Code, Path); + EXPECT_TRUE(static_cast<bool>(Replaces)); + auto Changed = tooling::applyAllReplacements(Code, *Replaces); + EXPECT_TRUE(static_cast<bool>(Changed)); + EXPECT_EQ(Expected, *Changed); +} + +TEST_F(ClangdVFSTest, ChangedHeaderFromISystem) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourcePath = testPath("source/foo.cpp"); + auto HeaderPath = testPath("headers/foo.h"); + FS.Files[HeaderPath] = "struct X { int bar; };"; + Annotations Code(R"cpp( + #include "foo.h" + + int main() { + X().ba^ + })cpp"); + CDB.ExtraClangFlags.push_back("-xc++"); + CDB.ExtraClangFlags.push_back("-isystem" + testPath("headers")); + + runAddDocument(Server, SourcePath, Code.code()); + auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"))); + // Update the header and rerun addDocument to make sure we get the updated + // files. + FS.Files[HeaderPath] = "struct X { int bar; int baz; };"; + runAddDocument(Server, SourcePath, Code.code()); + Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + // We want to make sure we see the updated version. + EXPECT_THAT(Completions, ElementsAre(Field(&CodeCompletion::Name, "bar"), + Field(&CodeCompletion::Name, "baz"))); +} + +// FIXME(ioeric): make this work for windows again. +#ifndef _WIN32 +// Check that running code completion doesn't stat() a bunch of files from the +// preamble again. (They should be using the preamble's stat-cache) +TEST(ClangdTests, PreambleVFSStatCache) { + class ListenStatsFSProvider : public FileSystemProvider { + public: + ListenStatsFSProvider(llvm::StringMap<unsigned> &CountStats) + : CountStats(CountStats) {} + + IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override { + class ListenStatVFS : public llvm::vfs::ProxyFileSystem { + public: + ListenStatVFS(IntrusiveRefCntPtr<llvm::vfs::FileSystem> FS, + llvm::StringMap<unsigned> &CountStats) + : ProxyFileSystem(std::move(FS)), CountStats(CountStats) {} + + llvm::ErrorOr<std::unique_ptr<llvm::vfs::File>> + openFileForRead(const Twine &Path) override { + ++CountStats[llvm::sys::path::filename(Path.str())]; + return ProxyFileSystem::openFileForRead(Path); + } + llvm::ErrorOr<llvm::vfs::Status> status(const Twine &Path) override { + ++CountStats[llvm::sys::path::filename(Path.str())]; + return ProxyFileSystem::status(Path); + } + + private: + llvm::StringMap<unsigned> &CountStats; + }; + + return IntrusiveRefCntPtr<ListenStatVFS>( + new ListenStatVFS(buildTestFS(Files), CountStats)); + } + + // If relative paths are used, they are resolved with testPath(). + llvm::StringMap<std::string> Files; + llvm::StringMap<unsigned> &CountStats; + }; + + llvm::StringMap<unsigned> CountStats; + ListenStatsFSProvider FS(CountStats); + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto SourcePath = testPath("foo.cpp"); + auto HeaderPath = testPath("foo.h"); + FS.Files[HeaderPath] = "struct TestSym {};"; + Annotations Code(R"cpp( + #include "foo.h" + + int main() { + TestSy^ + })cpp"); + + runAddDocument(Server, SourcePath, Code.code()); + + unsigned Before = CountStats["foo.h"]; + EXPECT_GT(Before, 0u); + auto Completions = cantFail(runCodeComplete(Server, SourcePath, Code.point(), + clangd::CodeCompleteOptions())) + .Completions; + EXPECT_EQ(CountStats["foo.h"], Before); + EXPECT_THAT(Completions, + ElementsAre(Field(&CodeCompletion::Name, "TestSym"))); +} +#endif + +TEST_F(ClangdVFSTest, FlagsWithPlugins) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + CDB.ExtraClangFlags = { + "-Xclang", + "-add-plugin", + "-Xclang", + "random-plugin", + }; + OverlayCDB OCDB(&CDB); + ClangdServer Server(OCDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const auto SourceContents = "int main() { return 0; }"; + FS.Files[FooCpp] = FooCpp; + Server.addDocument(FooCpp, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for diagnostics"; + EXPECT_NE(Result, "<no-ast>"); +} + +TEST_F(ClangdVFSTest, FallbackWhenPreambleIsNotReady) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; + int main() { + xy^ + })cpp"); + FS.Files[FooCpp] = FooCpp; + + auto Opts = clangd::CodeCompleteOptions(); + Opts.AllowFallback = true; + + // This will make compile command broken and preamble absent. + CDB.ExtraClangFlags = {"yolo.cc"}; + Server.addDocument(FooCpp, Code.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + // Identifier-based fallback completion doesn't know about "symbol" scope. + EXPECT_THAT(Res.Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "")))); + + // Make the compile command work again. + CDB.ExtraClangFlags = {"-std=c++11"}; + Server.addDocument(FooCpp, Code.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); +} + +TEST_F(ClangdVFSTest, FallbackWhenWaitingForCompileCommand) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + // Returns compile command only when notified. + class DelayedCompilationDatabase : public GlobalCompilationDatabase { + public: + DelayedCompilationDatabase(Notification &CanReturnCommand) + : CanReturnCommand(CanReturnCommand) {} + + llvm::Optional<tooling::CompileCommand> + getCompileCommand(PathRef File, ProjectInfo * = nullptr) const override { + // FIXME: make this timeout and fail instead of waiting forever in case + // something goes wrong. + CanReturnCommand.wait(); + auto FileName = llvm::sys::path::filename(File); + std::vector<std::string> CommandLine = {"clangd", "-ffreestanding", File}; + return {tooling::CompileCommand(llvm::sys::path::parent_path(File), + FileName, std::move(CommandLine), "")}; + } + + std::vector<std::string> ExtraClangFlags; + + private: + Notification &CanReturnCommand; + }; + + Notification CanReturnCommand; + DelayedCompilationDatabase CDB(CanReturnCommand); + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; + int main() { + xy^ + })cpp"); + FS.Files[FooCpp] = FooCpp; + Server.addDocument(FooCpp, Code.code()); + + // Sleep for some time to make sure code completion is not run because update + // hasn't been scheduled. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + auto Opts = clangd::CodeCompleteOptions(); + Opts.AllowFallback = true; + + auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + + CanReturnCommand.notify(); + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), + clangd::CodeCompleteOptions())) + .Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/ClangdUnitTests.cpp b/clangd/unittests/ClangdUnitTests.cpp new file mode 100644 index 00000000..2c239ce7 --- /dev/null +++ b/clangd/unittests/ClangdUnitTests.cpp @@ -0,0 +1,86 @@ +//===-- ClangdUnitTests.cpp - ClangdUnit tests ------------------*- 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 "ClangdUnit.h" +#include "SourceCode.h" +#include "TestTU.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::ElementsAre; + +TEST(ClangdUnitTest, GetBeginningOfIdentifier) { + std::string Preamble = R"cpp( +struct Bar { int func(); }; +#define MACRO(X) void f() { X; } +Bar* bar; + )cpp"; + // First ^ is the expected beginning, last is the search position. + for (std::string Text : std::vector<std::string>{ + "int ^f^oo();", // inside identifier + "int ^foo();", // beginning of identifier + "int ^foo^();", // end of identifier + "int foo(^);", // non-identifier + "^int foo();", // beginning of file (can't back up) + "int ^f0^0();", // after a digit (lexing at N-1 is wrong) + "int ^λλ^λ();", // UTF-8 handled properly when backing up + + // identifier in macro arg + "MACRO(bar->^func())", // beginning of identifier + "MACRO(bar->^fun^c())", // inside identifier + "MACRO(bar->^func^())", // end of identifier + "MACRO(^bar->func())", // begin identifier + "MACRO(^bar^->func())", // end identifier + "^MACRO(bar->func())", // beginning of macro name + "^MAC^RO(bar->func())", // inside macro name + "^MACRO^(bar->func())", // end of macro name + }) { + std::string WithPreamble = Preamble + Text; + Annotations TestCase(WithPreamble); + auto AST = TestTU::withCode(TestCase.code()).build(); + const auto &SourceMgr = AST.getASTContext().getSourceManager(); + SourceLocation Actual = getBeginningOfIdentifier( + AST, TestCase.points().back(), SourceMgr.getMainFileID()); + Position ActualPos = offsetToPosition( + TestCase.code(), + SourceMgr.getFileOffset(SourceMgr.getSpellingLoc(Actual))); + EXPECT_EQ(TestCase.points().front(), ActualPos) << Text; + } +} + +MATCHER_P(DeclNamed, Name, "") { + if (NamedDecl *ND = dyn_cast<NamedDecl>(arg)) + if (ND->getName() == Name) + return true; + if (auto *Stream = result_listener->stream()) { + llvm::raw_os_ostream OS(*Stream); + arg->dump(OS); + } + return false; +} + +TEST(ClangdUnitTest, TopLevelDecls) { + TestTU TU; + TU.HeaderCode = R"( + int header1(); + int header2; + )"; + TU.Code = "int main();"; + auto AST = TU.build(); + EXPECT_THAT(AST.getLocalTopLevelDecls(), ElementsAre(DeclNamed("main"))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/CodeCompleteTests.cpp b/clangd/unittests/CodeCompleteTests.cpp new file mode 100644 index 00000000..e584597e --- /dev/null +++ b/clangd/unittests/CodeCompleteTests.cpp @@ -0,0 +1,2551 @@ +//===-- CodeCompleteTests.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 "ClangdServer.h" +#include "CodeComplete.h" +#include "Compiler.h" +#include "Matchers.h" +#include "Protocol.h" +#include "Quality.h" +#include "SourceCode.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestIndex.h" +#include "TestTU.h" +#include "index/Index.h" +#include "index/MemIndex.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "clang/Tooling/CompilationDatabase.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/Path.h" +#include "llvm/Testing/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +namespace { +using ::llvm::Failed; +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::UnorderedElementsAre; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override {} +}; + +// GMock helpers for matching completion items. +MATCHER_P(Named, Name, "") { return arg.Name == Name; } +MATCHER_P(Scope, S, "") { return arg.Scope == S; } +MATCHER_P(Qualifier, Q, "") { return arg.RequiredQualifier == Q; } +MATCHER_P(Labeled, Label, "") { + return arg.RequiredQualifier + arg.Name + arg.Signature == Label; +} +MATCHER_P(SigHelpLabeled, Label, "") { return arg.label == Label; } +MATCHER_P(Kind, K, "") { return arg.Kind == K; } +MATCHER_P(Doc, D, "") { return arg.Documentation == D; } +MATCHER_P(ReturnType, D, "") { return arg.ReturnType == D; } +MATCHER_P(HasInclude, IncludeHeader, "") { + return !arg.Includes.empty() && arg.Includes[0].Header == IncludeHeader; +} +MATCHER_P(InsertInclude, IncludeHeader, "") { + return !arg.Includes.empty() && arg.Includes[0].Header == IncludeHeader && + bool(arg.Includes[0].Insertion); +} +MATCHER(InsertInclude, "") { + return !arg.Includes.empty() && bool(arg.Includes[0].Insertion); +} +MATCHER_P(SnippetSuffix, Text, "") { return arg.SnippetSuffix == Text; } +MATCHER_P(Origin, OriginSet, "") { return arg.Origin == OriginSet; } +MATCHER_P(Signature, S, "") { return arg.Signature == S; } + +// Shorthand for Contains(Named(Name)). +Matcher<const std::vector<CodeCompletion> &> Has(std::string Name) { + return Contains(Named(std::move(Name))); +} +Matcher<const std::vector<CodeCompletion> &> Has(std::string Name, + CompletionItemKind K) { + return Contains(AllOf(Named(std::move(Name)), Kind(K))); +} +MATCHER(IsDocumented, "") { return !arg.Documentation.empty(); } +MATCHER(Deprecated, "") { return arg.Deprecated; } + +std::unique_ptr<SymbolIndex> memIndex(std::vector<Symbol> Symbols) { + SymbolSlab::Builder Slab; + for (const auto &Sym : Symbols) + Slab.insert(Sym); + return MemIndex::build(std::move(Slab).build(), RefSlab()); +} + +CodeCompleteResult completions(ClangdServer &Server, llvm::StringRef TestCode, + Position point, + std::vector<Symbol> IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}) { + std::unique_ptr<SymbolIndex> OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + auto File = testPath("foo.cpp"); + runAddDocument(Server, File, TestCode); + auto CompletionList = + llvm::cantFail(runCodeComplete(Server, File, point, Opts)); + return CompletionList; +} + +CodeCompleteResult completions(ClangdServer &Server, llvm::StringRef Text, + std::vector<Symbol> IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + std::unique_ptr<SymbolIndex> OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + auto File = testPath(FilePath); + Annotations Test(Text); + runAddDocument(Server, File, Test.code()); + auto CompletionList = + llvm::cantFail(runCodeComplete(Server, File, Test.point(), Opts)); + return CompletionList; +} + +// Builds a server and runs code completion. +// If IndexSymbols is non-empty, an index will be built and passed to opts. +CodeCompleteResult completions(llvm::StringRef Text, + std::vector<Symbol> IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + return completions(Server, Text, std::move(IndexSymbols), std::move(Opts), + FilePath); +} + +// Builds a server and runs code completion. +// If IndexSymbols is non-empty, an index will be built and passed to opts. +CodeCompleteResult completionsNoCompile(llvm::StringRef Text, + std::vector<Symbol> IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + std::unique_ptr<SymbolIndex> OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + MockFSProvider FS; + Annotations Test(Text); + return codeComplete(FilePath, tooling::CompileCommand(), /*Preamble=*/nullptr, + Test.code(), Test.point(), FS.getFileSystem(), Opts); +} + +Symbol withReferences(int N, Symbol S) { + S.References = N; + return S; +} + +TEST(CompletionTest, Limit) { + clangd::CodeCompleteOptions Opts; + Opts.Limit = 2; + auto Results = completions(R"cpp( +struct ClassWithMembers { + int AAA(); + int BBB(); + int CCC(); +}; + +int main() { ClassWithMembers().^ } + )cpp", + /*IndexSymbols=*/{}, Opts); + + EXPECT_TRUE(Results.HasMore); + EXPECT_THAT(Results.Completions, ElementsAre(Named("AAA"), Named("BBB"))); +} + +TEST(CompletionTest, Filter) { + std::string Body = R"cpp( + #define MotorCar + int Car; + struct S { + int FooBar; + int FooBaz; + int Qux; + }; + )cpp"; + + // Only items matching the fuzzy query are returned. + EXPECT_THAT(completions(Body + "int main() { S().Foba^ }").Completions, + AllOf(Has("FooBar"), Has("FooBaz"), Not(Has("Qux")))); + + // Macros require prefix match. + EXPECT_THAT(completions(Body + "int main() { C^ }").Completions, + AllOf(Has("Car"), Not(Has("MotorCar")))); +} + +void TestAfterDotCompletion(clangd::CodeCompleteOptions Opts) { + auto Results = completions( + R"cpp( + int global_var; + + int global_func(); + + // Make sure this is not in preamble. + #define MACRO X + + struct GlobalClass {}; + + struct ClassWithMembers { + /// Doc for method. + int method(); + + int field; + private: + int private_field; + }; + + int test() { + struct LocalClass {}; + + /// Doc for local_var. + int local_var; + + ClassWithMembers().^ + } + )cpp", + {cls("IndexClass"), var("index_var"), func("index_func")}, Opts); + + EXPECT_TRUE(Results.RanParser); + // Class members. The only items that must be present in after-dot + // completion. + EXPECT_THAT(Results.Completions, + AllOf(Has("method"), Has("field"), Not(Has("ClassWithMembers")), + Not(Has("operator=")), Not(Has("~ClassWithMembers")))); + EXPECT_IFF(Opts.IncludeIneligibleResults, Results.Completions, + Has("private_field")); + // Global items. + EXPECT_THAT( + Results.Completions, + Not(AnyOf(Has("global_var"), Has("index_var"), Has("global_func"), + Has("global_func()"), Has("index_func"), Has("GlobalClass"), + Has("IndexClass"), Has("MACRO"), Has("LocalClass")))); + // There should be no code patterns (aka snippets) in after-dot + // completion. At least there aren't any we're aware of. + EXPECT_THAT(Results.Completions, + Not(Contains(Kind(CompletionItemKind::Snippet)))); + // Check documentation. + EXPECT_IFF(Opts.IncludeComments, Results.Completions, + Contains(IsDocumented())); +} + +void TestGlobalScopeCompletion(clangd::CodeCompleteOptions Opts) { + auto Results = completions( + R"cpp( + int global_var; + int global_func(); + + // Make sure this is not in preamble. + #define MACRO X + + struct GlobalClass {}; + + struct ClassWithMembers { + /// Doc for method. + int method(); + }; + + int test() { + struct LocalClass {}; + + /// Doc for local_var. + int local_var; + + ^ + } + )cpp", + {cls("IndexClass"), var("index_var"), func("index_func")}, Opts); + + EXPECT_TRUE(Results.RanParser); + // Class members. Should never be present in global completions. + EXPECT_THAT(Results.Completions, + Not(AnyOf(Has("method"), Has("method()"), Has("field")))); + // Global items. + EXPECT_THAT(Results.Completions, + AllOf(Has("global_var"), Has("index_var"), Has("global_func"), + Has("index_func" /* our fake symbol doesn't include () */), + Has("GlobalClass"), Has("IndexClass"))); + // A macro. + EXPECT_IFF(Opts.IncludeMacros, Results.Completions, Has("MACRO")); + // Local items. Must be present always. + EXPECT_THAT(Results.Completions, + AllOf(Has("local_var"), Has("LocalClass"), + Contains(Kind(CompletionItemKind::Snippet)))); + // Check documentation. + EXPECT_IFF(Opts.IncludeComments, Results.Completions, + Contains(IsDocumented())); +} + +TEST(CompletionTest, CompletionOptions) { + auto Test = [&](const clangd::CodeCompleteOptions &Opts) { + TestAfterDotCompletion(Opts); + TestGlobalScopeCompletion(Opts); + }; + // We used to test every combination of options, but that got too slow (2^N). + auto Flags = { + &clangd::CodeCompleteOptions::IncludeMacros, + &clangd::CodeCompleteOptions::IncludeComments, + &clangd::CodeCompleteOptions::IncludeCodePatterns, + &clangd::CodeCompleteOptions::IncludeIneligibleResults, + }; + // Test default options. + Test({}); + // Test with one flag flipped. + for (auto &F : Flags) { + clangd::CodeCompleteOptions O; + O.*F ^= true; + Test(O); + } +} + +TEST(CompletionTest, Accessible) { + auto Internal = completions(R"cpp( + class Foo { + public: void pub(); + protected: void prot(); + private: void priv(); + }; + void Foo::pub() { this->^ } + )cpp"); + EXPECT_THAT(Internal.Completions, + AllOf(Has("priv"), Has("prot"), Has("pub"))); + + auto External = completions(R"cpp( + class Foo { + public: void pub(); + protected: void prot(); + private: void priv(); + }; + void test() { + Foo F; + F.^ + } + )cpp"); + EXPECT_THAT(External.Completions, + AllOf(Has("pub"), Not(Has("prot")), Not(Has("priv")))); +} + +TEST(CompletionTest, Qualifiers) { + auto Results = completions(R"cpp( + class Foo { + public: int foo() const; + int bar() const; + }; + class Bar : public Foo { + int foo() const; + }; + void test() { Bar().^ } + )cpp"); + EXPECT_THAT(Results.Completions, + Contains(AllOf(Qualifier(""), Named("bar")))); + // Hidden members are not shown. + EXPECT_THAT(Results.Completions, + Not(Contains(AllOf(Qualifier("Foo::"), Named("foo"))))); + // Private members are not shown. + EXPECT_THAT(Results.Completions, + Not(Contains(AllOf(Qualifier(""), Named("foo"))))); +} + +TEST(CompletionTest, InjectedTypename) { + // These are suppressed when accessed as a member... + EXPECT_THAT(completions("struct X{}; void foo(){ X().^ }").Completions, + Not(Has("X"))); + EXPECT_THAT(completions("struct X{ void foo(){ this->^ } };").Completions, + Not(Has("X"))); + // ...but accessible in other, more useful cases. + EXPECT_THAT(completions("struct X{ void foo(){ ^ } };").Completions, + Has("X")); + EXPECT_THAT( + completions("struct Y{}; struct X:Y{ void foo(){ ^ } };").Completions, + Has("Y")); + EXPECT_THAT( + completions( + "template<class> struct Y{}; struct X:Y<int>{ void foo(){ ^ } };") + .Completions, + Has("Y")); + // This case is marginal (`using X::X` is useful), we allow it for now. + EXPECT_THAT(completions("struct X{}; void foo(){ X::^ }").Completions, + Has("X")); +} + +TEST(CompletionTest, SkipInjectedWhenUnqualified) { + EXPECT_THAT(completions("struct X { void f() { X^ }};").Completions, + ElementsAre(Named("X"), Named("~X"))); +} + +TEST(CompletionTest, Snippets) { + clangd::CodeCompleteOptions Opts; + auto Results = completions( + R"cpp( + struct fake { + int a; + int f(int i, const float f) const; + }; + int main() { + fake f; + f.^ + } + )cpp", + /*IndexSymbols=*/{}, Opts); + EXPECT_THAT( + Results.Completions, + HasSubsequence(Named("a"), + SnippetSuffix("(${1:int i}, ${2:const float f})"))); +} + +TEST(CompletionTest, Kinds) { + auto Results = completions( + R"cpp( + int variable; + struct Struct {}; + int function(); + // make sure MACRO is not included in preamble. + #define MACRO 10 + int X = ^ + )cpp", + {func("indexFunction"), var("indexVariable"), cls("indexClass")}); + EXPECT_THAT(Results.Completions, + AllOf(Has("function", CompletionItemKind::Function), + Has("variable", CompletionItemKind::Variable), + Has("int", CompletionItemKind::Keyword), + Has("Struct", CompletionItemKind::Class), + Has("MACRO", CompletionItemKind::Text), + Has("indexFunction", CompletionItemKind::Function), + Has("indexVariable", CompletionItemKind::Variable), + Has("indexClass", CompletionItemKind::Class))); + + Results = completions("nam^"); + EXPECT_THAT(Results.Completions, + Has("namespace", CompletionItemKind::Snippet)); +} + +TEST(CompletionTest, NoDuplicates) { + auto Results = completions( + R"cpp( + class Adapter { + }; + + void f() { + Adapter^ + } + )cpp", + {cls("Adapter")}); + + // Make sure there are no duplicate entries of 'Adapter'. + EXPECT_THAT(Results.Completions, ElementsAre(Named("Adapter"))); +} + +TEST(CompletionTest, ScopedNoIndex) { + auto Results = completions( + R"cpp( + namespace fake { int BigBang, Babble, Box; }; + int main() { fake::ba^ } + ")cpp"); + // Babble is a better match than BigBang. Box doesn't match at all. + EXPECT_THAT(Results.Completions, + ElementsAre(Named("Babble"), Named("BigBang"))); +} + +TEST(CompletionTest, Scoped) { + auto Results = completions( + R"cpp( + namespace fake { int Babble, Box; }; + int main() { fake::ba^ } + ")cpp", + {var("fake::BigBang")}); + EXPECT_THAT(Results.Completions, + ElementsAre(Named("Babble"), Named("BigBang"))); +} + +TEST(CompletionTest, ScopedWithFilter) { + auto Results = completions( + R"cpp( + void f() { ns::x^ } + )cpp", + {cls("ns::XYZ"), func("ns::foo")}); + EXPECT_THAT(Results.Completions, UnorderedElementsAre(Named("XYZ"))); +} + +TEST(CompletionTest, ReferencesAffectRanking) { + auto Results = completions("int main() { abs^ }", {ns("absl"), func("absb")}); + EXPECT_THAT(Results.Completions, + HasSubsequence(Named("absb"), Named("absl"))); + Results = completions("int main() { abs^ }", + {withReferences(10000, ns("absl")), func("absb")}); + EXPECT_THAT(Results.Completions, + HasSubsequence(Named("absl"), Named("absb"))); +} + +TEST(CompletionTest, ContextWords) { + auto Results = completions(R"cpp( + enum class Color { RED, YELLOW, BLUE }; + + // (blank lines so the definition above isn't "context") + + // "It was a yellow car," he said. "Big yellow car, new." + auto Finish = Color::^ + )cpp"); + // Yellow would normally sort last (alphabetic). + // But the recent mention shuold bump it up. + ASSERT_THAT(Results.Completions, + HasSubsequence(Named("YELLOW"), Named("BLUE"))); +} + +TEST(CompletionTest, GlobalQualified) { + auto Results = completions( + R"cpp( + void f() { ::^ } + )cpp", + {cls("XYZ")}); + EXPECT_THAT(Results.Completions, + AllOf(Has("XYZ", CompletionItemKind::Class), + Has("f", CompletionItemKind::Function))); +} + +TEST(CompletionTest, FullyQualified) { + auto Results = completions( + R"cpp( + namespace ns { void bar(); } + void f() { ::ns::^ } + )cpp", + {cls("ns::XYZ")}); + EXPECT_THAT(Results.Completions, + AllOf(Has("XYZ", CompletionItemKind::Class), + Has("bar", CompletionItemKind::Function))); +} + +TEST(CompletionTest, SemaIndexMerge) { + auto Results = completions( + R"cpp( + namespace ns { int local; void both(); } + void f() { ::ns::^ } + )cpp", + {func("ns::both"), cls("ns::Index")}); + // We get results from both index and sema, with no duplicates. + EXPECT_THAT(Results.Completions, + UnorderedElementsAre( + AllOf(Named("local"), Origin(SymbolOrigin::AST)), + AllOf(Named("Index"), Origin(SymbolOrigin::Static)), + AllOf(Named("both"), + Origin(SymbolOrigin::AST | SymbolOrigin::Static)))); +} + +TEST(CompletionTest, SemaIndexMergeWithLimit) { + clangd::CodeCompleteOptions Opts; + Opts.Limit = 1; + auto Results = completions( + R"cpp( + namespace ns { int local; void both(); } + void f() { ::ns::^ } + )cpp", + {func("ns::both"), cls("ns::Index")}, Opts); + EXPECT_EQ(Results.Completions.size(), Opts.Limit); + EXPECT_TRUE(Results.HasMore); +} + +TEST(CompletionTest, IncludeInsertionPreprocessorIntegrationTests) { + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string Subdir = testPath("sub"); + std::string SearchDirArg = (Twine("-I") + Subdir).str(); + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = ""; + + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + auto BarURI = URI::create(BarHeader).toString(); + Symbol Sym = cls("ns::X"); + Sym.CanonicalDeclaration.FileURI = BarURI.c_str(); + Sym.IncludeHeaders.emplace_back(BarURI, 1); + // Shoten include path based on search dirctory and insert. + auto Results = completions(Server, + R"cpp( + int main() { ns::^ } + )cpp", + {Sym}); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Named("X"), InsertInclude("\"bar.h\"")))); + // Can be disabled via option. + CodeCompleteOptions NoInsertion; + NoInsertion.InsertIncludes = CodeCompleteOptions::NeverInsert; + Results = completions(Server, + R"cpp( + int main() { ns::^ } + )cpp", + {Sym}, NoInsertion); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Named("X"), Not(InsertInclude())))); + // Duplicate based on inclusions in preamble. + Results = completions(Server, + R"cpp( + #include "sub/bar.h" // not shortest, so should only match resolved. + int main() { ns::^ } + )cpp", + {Sym}); + EXPECT_THAT(Results.Completions, ElementsAre(AllOf(Named("X"), Labeled("X"), + Not(InsertInclude())))); +} + +TEST(CompletionTest, NoIncludeInsertionWhenDeclFoundInFile) { + MockFSProvider FS; + MockCompilationDatabase CDB; + + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + Symbol SymX = cls("ns::X"); + Symbol SymY = cls("ns::Y"); + std::string BarHeader = testPath("bar.h"); + auto BarURI = URI::create(BarHeader).toString(); + SymX.CanonicalDeclaration.FileURI = BarURI.c_str(); + SymY.CanonicalDeclaration.FileURI = BarURI.c_str(); + SymX.IncludeHeaders.emplace_back("<bar>", 1); + SymY.IncludeHeaders.emplace_back("<bar>", 1); + // Shoten include path based on search dirctory and insert. + auto Results = completions(Server, + R"cpp( + namespace ns { + class X; + class Y {}; + } + int main() { ns::^ } + )cpp", + {SymX, SymY}); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Named("X"), Not(InsertInclude())), + AllOf(Named("Y"), Not(InsertInclude())))); +} + +TEST(CompletionTest, IndexSuppressesPreambleCompletions) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + FS.Files[testPath("bar.h")] = + R"cpp(namespace ns { struct preamble { int member; }; })cpp"; + auto File = testPath("foo.cpp"); + Annotations Test(R"cpp( + #include "bar.h" + namespace ns { int local; } + void f() { ns::^; } + void f2() { ns::preamble().$2^; } + )cpp"); + runAddDocument(Server, File, Test.code()); + clangd::CodeCompleteOptions Opts = {}; + + auto I = memIndex({var("ns::index")}); + Opts.Index = I.get(); + auto WithIndex = cantFail(runCodeComplete(Server, File, Test.point(), Opts)); + EXPECT_THAT(WithIndex.Completions, + UnorderedElementsAre(Named("local"), Named("index"))); + auto ClassFromPreamble = + cantFail(runCodeComplete(Server, File, Test.point("2"), Opts)); + EXPECT_THAT(ClassFromPreamble.Completions, Contains(Named("member"))); + + Opts.Index = nullptr; + auto WithoutIndex = + cantFail(runCodeComplete(Server, File, Test.point(), Opts)); + EXPECT_THAT(WithoutIndex.Completions, + UnorderedElementsAre(Named("local"), Named("preamble"))); +} + +// This verifies that we get normal preprocessor completions in the preamble. +// This is a regression test for an old bug: if we override the preamble and +// try to complete inside it, clang kicks our completion point just outside the +// preamble, resulting in always getting top-level completions. +TEST(CompletionTest, CompletionInPreamble) { + auto Results = completions(R"cpp( + #ifnd^ef FOO_H_ + #define BAR_H_ + #include <bar.h> + int foo() {} + #endif + )cpp") + .Completions; + EXPECT_THAT(Results, ElementsAre(Named("ifndef"))); +} + +TEST(CompletionTest, DynamicIndexIncludeInsertion) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer::Options Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + + FS.Files[testPath("foo_header.h")] = R"cpp( + #pragma once + struct Foo { + // Member doc + int foo(); + }; + )cpp"; + const std::string FileContent(R"cpp( + #include "foo_header.h" + int Foo::foo() { + return 42; + } + )cpp"); + Server.addDocument(testPath("foo_impl.cpp"), FileContent); + // Wait for the dynamic index being built. + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(completions(Server, "Foo^ foo;").Completions, + ElementsAre(AllOf(Named("Foo"), + HasInclude('"' + + llvm::sys::path::convert_to_slash( + testPath("foo_header.h")) + + '"'), + InsertInclude()))); +} + +TEST(CompletionTest, DynamicIndexMultiFile) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + auto Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + + FS.Files[testPath("foo.h")] = R"cpp( + namespace ns { class XYZ {}; void foo(int x) {} } + )cpp"; + runAddDocument(Server, testPath("foo.cpp"), R"cpp( + #include "foo.h" + )cpp"); + + auto File = testPath("bar.cpp"); + Annotations Test(R"cpp( + namespace ns { + class XXX {}; + /// Doooc + void fooooo() {} + } + void f() { ns::^ } + )cpp"); + runAddDocument(Server, File, Test.code()); + + auto Results = cantFail(runCodeComplete(Server, File, Test.point(), {})); + // "XYZ" and "foo" are not included in the file being completed but are still + // visible through the index. + EXPECT_THAT(Results.Completions, Has("XYZ", CompletionItemKind::Class)); + EXPECT_THAT(Results.Completions, Has("foo", CompletionItemKind::Function)); + EXPECT_THAT(Results.Completions, Has("XXX", CompletionItemKind::Class)); + EXPECT_THAT(Results.Completions, + Contains((Named("fooooo"), Kind(CompletionItemKind::Function), + Doc("Doooc"), ReturnType("void")))); +} + +TEST(CompletionTest, Documentation) { + auto Results = completions( + R"cpp( + // Non-doxygen comment. + int foo(); + /// Doxygen comment. + /// \param int a + int bar(int a); + /* Multi-line + block comment + */ + int baz(); + + int x = ^ + )cpp"); + EXPECT_THAT(Results.Completions, + Contains(AllOf(Named("foo"), Doc("Non-doxygen comment.")))); + EXPECT_THAT( + Results.Completions, + Contains(AllOf(Named("bar"), Doc("Doxygen comment.\n\\param int a")))); + EXPECT_THAT(Results.Completions, + Contains(AllOf(Named("baz"), Doc("Multi-line\nblock comment")))); +} + +TEST(CompletionTest, GlobalCompletionFiltering) { + + Symbol Class = cls("XYZ"); + Class.Flags = static_cast<Symbol::SymbolFlag>( + Class.Flags & ~(Symbol::IndexedForCodeCompletion)); + Symbol Func = func("XYZ::foooo"); + Func.Flags = static_cast<Symbol::SymbolFlag>( + Func.Flags & ~(Symbol::IndexedForCodeCompletion)); + + auto Results = completions(R"(// void f() { + XYZ::foooo^ + })", + {Class, Func}); + EXPECT_THAT(Results.Completions, IsEmpty()); +} + +TEST(CodeCompleteTest, DisableTypoCorrection) { + auto Results = completions(R"cpp( + namespace clang { int v; } + void f() { clangd::^ + )cpp"); + EXPECT_TRUE(Results.Completions.empty()); +} + +TEST(CodeCompleteTest, NoColonColonAtTheEnd) { + auto Results = completions(R"cpp( + namespace clang { } + void f() { + clan^ + } + )cpp"); + + EXPECT_THAT(Results.Completions, Contains(Labeled("clang"))); + EXPECT_THAT(Results.Completions, Not(Contains(Labeled("clang::")))); +} + +TEST(CompletionTest, BacktrackCrashes) { + // Sema calls code completion callbacks twice in these cases. + auto Results = completions(R"cpp( + namespace ns { + struct FooBarBaz {}; + } // namespace ns + + int foo(ns::FooBar^ + )cpp"); + + EXPECT_THAT(Results.Completions, ElementsAre(Labeled("FooBarBaz"))); + + // Check we don't crash in that case too. + completions(R"cpp( + struct FooBarBaz {}; + void test() { + if (FooBarBaz * x^) {} + } +)cpp"); +} + +TEST(CompletionTest, CompleteInMacroWithStringification) { + auto Results = completions(R"cpp( +void f(const char *, int x); +#define F(x) f(#x, x) + +namespace ns { +int X; +int Y; +} // namespace ns + +int f(int input_num) { + F(ns::^) +} +)cpp"); + + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("X"), Named("Y"))); +} + +TEST(CompletionTest, CompleteInMacroAndNamespaceWithStringification) { + auto Results = completions(R"cpp( +void f(const char *, int x); +#define F(x) f(#x, x) + +namespace ns { +int X; + +int f(int input_num) { + F(^) +} +} // namespace ns +)cpp"); + + EXPECT_THAT(Results.Completions, Contains(Named("X"))); +} + +TEST(CompletionTest, IgnoreCompleteInExcludedPPBranchWithRecoveryContext) { + auto Results = completions(R"cpp( + int bar(int param_in_bar) { + } + + int foo(int param_in_foo) { +#if 0 + // In recorvery mode, "param_in_foo" will also be suggested among many other + // unrelated symbols; however, this is really a special case where this works. + // If the #if block is outside of the function, "param_in_foo" is still + // suggested, but "bar" and "foo" are missing. So the recovery mode doesn't + // really provide useful results in excluded branches. + par^ +#endif + } +)cpp"); + + EXPECT_TRUE(Results.Completions.empty()); +} +SignatureHelp signatures(llvm::StringRef Text, Position Point, + std::vector<Symbol> IndexSymbols = {}) { + std::unique_ptr<SymbolIndex> Index; + if (!IndexSymbols.empty()) + Index = memIndex(IndexSymbols); + + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer::Options Opts = ClangdServer::optsForTest(); + Opts.StaticIndex = Index.get(); + + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + auto File = testPath("foo.cpp"); + runAddDocument(Server, File, Text); + return llvm::cantFail(runSignatureHelp(Server, File, Point)); +} + +SignatureHelp signatures(llvm::StringRef Text, + std::vector<Symbol> IndexSymbols = {}) { + Annotations Test(Text); + return signatures(Test.code(), Test.point(), std::move(IndexSymbols)); +} + +MATCHER_P(ParamsAre, P, "") { + if (P.size() != arg.parameters.size()) + return false; + for (unsigned I = 0; I < P.size(); ++I) + if (P[I] != arg.parameters[I].label) + return false; + return true; +} +MATCHER_P(SigDoc, Doc, "") { return arg.documentation == Doc; } + +Matcher<SignatureInformation> Sig(std::string Label, + std::vector<std::string> Params) { + return AllOf(SigHelpLabeled(Label), ParamsAre(Params)); +} + +TEST(SignatureHelpTest, Overloads) { + auto Results = signatures(R"cpp( + void foo(int x, int y); + void foo(int x, float y); + void foo(float x, int y); + void foo(float x, float y); + void bar(int x, int y = 0); + int main() { foo(^); } + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre( + Sig("foo(float x, float y) -> void", {"float x", "float y"}), + Sig("foo(float x, int y) -> void", {"float x", "int y"}), + Sig("foo(int x, float y) -> void", {"int x", "float y"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + // We always prefer the first signature. + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(0, Results.activeParameter); +} + +TEST(SignatureHelpTest, DefaultArgs) { + auto Results = signatures(R"cpp( + void bar(int x, int y = 0); + void bar(float x = 0, int y = 42); + int main() { bar(^ + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre( + Sig("bar(int x, int y = 0) -> void", {"int x", "int y = 0"}), + Sig("bar(float x = 0, int y = 42) -> void", + {"float x = 0", "int y = 42"}))); + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(0, Results.activeParameter); +} + +TEST(SignatureHelpTest, ActiveArg) { + auto Results = signatures(R"cpp( + int baz(int a, int b, int c); + int main() { baz(baz(1,2,3), ^); } + )cpp"); + EXPECT_THAT(Results.signatures, + ElementsAre(Sig("baz(int a, int b, int c) -> int", + {"int a", "int b", "int c"}))); + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(1, Results.activeParameter); +} + +TEST(SignatureHelpTest, OpeningParen) { + llvm::StringLiteral Tests[] = {// Recursive function call. + R"cpp( + int foo(int a, int b, int c); + int main() { + foo(foo $p^( foo(10, 10, 10), ^ ))); + })cpp", + // Functional type cast. + R"cpp( + struct Foo { + Foo(int a, int b, int c); + }; + int main() { + Foo $p^( 10, ^ ); + })cpp", + // New expression. + R"cpp( + struct Foo { + Foo(int a, int b, int c); + }; + int main() { + new Foo $p^( 10, ^ ); + })cpp", + // Macro expansion. + R"cpp( + int foo(int a, int b, int c); + #define FOO foo( + + int main() { + // Macro expansions. + $p^FOO 10, ^ ); + })cpp", + // Macro arguments. + R"cpp( + int foo(int a, int b, int c); + int main() { + #define ID(X) X + ID(foo $p^( foo(10), ^ )) + })cpp"}; + + for (auto Test : Tests) { + Annotations Code(Test); + EXPECT_EQ(signatures(Code.code(), Code.point()).argListStart, + Code.point("p")) + << "Test source:" << Test; + } +} + +class IndexRequestCollector : public SymbolIndex { +public: + bool + fuzzyFind(const FuzzyFindRequest &Req, + llvm::function_ref<void(const Symbol &)> Callback) const override { + std::lock_guard<std::mutex> Lock(Mut); + Requests.push_back(Req); + return true; + } + + void lookup(const LookupRequest &, + llvm::function_ref<void(const Symbol &)>) const override {} + + void refs(const RefsRequest &, + llvm::function_ref<void(const Ref &)>) const override {} + + // This is incorrect, but IndexRequestCollector is not an actual index and it + // isn't used in production code. + size_t estimateMemoryUsage() const override { return 0; } + + const std::vector<FuzzyFindRequest> consumeRequests() const { + std::lock_guard<std::mutex> Lock(Mut); + auto Reqs = std::move(Requests); + Requests = {}; + return Reqs; + } + +private: + // We need a mutex to handle async fuzzy find requests. + mutable std::mutex Mut; + mutable std::vector<FuzzyFindRequest> Requests; +}; + +std::vector<FuzzyFindRequest> captureIndexRequests(llvm::StringRef Code) { + clangd::CodeCompleteOptions Opts; + IndexRequestCollector Requests; + Opts.Index = &Requests; + completions(Code, {}, Opts); + return Requests.consumeRequests(); +} + +TEST(CompletionTest, UnqualifiedIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace std {} + using namespace std; + namespace ns { + void f() { + vec^ + } + } + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("", "ns::", "std::")))); +} + +TEST(CompletionTest, EnclosingScopeComesFirst) { + auto Requests = captureIndexRequests(R"cpp( + namespace std {} + using namespace std; + namespace nx { + namespace ns { + namespace { + void f() { + vec^ + } + } + } + } + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field( + &FuzzyFindRequest::Scopes, + UnorderedElementsAre("", "std::", "nx::ns::", "nx::")))); + EXPECT_EQ(Requests[0].Scopes[0], "nx::ns::"); +} + +TEST(CompletionTest, ResolvedQualifiedIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace ns1 {} + namespace ns2 {} // ignore + namespace ns3 { namespace nns3 {} } + namespace foo { + using namespace ns1; + using namespace ns3::nns3; + } + namespace ns { + void f() { + foo::^ + } + } + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field( + &FuzzyFindRequest::Scopes, + UnorderedElementsAre("foo::", "ns1::", "ns3::nns3::")))); +} + +TEST(CompletionTest, UnresolvedQualifierIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace a {} + using namespace a; + namespace ns { + void f() { + bar::^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field( + &FuzzyFindRequest::Scopes, + UnorderedElementsAre("a::bar::", "ns::bar::", "bar::")))); +} + +TEST(CompletionTest, UnresolvedNestedQualifierIdQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace a {} + using namespace a; + namespace ns { + void f() { + ::a::bar::^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("a::bar::")))); +} + +TEST(CompletionTest, EmptyQualifiedQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace ns { + void f() { + ^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("", "ns::")))); +} + +TEST(CompletionTest, GlobalQualifiedQuery) { + auto Requests = captureIndexRequests(R"cpp( + namespace ns { + void f() { + ::^ + } + } // namespace ns + )cpp"); + + EXPECT_THAT(Requests, ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("")))); +} + +TEST(CompletionTest, NoDuplicatedQueryScopes) { + auto Requests = captureIndexRequests(R"cpp( + namespace {} + + namespace na { + namespace {} + namespace nb { + ^ + } // namespace nb + } // namespace na + )cpp"); + + EXPECT_THAT(Requests, + ElementsAre(Field(&FuzzyFindRequest::Scopes, + UnorderedElementsAre("na::", "na::nb::", "")))); +} + +TEST(CompletionTest, NoIndexCompletionsInsideClasses) { + auto Completions = completions( + R"cpp( + struct Foo { + int SomeNameOfField; + typedef int SomeNameOfTypedefField; + }; + + Foo::^)cpp", + {func("::SomeNameInTheIndex"), func("::Foo::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + AllOf(Contains(Labeled("SomeNameOfField")), + Contains(Labeled("SomeNameOfTypedefField")), + Not(Contains(Labeled("SomeNameInTheIndex"))))); +} + +TEST(CompletionTest, NoIndexCompletionsInsideDependentCode) { + { + auto Completions = completions( + R"cpp( + template <class T> + void foo() { + T::^ + } + )cpp", + {func("::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + Not(Contains(Labeled("SomeNameInTheIndex")))); + } + + { + auto Completions = completions( + R"cpp( + template <class T> + void foo() { + T::template Y<int>::^ + } + )cpp", + {func("::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + Not(Contains(Labeled("SomeNameInTheIndex")))); + } + + { + auto Completions = completions( + R"cpp( + template <class T> + void foo() { + T::foo::^ + } + )cpp", + {func("::SomeNameInTheIndex")}); + + EXPECT_THAT(Completions.Completions, + Not(Contains(Labeled("SomeNameInTheIndex")))); + } +} + +TEST(CompletionTest, OverloadBundling) { + clangd::CodeCompleteOptions Opts; + Opts.BundleOverloads = true; + + std::string Context = R"cpp( + struct X { + // Overload with int + int a(int); + // Overload with bool + int a(bool); + int b(float); + }; + int GFuncC(int); + int GFuncD(int); + )cpp"; + + // Member completions are bundled. + EXPECT_THAT(completions(Context + "int y = X().^", {}, Opts).Completions, + UnorderedElementsAre(Labeled("a(…)"), Labeled("b(float)"))); + + // Non-member completions are bundled, including index+sema. + Symbol NoArgsGFunc = func("GFuncC"); + EXPECT_THAT( + completions(Context + "int y = GFunc^", {NoArgsGFunc}, Opts).Completions, + UnorderedElementsAre(Labeled("GFuncC(…)"), Labeled("GFuncD(int)"))); + + // Differences in header-to-insert suppress bundling. + std::string DeclFile = URI::create(testPath("foo")).toString(); + NoArgsGFunc.CanonicalDeclaration.FileURI = DeclFile.c_str(); + NoArgsGFunc.IncludeHeaders.emplace_back("<foo>", 1); + EXPECT_THAT( + completions(Context + "int y = GFunc^", {NoArgsGFunc}, Opts).Completions, + UnorderedElementsAre(AllOf(Named("GFuncC"), InsertInclude("<foo>")), + Labeled("GFuncC(int)"), Labeled("GFuncD(int)"))); + + // Examine a bundled completion in detail. + auto A = + completions(Context + "int y = X().a^", {}, Opts).Completions.front(); + EXPECT_EQ(A.Name, "a"); + EXPECT_EQ(A.Signature, "(…)"); + EXPECT_EQ(A.BundleSize, 2u); + EXPECT_EQ(A.Kind, CompletionItemKind::Method); + EXPECT_EQ(A.ReturnType, "int"); // All overloads return int. + // For now we just return one of the doc strings arbitrarily. + EXPECT_THAT(A.Documentation, AnyOf(HasSubstr("Overload with int"), + HasSubstr("Overload with bool"))); + EXPECT_EQ(A.SnippetSuffix, "($0)"); +} + +TEST(CompletionTest, DocumentationFromChangedFileCrash) { + MockFSProvider FS; + auto FooH = testPath("foo.h"); + auto FooCpp = testPath("foo.cpp"); + FS.Files[FooH] = R"cpp( + // this is my documentation comment. + int func(); + )cpp"; + FS.Files[FooCpp] = ""; + + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Annotations Source(R"cpp( + #include "foo.h" + int func() { + // This makes sure we have func from header in the AST. + } + int a = fun^ + )cpp"); + Server.addDocument(FooCpp, Source.code(), WantDiagnostics::Yes); + // We need to wait for preamble to build. + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + // Change the header file. Completion will reuse the old preamble! + FS.Files[FooH] = R"cpp( + int func(); + )cpp"; + + clangd::CodeCompleteOptions Opts; + Opts.IncludeComments = true; + CodeCompleteResult Completions = + cantFail(runCodeComplete(Server, FooCpp, Source.point(), Opts)); + // We shouldn't crash. Unfortunately, current workaround is to not produce + // comments for symbols from headers. + EXPECT_THAT(Completions.Completions, + Contains(AllOf(Not(IsDocumented()), Named("func")))); +} + +TEST(CompletionTest, NonDocComments) { + MockFSProvider FS; + auto FooCpp = testPath("foo.cpp"); + FS.Files[FooCpp] = ""; + + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + Annotations Source(R"cpp( + // We ignore namespace comments, for rationale see CodeCompletionStrings.h. + namespace comments_ns { + } + + // ------------------ + int comments_foo(); + + // A comment and a decl are separated by newlines. + // Therefore, the comment shouldn't show up as doc comment. + + int comments_bar(); + + // this comment should be in the results. + int comments_baz(); + + + template <class T> + struct Struct { + int comments_qux(); + int comments_quux(); + }; + + + // This comment should not be there. + + template <class T> + int Struct<T>::comments_qux() { + } + + // This comment **should** be in results. + template <class T> + int Struct<T>::comments_quux() { + int a = comments^; + } + )cpp"); + // FIXME: Auto-completion in a template requires disabling delayed template + // parsing. + CDB.ExtraClangFlags.push_back("-fno-delayed-template-parsing"); + runAddDocument(Server, FooCpp, Source.code(), WantDiagnostics::Yes); + CodeCompleteResult Completions = cantFail(runCodeComplete( + Server, FooCpp, Source.point(), clangd::CodeCompleteOptions())); + + // We should not get any of those comments in completion. + EXPECT_THAT( + Completions.Completions, + UnorderedElementsAre(AllOf(Not(IsDocumented()), Named("comments_foo")), + AllOf(IsDocumented(), Named("comments_baz")), + AllOf(IsDocumented(), Named("comments_quux")), + AllOf(Not(IsDocumented()), Named("comments_ns")), + // FIXME(ibiryukov): the following items should have + // empty documentation, since they are separated from + // a comment with an empty line. Unfortunately, I + // couldn't make Sema tests pass if we ignore those. + AllOf(IsDocumented(), Named("comments_bar")), + AllOf(IsDocumented(), Named("comments_qux")))); +} + +TEST(CompletionTest, CompleteOnInvalidLine) { + auto FooCpp = testPath("foo.cpp"); + + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + MockFSProvider FS; + FS.Files[FooCpp] = "// empty file"; + + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + // Run completion outside the file range. + Position Pos; + Pos.line = 100; + Pos.character = 0; + EXPECT_THAT_EXPECTED( + runCodeComplete(Server, FooCpp, Pos, clangd::CodeCompleteOptions()), + Failed()); +} + +TEST(CompletionTest, QualifiedNames) { + auto Results = completions( + R"cpp( + namespace ns { int local; void both(); } + void f() { ::ns::^ } + )cpp", + {func("ns::both"), cls("ns::Index")}); + // We get results from both index and sema, with no duplicates. + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(Scope("ns::"), Scope("ns::"), Scope("ns::"))); +} + +TEST(CompletionTest, Render) { + CodeCompletion C; + C.Name = "x"; + C.Signature = "(bool) const"; + C.SnippetSuffix = "(${0:bool})"; + C.ReturnType = "int"; + C.RequiredQualifier = "Foo::"; + C.Scope = "ns::Foo::"; + C.Documentation = "This is x()."; + C.Includes.emplace_back(); + auto &Include = C.Includes.back(); + Include.Header = "\"foo.h\""; + C.Kind = CompletionItemKind::Method; + C.Score.Total = 1.0; + C.Origin = SymbolOrigin::AST | SymbolOrigin::Static; + + CodeCompleteOptions Opts; + Opts.IncludeIndicator.Insert = "^"; + Opts.IncludeIndicator.NoInsert = ""; + Opts.EnableSnippets = false; + + auto R = C.render(Opts); + EXPECT_EQ(R.label, "Foo::x(bool) const"); + EXPECT_EQ(R.insertText, "Foo::x"); + EXPECT_EQ(R.insertTextFormat, InsertTextFormat::PlainText); + EXPECT_EQ(R.filterText, "x"); + EXPECT_EQ(R.detail, "int\n\"foo.h\""); + EXPECT_EQ(R.documentation, "This is x()."); + EXPECT_THAT(R.additionalTextEdits, IsEmpty()); + EXPECT_EQ(R.sortText, sortText(1.0, "x")); + EXPECT_FALSE(R.deprecated); + + Opts.EnableSnippets = true; + R = C.render(Opts); + EXPECT_EQ(R.insertText, "Foo::x(${0:bool})"); + EXPECT_EQ(R.insertTextFormat, InsertTextFormat::Snippet); + + Include.Insertion.emplace(); + R = C.render(Opts); + EXPECT_EQ(R.label, "^Foo::x(bool) const"); + EXPECT_THAT(R.additionalTextEdits, Not(IsEmpty())); + + Opts.ShowOrigins = true; + R = C.render(Opts); + EXPECT_EQ(R.label, "^[AS]Foo::x(bool) const"); + + C.BundleSize = 2; + R = C.render(Opts); + EXPECT_EQ(R.detail, "[2 overloads]\n\"foo.h\""); + + C.Deprecated = true; + R = C.render(Opts); + EXPECT_TRUE(R.deprecated); +} + +TEST(CompletionTest, IgnoreRecoveryResults) { + auto Results = completions( + R"cpp( + namespace ns { int NotRecovered() { return 0; } } + void f() { + // Sema enters recovery mode first and then normal mode. + if (auto x = ns::NotRecover^) + } + )cpp"); + EXPECT_THAT(Results.Completions, UnorderedElementsAre(Named("NotRecovered"))); +} + +TEST(CompletionTest, ScopeOfClassFieldInConstructorInitializer) { + auto Results = completions( + R"cpp( + namespace ns { + class X { public: X(); int x_; }; + X::X() : x_^(0) {} + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Scope("ns::X::"), Named("x_")))); +} + +TEST(CompletionTest, CodeCompletionContext) { + auto Results = completions( + R"cpp( + namespace ns { + class X { public: X(); int x_; }; + void f() { + X x; + x.^; + } + } + )cpp"); + + EXPECT_THAT(Results.Context, CodeCompletionContext::CCC_DotMemberAccess); +} + +TEST(CompletionTest, FixItForArrowToDot) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + Annotations TestCode( + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + class ClassWithPtr { + public: + void MemberFunction(); + Auxilary* operator->() const; + Auxilary* Aux; + }; + void f() { + ClassWithPtr x; + x[[->]]^; + } + )cpp"); + auto Results = + completions(Server, TestCode.code(), TestCode.point(), {}, Opts); + EXPECT_EQ(Results.Completions.size(), 3u); + + TextEdit ReplacementEdit; + ReplacementEdit.range = TestCode.range(); + ReplacementEdit.newText = "."; + for (const auto &C : Results.Completions) { + EXPECT_TRUE(C.FixIts.size() == 1u || C.Name == "AuxFunction"); + if (!C.FixIts.empty()) { + EXPECT_THAT(C.FixIts, ElementsAre(ReplacementEdit)); + } + } +} + +TEST(CompletionTest, FixItForDotToArrow) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + Annotations TestCode( + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + class ClassWithPtr { + public: + void MemberFunction(); + Auxilary* operator->() const; + Auxilary* Aux; + }; + void f() { + ClassWithPtr x; + x[[.]]^; + } + )cpp"); + auto Results = + completions(Server, TestCode.code(), TestCode.point(), {}, Opts); + EXPECT_EQ(Results.Completions.size(), 3u); + + TextEdit ReplacementEdit; + ReplacementEdit.range = TestCode.range(); + ReplacementEdit.newText = "->"; + for (const auto &C : Results.Completions) { + EXPECT_TRUE(C.FixIts.empty() || C.Name == "AuxFunction"); + if (!C.FixIts.empty()) { + EXPECT_THAT(C.FixIts, ElementsAre(ReplacementEdit)); + } + } +} + +TEST(CompletionTest, RenderWithFixItMerged) { + TextEdit FixIt; + FixIt.range.end.character = 5; + FixIt.newText = "->"; + + CodeCompletion C; + C.Name = "x"; + C.RequiredQualifier = "Foo::"; + C.FixIts = {FixIt}; + C.CompletionTokenRange.start.character = 5; + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + + auto R = C.render(Opts); + EXPECT_TRUE(R.textEdit); + EXPECT_EQ(R.textEdit->newText, "->Foo::x"); + EXPECT_TRUE(R.additionalTextEdits.empty()); +} + +TEST(CompletionTest, RenderWithFixItNonMerged) { + TextEdit FixIt; + FixIt.range.end.character = 4; + FixIt.newText = "->"; + + CodeCompletion C; + C.Name = "x"; + C.RequiredQualifier = "Foo::"; + C.FixIts = {FixIt}; + C.CompletionTokenRange.start.character = 5; + + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + + auto R = C.render(Opts); + EXPECT_TRUE(R.textEdit); + EXPECT_EQ(R.textEdit->newText, "Foo::x"); + EXPECT_THAT(R.additionalTextEdits, UnorderedElementsAre(FixIt)); +} + +TEST(CompletionTest, CompletionTokenRange) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + constexpr const char *TestCodes[] = { + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + void f() { + Auxilary x; + x.[[Aux]]^; + } + )cpp", + R"cpp( + class Auxilary { + public: + void AuxFunction(); + }; + void f() { + Auxilary x; + x.[[]]^; + } + )cpp"}; + for (const auto &Text : TestCodes) { + Annotations TestCode(Text); + auto Results = completions(Server, TestCode.code(), TestCode.point()); + + EXPECT_EQ(Results.Completions.size(), 1u); + EXPECT_THAT(Results.Completions.front().CompletionTokenRange, + TestCode.range()); + } +} + +TEST(SignatureHelpTest, OverloadsOrdering) { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, float y); + void foo(float x, int y); + void foo(float x, float y); + void foo(int x, int y = 0); + int main() { foo(^); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre( + Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y = 0) -> void", {"int x", "int y = 0"}), + Sig("foo(float x, int y) -> void", {"float x", "int y"}), + Sig("foo(int x, float y) -> void", {"int x", "float y"}), + Sig("foo(float x, float y) -> void", {"float x", "float y"}))); + // We always prefer the first signature. + EXPECT_EQ(0, Results.activeSignature); + EXPECT_EQ(0, Results.activeParameter); +} + +TEST(SignatureHelpTest, InstantiatedSignatures) { + StringRef Sig0 = R"cpp( + template <class T> + void foo(T, T, T); + + int main() { + foo<int>(^); + } + )cpp"; + + EXPECT_THAT(signatures(Sig0).signatures, + ElementsAre(Sig("foo(T, T, T) -> void", {"T", "T", "T"}))); + + StringRef Sig1 = R"cpp( + template <class T> + void foo(T, T, T); + + int main() { + foo(10, ^); + })cpp"; + + EXPECT_THAT(signatures(Sig1).signatures, + ElementsAre(Sig("foo(T, T, T) -> void", {"T", "T", "T"}))); + + StringRef Sig2 = R"cpp( + template <class ...T> + void foo(T...); + + int main() { + foo<int>(^); + } + )cpp"; + + EXPECT_THAT(signatures(Sig2).signatures, + ElementsAre(Sig("foo(T...) -> void", {"T..."}))); + + // It is debatable whether we should substitute the outer template parameter + // ('T') in that case. Currently we don't substitute it in signature help, but + // do substitute in code complete. + // FIXME: make code complete and signature help consistent, figure out which + // way is better. + StringRef Sig3 = R"cpp( + template <class T> + struct X { + template <class U> + void foo(T, U); + }; + + int main() { + X<int>().foo<double>(^) + } + )cpp"; + + EXPECT_THAT(signatures(Sig3).signatures, + ElementsAre(Sig("foo(T, U) -> void", {"T", "U"}))); +} + +TEST(SignatureHelpTest, IndexDocumentation) { + Symbol Foo0 = sym("foo", index::SymbolKind::Function, "@F@\\0#"); + Foo0.Documentation = "Doc from the index"; + Symbol Foo1 = sym("foo", index::SymbolKind::Function, "@F@\\0#I#"); + Foo1.Documentation = "Doc from the index"; + Symbol Foo2 = sym("foo", index::SymbolKind::Function, "@F@\\0#I#I#"); + + StringRef Sig0 = R"cpp( + int foo(); + int foo(double); + + void test() { + foo(^); + } + )cpp"; + + EXPECT_THAT( + signatures(Sig0, {Foo0}).signatures, + ElementsAre(AllOf(Sig("foo() -> int", {}), SigDoc("Doc from the index")), + AllOf(Sig("foo(double) -> int", {"double"}), SigDoc("")))); + + StringRef Sig1 = R"cpp( + int foo(); + // Overriden doc from sema + int foo(int); + // Doc from sema + int foo(int, int); + + void test() { + foo(^); + } + )cpp"; + + EXPECT_THAT( + signatures(Sig1, {Foo0, Foo1, Foo2}).signatures, + ElementsAre(AllOf(Sig("foo() -> int", {}), SigDoc("Doc from the index")), + AllOf(Sig("foo(int) -> int", {"int"}), + SigDoc("Overriden doc from sema")), + AllOf(Sig("foo(int, int) -> int", {"int", "int"}), + SigDoc("Doc from sema")))); +} + +TEST(SignatureHelpTest, DynamicIndexDocumentation) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer::Options Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + + FS.Files[testPath("foo.h")] = R"cpp( + struct Foo { + // Member doc + int foo(); + }; + )cpp"; + Annotations FileContent(R"cpp( + #include "foo.h" + void test() { + Foo f; + f.foo(^); + } + )cpp"); + auto File = testPath("test.cpp"); + Server.addDocument(File, FileContent.code()); + // Wait for the dynamic index being built. + ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT( + llvm::cantFail(runSignatureHelp(Server, File, FileContent.point())) + .signatures, + ElementsAre(AllOf(Sig("foo() -> int", {}), SigDoc("Member doc")))); +} + +TEST(CompletionTest, CompletionFunctionArgsDisabled) { + CodeCompleteOptions Opts; + Opts.EnableSnippets = true; + Opts.EnableFunctionArgSnippets = false; + + { + auto Results = completions( + R"cpp( + void xfoo(); + void xfoo(int x, int y); + void f() { xfo^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("xfoo"), SnippetSuffix("()")), + AllOf(Named("xfoo"), SnippetSuffix("($0)")))); + } + { + auto Results = completions( + R"cpp( + void xbar(); + void f() { xba^ })cpp", + {}, Opts); + EXPECT_THAT(Results.Completions, UnorderedElementsAre(AllOf( + Named("xbar"), SnippetSuffix("()")))); + } + { + Opts.BundleOverloads = true; + auto Results = completions( + R"cpp( + void xfoo(); + void xfoo(int x, int y); + void f() { xfo^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("xfoo"), SnippetSuffix("($0)")))); + } + { + auto Results = completions( + R"cpp( + template <class T, class U> + void xfoo(int a, U b); + void f() { xfo^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("xfoo"), SnippetSuffix("<$1>($0)")))); + } + { + auto Results = completions( + R"cpp( + template <class T> + class foo_class{}; + template <class T> + using foo_alias = T**; + void f() { foo_^ })cpp", + {}, Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Named("foo_class"), SnippetSuffix("<$0>")), + AllOf(Named("foo_alias"), SnippetSuffix("<$0>")))); + } +} + +TEST(CompletionTest, SuggestOverrides) { + constexpr const char *const Text(R"cpp( + class A { + public: + virtual void vfunc(bool param); + virtual void vfunc(bool param, int p); + void func(bool param); + }; + class B : public A { + virtual void ttt(bool param) const; + void vfunc(bool param, int p) override; + }; + class C : public B { + public: + void vfunc(bool param) override; + ^ + }; + )cpp"); + const auto Results = completions(Text); + EXPECT_THAT(Results.Completions, + AllOf(Contains(Labeled("void vfunc(bool param, int p) override")), + Contains(Labeled("void ttt(bool param) const override")), + Not(Contains(Labeled("void vfunc(bool param) override"))))); +} + +TEST(CompletionTest, OverridesNonIdentName) { + // Check the completions call does not crash. + completions(R"cpp( + struct Base { + virtual ~Base() = 0; + virtual operator int() = 0; + virtual Base& operator+(Base&) = 0; + }; + + struct Derived : Base { + ^ + }; + )cpp"); +} + +TEST(GuessCompletionPrefix, Filters) { + for (llvm::StringRef Case : { + "[[scope::]][[ident]]^", + "[[]][[]]^", + "\n[[]][[]]^", + "[[]][[ab]]^", + "x.[[]][[ab]]^", + "x.[[]][[]]^", + "[[x::]][[ab]]^", + "[[x::]][[]]^", + "[[::x::]][[ab]]^", + "some text [[scope::more::]][[identif]]^ier", + "some text [[scope::]][[mor]]^e::identifier", + "weird case foo::[[::bar::]][[baz]]^", + }) { + Annotations F(Case); + auto Offset = cantFail(positionToOffset(F.code(), F.point())); + auto ToStringRef = [&](Range R) { + return F.code().slice(cantFail(positionToOffset(F.code(), R.start)), + cantFail(positionToOffset(F.code(), R.end))); + }; + auto WantQualifier = ToStringRef(F.ranges()[0]), + WantName = ToStringRef(F.ranges()[1]); + + auto Prefix = guessCompletionPrefix(F.code(), Offset); + // Even when components are empty, check their offsets are correct. + EXPECT_EQ(WantQualifier, Prefix.Qualifier) << Case; + EXPECT_EQ(WantQualifier.begin(), Prefix.Qualifier.begin()) << Case; + EXPECT_EQ(WantName, Prefix.Name) << Case; + EXPECT_EQ(WantName.begin(), Prefix.Name.begin()) << Case; + } +} + +TEST(CompletionTest, EnableSpeculativeIndexRequest) { + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto File = testPath("foo.cpp"); + Annotations Test(R"cpp( + namespace ns1 { int abc; } + namespace ns2 { int abc; } + void f() { ns1::ab$1^; ns1::ab$2^; } + void f2() { ns2::ab$3^; } + )cpp"); + runAddDocument(Server, File, Test.code()); + clangd::CodeCompleteOptions Opts = {}; + + IndexRequestCollector Requests; + Opts.Index = &Requests; + Opts.SpeculativeIndexRequest = true; + + auto CompleteAtPoint = [&](StringRef P) { + cantFail(runCodeComplete(Server, File, Test.point(P), Opts)); + // Sleep for a while to make sure asynchronous call (if applicable) is also + // triggered before callback is invoked. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + }; + + CompleteAtPoint("1"); + auto Reqs1 = Requests.consumeRequests(); + ASSERT_EQ(Reqs1.size(), 1u); + EXPECT_THAT(Reqs1[0].Scopes, UnorderedElementsAre("ns1::")); + + CompleteAtPoint("2"); + auto Reqs2 = Requests.consumeRequests(); + // Speculation succeeded. Used speculative index result. + ASSERT_EQ(Reqs2.size(), 1u); + EXPECT_EQ(Reqs2[0], Reqs1[0]); + + CompleteAtPoint("3"); + // Speculation failed. Sent speculative index request and the new index + // request after sema. + auto Reqs3 = Requests.consumeRequests(); + ASSERT_EQ(Reqs3.size(), 2u); +} + +TEST(CompletionTest, InsertTheMostPopularHeader) { + std::string DeclFile = URI::create(testPath("foo")).toString(); + Symbol sym = func("Func"); + sym.CanonicalDeclaration.FileURI = DeclFile.c_str(); + sym.IncludeHeaders.emplace_back("\"foo.h\"", 2); + sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000); + + auto Results = completions("Fun^", {sym}).Completions; + assert(!Results.empty()); + EXPECT_THAT(Results[0], AllOf(Named("Func"), InsertInclude("\"bar.h\""))); + EXPECT_EQ(Results[0].Includes.size(), 2u); +} + +TEST(CompletionTest, NoInsertIncludeIfOnePresent) { + MockFSProvider FS; + MockCompilationDatabase CDB; + + std::string FooHeader = testPath("foo.h"); + FS.Files[FooHeader] = ""; + + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + std::string DeclFile = URI::create(testPath("foo")).toString(); + Symbol sym = func("Func"); + sym.CanonicalDeclaration.FileURI = DeclFile.c_str(); + sym.IncludeHeaders.emplace_back("\"foo.h\"", 2); + sym.IncludeHeaders.emplace_back("\"bar.h\"", 1000); + + EXPECT_THAT( + completions(Server, "#include \"foo.h\"\nFun^", {sym}).Completions, + UnorderedElementsAre( + AllOf(Named("Func"), HasInclude("\"foo.h\""), Not(InsertInclude())))); +} + +TEST(CompletionTest, MergeMacrosFromIndexAndSema) { + Symbol Sym; + Sym.Name = "Clangd_Macro_Test"; + Sym.ID = SymbolID("c:foo.cpp@8@macro@Clangd_Macro_Test"); + Sym.SymInfo.Kind = index::SymbolKind::Macro; + Sym.Flags |= Symbol::IndexedForCodeCompletion; + EXPECT_THAT(completions("#define Clangd_Macro_Test\nClangd_Macro_T^", {Sym}) + .Completions, + UnorderedElementsAre(Named("Clangd_Macro_Test"))); +} + +TEST(CompletionTest, MacroFromPreamble) { + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string FooHeader = testPath("foo.h"); + FS.Files[FooHeader] = "#define CLANGD_PREAMBLE_HEADER x\n"; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + auto Results = completions( + R"cpp(#include "foo.h" + #define CLANGD_PREAMBLE_MAIN x + + int x = 0; + #define CLANGD_MAIN x + void f() { CLANGD_^ } + )cpp", + {func("CLANGD_INDEX")}); + // We should get results from the main file, including the preamble section. + // However no results from included files (the index should cover them). + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("CLANGD_PREAMBLE_MAIN"), + Named("CLANGD_MAIN"), + Named("CLANGD_INDEX"))); +} + +TEST(CompletionTest, DeprecatedResults) { + std::string Body = R"cpp( + void TestClangd(); + void TestClangc() __attribute__((deprecated("", ""))); + )cpp"; + + EXPECT_THAT( + completions(Body + "int main() { TestClang^ }").Completions, + UnorderedElementsAre(AllOf(Named("TestClangd"), Not(Deprecated())), + AllOf(Named("TestClangc"), Deprecated()))); +} + +TEST(SignatureHelpTest, InsideArgument) { + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int main() { foo(1+^); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre(Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + EXPECT_EQ(0, Results.activeParameter); + } + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int main() { foo(1^); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre(Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + EXPECT_EQ(0, Results.activeParameter); + } + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int main() { foo(1^0); } + )cpp"); + EXPECT_THAT( + Results.signatures, + ElementsAre(Sig("foo(int x) -> void", {"int x"}), + Sig("foo(int x, int y) -> void", {"int x", "int y"}))); + EXPECT_EQ(0, Results.activeParameter); + } + { + const auto Results = signatures(R"cpp( + void foo(int x); + void foo(int x, int y); + int bar(int x, int y); + int main() { bar(foo(2, 3^)); } + )cpp"); + EXPECT_THAT(Results.signatures, ElementsAre(Sig("foo(int x, int y) -> void", + {"int x", "int y"}))); + EXPECT_EQ(1, Results.activeParameter); + } +} + +TEST(SignatureHelpTest, ConstructorInitializeFields) { + { + const auto Results = signatures(R"cpp( + struct A { + A(int); + }; + struct B { + B() : a_elem(^) {} + A a_elem; + }; + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre(Sig("A(int)", {"int"}), + Sig("A(A &&)", {"A &&"}), + Sig("A(const A &)", {"const A &"}))); + } + { + const auto Results = signatures(R"cpp( + struct A { + A(int); + }; + struct C { + C(int); + C(A); + }; + struct B { + B() : c_elem(A(1^)) {} + C c_elem; + }; + )cpp"); + EXPECT_THAT(Results.signatures, + UnorderedElementsAre(Sig("A(int)", {"int"}), + Sig("A(A &&)", {"A &&"}), + Sig("A(const A &)", {"const A &"}))); + } +} + +TEST(CompletionTest, IncludedCompletionKinds) { + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string Subdir = testPath("sub"); + std::string SearchDirArg = (Twine("-I") + Subdir).str(); + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = ""; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + auto Results = completions(Server, + R"cpp( + #include "^" + )cpp"); + EXPECT_THAT(Results.Completions, + AllOf(Has("sub/", CompletionItemKind::Folder), + Has("bar.h\"", CompletionItemKind::File))); +} + +TEST(CompletionTest, NoCrashAtNonAlphaIncludeHeader) { + auto Results = completions( + R"cpp( + #include "./^" + )cpp"); + EXPECT_TRUE(Results.Completions.empty()); +} + +TEST(CompletionTest, NoAllScopesCompletionWhenQualified) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions( + R"cpp( + void f() { na::Clangd^ } + )cpp", + {cls("na::ClangdA"), cls("nx::ClangdX"), cls("Clangd3")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre( + AllOf(Qualifier(""), Scope("na::"), Named("ClangdA")))); +} + +TEST(CompletionTest, AllScopesCompletion) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions( + R"cpp( + namespace na { + void f() { Clangd^ } + } + )cpp", + {cls("nx::Clangd1"), cls("ny::Clangd2"), cls("Clangd3"), + cls("na::nb::Clangd4")}, + Opts); + EXPECT_THAT( + Results.Completions, + UnorderedElementsAre(AllOf(Qualifier("nx::"), Named("Clangd1")), + AllOf(Qualifier("ny::"), Named("Clangd2")), + AllOf(Qualifier(""), Scope(""), Named("Clangd3")), + AllOf(Qualifier("nb::"), Named("Clangd4")))); +} + +TEST(CompletionTest, NoQualifierIfShadowed) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions(R"cpp( + namespace nx { class Clangd1 {}; } + using nx::Clangd1; + void f() { Clangd^ } + )cpp", + {cls("nx::Clangd1"), cls("nx::Clangd2")}, Opts); + // Although Clangd1 is from another namespace, Sema tells us it's in-scope and + // needs no qualifier. + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("Clangd1")), + AllOf(Qualifier("nx::"), Named("Clangd2")))); +} + +TEST(CompletionTest, NoCompletionsForNewNames) { + clangd::CodeCompleteOptions Opts; + Opts.AllScopes = true; + auto Results = completions(R"cpp( + void f() { int n^ } + )cpp", + {cls("naber"), cls("nx::naber")}, Opts); + EXPECT_THAT(Results.Completions, UnorderedElementsAre()); +} + +TEST(CompletionTest, ObjectiveCMethodNoArguments) { + auto Results = completions(R"objc( + @interface Foo + @property(nonatomic, setter=setXToIgnoreComplete:) int value; + @end + Foo *foo = [Foo new]; int y = [foo v^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("value"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("int"))); + EXPECT_THAT(C, ElementsAre(Signature(""))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix(""))); +} + +TEST(CompletionTest, ObjectiveCMethodOneArgument) { + auto Results = completions(R"objc( + @interface Foo + - (int)valueForCharacter:(char)c; + @end + Foo *foo = [Foo new]; int y = [foo v^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("valueForCharacter:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("int"))); + EXPECT_THAT(C, ElementsAre(Signature("(char)"))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix("${1:(char)}"))); +} + +TEST(CompletionTest, ObjectiveCMethodTwoArgumentsFromBeginning) { + auto Results = completions(R"objc( + @interface Foo + + (id)fooWithValue:(int)value fooey:(unsigned int)fooey; + @end + id val = [Foo foo^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("fooWithValue:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("id"))); + EXPECT_THAT(C, ElementsAre(Signature("(int) fooey:(unsigned int)"))); + EXPECT_THAT( + C, ElementsAre(SnippetSuffix("${1:(int)} fooey:${2:(unsigned int)}"))); +} + +TEST(CompletionTest, ObjectiveCMethodTwoArgumentsFromMiddle) { + auto Results = completions(R"objc( + @interface Foo + + (id)fooWithValue:(int)value fooey:(unsigned int)fooey; + @end + id val = [Foo fooWithValue:10 f^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("fooey:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("id"))); + EXPECT_THAT(C, ElementsAre(Signature("(unsigned int)"))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix("${1:(unsigned int)}"))); +} + +TEST(CompletionTest, WorksWithNullType) { + auto R = completions(R"cpp( + int main() { + for (auto [loopVar] : y ) { // y has to be unresolved. + int z = loopV^; + } + } + )cpp"); + EXPECT_THAT(R.Completions, ElementsAre(Named("loopVar"))); +} + +TEST(CompletionTest, UsingDecl) { + const char *Header(R"cpp( + void foo(int); + namespace std { + using ::foo; + })cpp"); + const char *Source(R"cpp( + void bar() { + std::^; + })cpp"); + auto Index = TestTU::withHeaderCode(Header).index(); + clangd::CodeCompleteOptions Opts; + Opts.Index = Index.get(); + Opts.AllScopes = true; + auto R = completions(Source, {}, Opts); + EXPECT_THAT(R.Completions, + ElementsAre(AllOf(Scope("std::"), Named("foo"), + Kind(CompletionItemKind::Reference)))); +} + +TEST(CompletionTest, ScopeIsUnresolved) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions(R"cpp( + namespace a { + void f() { b::X^ } + } + )cpp", + {cls("a::b::XYZ")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("XYZ")))); +} + +TEST(CompletionTest, NestedScopeIsUnresolved) { + clangd::CodeCompleteOptions Opts = {}; + Opts.AllScopes = true; + + auto Results = completions(R"cpp( + namespace a { + namespace b {} + void f() { b::c::X^ } + } + )cpp", + {cls("a::b::c::XYZ")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("XYZ")))); +} + +// Clang parser gets confused here and doesn't report the ns:: prefix. +// Naive behavior is to insert it again. We examine the source and recover. +TEST(CompletionTest, NamespaceDoubleInsertion) { + clangd::CodeCompleteOptions Opts = {}; + + auto Results = completions(R"cpp( + namespace foo { + namespace ns {} + #define M(X) < X + M(ns::ABC^ + } + )cpp", + {cls("foo::ns::ABCDE")}, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Named("ABCDE")))); +} + +TEST(NoCompileCompletionTest, Basic) { + auto Results = completionsNoCompile(R"cpp( + void func() { + int xyz; + int abc; + ^ + } + )cpp"); + EXPECT_FALSE(Results.RanParser); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("void"), Named("func"), Named("int"), + Named("xyz"), Named("abc"))); +} + +TEST(NoCompileCompletionTest, WithFilter) { + auto Results = completionsNoCompile(R"cpp( + void func() { + int sym1; + int sym2; + int xyz1; + int xyz2; + sy^ + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("sym1"), Named("sym2"))); +} + +TEST(NoCompileCompletionTest, WithIndex) { + std::vector<Symbol> Syms = {func("xxx"), func("a::xxx"), func("ns::b::xxx"), + func("c::xxx"), func("ns::d::xxx")}; + auto Results = completionsNoCompile( + R"cpp( + // Current-scopes, unqualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + xx^ + } + )cpp", + Syms); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Scope("")), + AllOf(Qualifier(""), Scope("a::")), + AllOf(Qualifier(""), Scope("ns::b::")))); + CodeCompleteOptions Opts; + Opts.AllScopes = true; + Results = completionsNoCompile( + R"cpp( + // All-scopes unqualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + xx^ + } + )cpp", + Syms, Opts); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(AllOf(Qualifier(""), Scope("")), + AllOf(Qualifier(""), Scope("a::")), + AllOf(Qualifier(""), Scope("ns::b::")), + AllOf(Qualifier("c::"), Scope("c::")), + AllOf(Qualifier("d::"), Scope("ns::d::")))); + Results = completionsNoCompile( + R"cpp( + // Qualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + b::xx^ + } + )cpp", + Syms, Opts); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Qualifier(""), Scope("ns::b::")))); + Results = completionsNoCompile( + R"cpp( + // Absolutely qualified completion. + using namespace a; + namespace ns { + using namespace b; + void foo() { + ::a::xx^ + } + )cpp", + Syms, Opts); + EXPECT_THAT(Results.Completions, + ElementsAre(AllOf(Qualifier(""), Scope("a::")))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/CodeCompletionStringsTests.cpp b/clangd/unittests/CodeCompletionStringsTests.cpp new file mode 100644 index 00000000..43429c86 --- /dev/null +++ b/clangd/unittests/CodeCompletionStringsTests.cpp @@ -0,0 +1,160 @@ +//===-- CodeCompletionStringsTests.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 "CodeCompletionStrings.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +class CompletionStringTest : public ::testing::Test { +public: + CompletionStringTest() + : Allocator(std::make_shared<clang::GlobalCodeCompletionAllocator>()), + CCTUInfo(Allocator), Builder(*Allocator, CCTUInfo) {} + +protected: + void computeSignature(const CodeCompletionString &CCS) { + Signature.clear(); + Snippet.clear(); + getSignature(CCS, &Signature, &Snippet); + } + + std::shared_ptr<clang::GlobalCodeCompletionAllocator> Allocator; + CodeCompletionTUInfo CCTUInfo; + CodeCompletionBuilder Builder; + std::string Signature; + std::string Snippet; +}; + +TEST_F(CompletionStringTest, ReturnType) { + Builder.AddResultTypeChunk("result"); + Builder.AddResultTypeChunk("redundant result no no"); + EXPECT_EQ(getReturnType(*Builder.TakeString()), "result"); +} + +TEST_F(CompletionStringTest, Documentation) { + Builder.addBriefComment("This is ignored"); + EXPECT_EQ(formatDocumentation(*Builder.TakeString(), "Is this brief?"), + "Is this brief?"); +} + +TEST_F(CompletionStringTest, DocumentationWithAnnotation) { + Builder.addBriefComment("This is ignored"); + Builder.AddAnnotation("Ano"); + EXPECT_EQ(formatDocumentation(*Builder.TakeString(), "Is this brief?"), + "Annotation: Ano\n\nIs this brief?"); +} + +TEST_F(CompletionStringTest, MultipleAnnotations) { + Builder.AddAnnotation("Ano1"); + Builder.AddAnnotation("Ano2"); + Builder.AddAnnotation("Ano3"); + + EXPECT_EQ(formatDocumentation(*Builder.TakeString(), ""), + "Annotations: Ano1 Ano2 Ano3\n"); +} + +TEST_F(CompletionStringTest, EmptySignature) { + Builder.AddTypedTextChunk("X"); + Builder.AddResultTypeChunk("result no no"); + computeSignature(*Builder.TakeString()); + EXPECT_EQ(Signature, ""); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, Function) { + Builder.AddResultTypeChunk("result no no"); + Builder.addBriefComment("This comment is ignored"); + Builder.AddTypedTextChunk("Foo"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("p1"); + Builder.AddChunk(CodeCompletionString::CK_Comma); + Builder.AddPlaceholderChunk("p2"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(p1, p2)"); + EXPECT_EQ(Snippet, "(${1:p1}, ${2:p2})"); + EXPECT_EQ(formatDocumentation(*CCS, "Foo's comment"), "Foo's comment"); +} + +TEST_F(CompletionStringTest, EscapeSnippet) { + Builder.AddTypedTextChunk("Foo"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("$p}1\\"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + + computeSignature(*Builder.TakeString()); + EXPECT_EQ(Signature, "($p}1\\)"); + EXPECT_EQ(Snippet, "(${1:\\$p\\}1\\\\})"); +} + +TEST_F(CompletionStringTest, IgnoreInformativeQualifier) { + Builder.AddTypedTextChunk("X"); + Builder.AddInformativeChunk("info ok"); + Builder.AddInformativeChunk("info no no::"); + computeSignature(*Builder.TakeString()); + EXPECT_EQ(Signature, "info ok"); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodNoArguments) { + Builder.AddResultTypeChunk("void"); + Builder.AddTypedTextChunk("methodName"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, ""); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodOneArgument) { + Builder.AddResultTypeChunk("void"); + Builder.AddTypedTextChunk("methodWithArg:"); + Builder.AddPlaceholderChunk("(type)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type)"); + EXPECT_EQ(Snippet, "${1:(type)}"); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromBeginning) { + Builder.AddResultTypeChunk("int"); + Builder.AddTypedTextChunk("withFoo:"); + Builder.AddPlaceholderChunk("(type)"); + Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace); + Builder.AddTypedTextChunk("bar:"); + Builder.AddPlaceholderChunk("(type2)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type) bar:(type2)"); + EXPECT_EQ(Snippet, "${1:(type)} bar:${2:(type2)}"); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromMiddle) { + Builder.AddResultTypeChunk("int"); + Builder.AddInformativeChunk("withFoo:"); + Builder.AddTypedTextChunk("bar:"); + Builder.AddPlaceholderChunk("(type2)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type2)"); + EXPECT_EQ(Snippet, "${1:(type2)}"); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/ContextTests.cpp b/clangd/unittests/ContextTests.cpp new file mode 100644 index 00000000..d760f4eb --- /dev/null +++ b/clangd/unittests/ContextTests.cpp @@ -0,0 +1,56 @@ +//===-- ContextTests.cpp - Context tests ------------------------*- 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 "Context.h" + +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +TEST(ContextTests, Simple) { + Key<int> IntParam; + Key<int> ExtraIntParam; + + Context Ctx = Context::empty().derive(IntParam, 10).derive(ExtraIntParam, 20); + + EXPECT_EQ(*Ctx.get(IntParam), 10); + EXPECT_EQ(*Ctx.get(ExtraIntParam), 20); +} + +TEST(ContextTests, MoveOps) { + Key<std::unique_ptr<int>> Param; + + Context Ctx = Context::empty().derive(Param, llvm::make_unique<int>(10)); + EXPECT_EQ(**Ctx.get(Param), 10); + + Context NewCtx = std::move(Ctx); + EXPECT_EQ(**NewCtx.get(Param), 10); +} + +TEST(ContextTests, Builders) { + Key<int> ParentParam; + Key<int> ParentAndChildParam; + Key<int> ChildParam; + + Context ParentCtx = + Context::empty().derive(ParentParam, 10).derive(ParentAndChildParam, 20); + Context ChildCtx = + ParentCtx.derive(ParentAndChildParam, 30).derive(ChildParam, 40); + + EXPECT_EQ(*ParentCtx.get(ParentParam), 10); + EXPECT_EQ(*ParentCtx.get(ParentAndChildParam), 20); + EXPECT_EQ(ParentCtx.get(ChildParam), nullptr); + + EXPECT_EQ(*ChildCtx.get(ParentParam), 10); + EXPECT_EQ(*ChildCtx.get(ParentAndChildParam), 30); + EXPECT_EQ(*ChildCtx.get(ChildParam), 40); +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/DexTests.cpp b/clangd/unittests/DexTests.cpp new file mode 100644 index 00000000..11aedec5 --- /dev/null +++ b/clangd/unittests/DexTests.cpp @@ -0,0 +1,753 @@ +//===-- DexTests.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 "FuzzyMatch.h" +#include "TestFS.h" +#include "TestIndex.h" +#include "index/Index.h" +#include "index/Merge.h" +#include "index/SymbolID.h" +#include "index/dex/Dex.h" +#include "index/dex/Iterator.h" +#include "index/dex/Token.h" +#include "index/dex/Trigram.h" +#include "llvm/Support/ScopedPrinter.h" +#include "llvm/Support/raw_ostream.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <string> +#include <vector> + +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +namespace clang { +namespace clangd { +namespace dex { +namespace { + +//===----------------------------------------------------------------------===// +// Query iterator tests. +//===----------------------------------------------------------------------===// + +std::vector<DocID> consumeIDs(Iterator &It) { + auto IDAndScore = consume(It); + std::vector<DocID> IDs(IDAndScore.size()); + for (size_t I = 0; I < IDAndScore.size(); ++I) + IDs[I] = IDAndScore[I].first; + return IDs; +} + +TEST(DexIterators, DocumentIterator) { + const PostingList L({4, 7, 8, 20, 42, 100}); + auto DocIterator = L.iterator(); + + EXPECT_EQ(DocIterator->peek(), 4U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advance(); + EXPECT_EQ(DocIterator->peek(), 7U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advanceTo(20); + EXPECT_EQ(DocIterator->peek(), 20U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advanceTo(65); + EXPECT_EQ(DocIterator->peek(), 100U); + EXPECT_FALSE(DocIterator->reachedEnd()); + + DocIterator->advanceTo(420); + EXPECT_TRUE(DocIterator->reachedEnd()); +} + +TEST(DexIterators, AndTwoLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + + auto And = C.intersect(L1.iterator(), L0.iterator()); + + EXPECT_FALSE(And->reachedEnd()); + EXPECT_THAT(consumeIDs(*And), ElementsAre(0U, 7U, 10U, 320U, 9000U)); + + And = C.intersect(L0.iterator(), L1.iterator()); + + And->advanceTo(0); + EXPECT_EQ(And->peek(), 0U); + And->advanceTo(5); + EXPECT_EQ(And->peek(), 7U); + And->advanceTo(10); + EXPECT_EQ(And->peek(), 10U); + And->advanceTo(42); + EXPECT_EQ(And->peek(), 320U); + And->advanceTo(8999); + EXPECT_EQ(And->peek(), 9000U); + And->advanceTo(9001); +} + +TEST(DexIterators, AndThreeLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + const PostingList L2({1, 4, 7, 11, 30, 60, 320, 9000}); + + auto And = C.intersect(L0.iterator(), L1.iterator(), L2.iterator()); + EXPECT_EQ(And->peek(), 7U); + And->advanceTo(300); + EXPECT_EQ(And->peek(), 320U); + And->advanceTo(100000); + + EXPECT_TRUE(And->reachedEnd()); +} + +TEST(DexIterators, AndEmpty) { + Corpus C{10000}; + const PostingList L1{1}; + const PostingList L2{2}; + // These iterators are empty, but the optimizer can't tell. + auto Empty1 = C.intersect(L1.iterator(), L2.iterator()); + auto Empty2 = C.intersect(L1.iterator(), L2.iterator()); + // And syncs iterators on construction, and used to fail on empty children. + auto And = C.intersect(std::move(Empty1), std::move(Empty2)); + EXPECT_TRUE(And->reachedEnd()); +} + +TEST(DexIterators, OrTwoLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + + auto Or = C.unionOf(L0.iterator(), L1.iterator()); + + EXPECT_FALSE(Or->reachedEnd()); + EXPECT_EQ(Or->peek(), 0U); + Or->advance(); + EXPECT_EQ(Or->peek(), 4U); + Or->advance(); + EXPECT_EQ(Or->peek(), 5U); + Or->advance(); + EXPECT_EQ(Or->peek(), 7U); + Or->advance(); + EXPECT_EQ(Or->peek(), 10U); + Or->advance(); + EXPECT_EQ(Or->peek(), 30U); + Or->advanceTo(42); + EXPECT_EQ(Or->peek(), 42U); + Or->advanceTo(300); + EXPECT_EQ(Or->peek(), 320U); + Or->advanceTo(9000); + EXPECT_EQ(Or->peek(), 9000U); + Or->advanceTo(9001); + EXPECT_TRUE(Or->reachedEnd()); + + Or = C.unionOf(L0.iterator(), L1.iterator()); + + EXPECT_THAT(consumeIDs(*Or), + ElementsAre(0U, 4U, 5U, 7U, 10U, 30U, 42U, 60U, 320U, 9000U)); +} + +TEST(DexIterators, OrThreeLists) { + Corpus C{10000}; + const PostingList L0({0, 5, 7, 10, 42, 320, 9000}); + const PostingList L1({0, 4, 7, 10, 30, 60, 320, 9000}); + const PostingList L2({1, 4, 7, 11, 30, 60, 320, 9000}); + + auto Or = C.unionOf(L0.iterator(), L1.iterator(), L2.iterator()); + + EXPECT_FALSE(Or->reachedEnd()); + EXPECT_EQ(Or->peek(), 0U); + + Or->advance(); + EXPECT_EQ(Or->peek(), 1U); + + Or->advance(); + EXPECT_EQ(Or->peek(), 4U); + + Or->advanceTo(7); + + Or->advanceTo(59); + EXPECT_EQ(Or->peek(), 60U); + + Or->advanceTo(9001); + EXPECT_TRUE(Or->reachedEnd()); +} + +// FIXME(kbobyrev): The testcase below is similar to what is expected in real +// queries. It should be updated once new iterators (such as boosting, limiting, +// etc iterators) appear. However, it is not exhaustive and it would be +// beneficial to implement automatic generation (e.g. fuzzing) of query trees +// for more comprehensive testing. +TEST(DexIterators, QueryTree) { + // + // +-----------------+ + // |And Iterator:1, 5| + // +--------+--------+ + // | + // | + // +-------------+----------------------+ + // | | + // | | + // +----------v----------+ +----------v------------+ + // |And Iterator: 1, 5, 9| |Or Iterator: 0, 1, 3, 5| + // +----------+----------+ +----------+------------+ + // | | + // +------+-----+ ------------+ + // | | | | + // +-------v-----+ +----+---+ +---v----+ +----v---+ + // |1, 3, 5, 8, 9| |Boost: 2| |Boost: 3| |Boost: 4| + // +-------------+ +----+---+ +---+----+ +----+---+ + // | | | + // +----v-----+ +-v--+ +---v---+ + // |1, 5, 7, 9| |1, 5| |0, 3, 5| + // +----------+ +----+ +-------+ + // + Corpus C{10}; + const PostingList L0({1, 3, 5, 8, 9}); + const PostingList L1({1, 5, 7, 9}); + const PostingList L2({1, 5}); + const PostingList L3({0, 3, 5}); + + // Root of the query tree: [1, 5] + auto Root = C.intersect( + // Lower And Iterator: [1, 5, 9] + C.intersect(L0.iterator(), C.boost(L1.iterator(), 2U)), + // Lower Or Iterator: [0, 1, 5] + C.unionOf(C.boost(L2.iterator(), 3U), C.boost(L3.iterator(), 4U))); + + EXPECT_FALSE(Root->reachedEnd()); + EXPECT_EQ(Root->peek(), 1U); + Root->advanceTo(0); + // Advance multiple times. Shouldn't do anything. + Root->advanceTo(1); + Root->advanceTo(0); + EXPECT_EQ(Root->peek(), 1U); + auto ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 6); + Root->advance(); + EXPECT_EQ(Root->peek(), 5U); + Root->advanceTo(5); + EXPECT_EQ(Root->peek(), 5U); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 8); + Root->advanceTo(9000); + EXPECT_TRUE(Root->reachedEnd()); +} + +TEST(DexIterators, StringRepresentation) { + Corpus C{10}; + const PostingList L1({1, 3, 5}); + const PostingList L2({1, 7, 9}); + + // No token given, prints full posting list. + auto I1 = L1.iterator(); + EXPECT_EQ(llvm::to_string(*I1), "[1 3 5]"); + + // Token given, uses token's string representation. + Token Tok(Token::Kind::Trigram, "L2"); + auto I2 = L1.iterator(&Tok); + EXPECT_EQ(llvm::to_string(*I2), "T=L2"); + + auto Tree = C.limit(C.intersect(move(I1), move(I2)), 10); + // AND reorders its children, we don't care which order it prints. + EXPECT_THAT(llvm::to_string(*Tree), AnyOf("(LIMIT 10 (& [1 3 5] T=L2))", + "(LIMIT 10 (& T=L2 [1 3 5]))")); +} + +TEST(DexIterators, Limit) { + Corpus C{10000}; + const PostingList L0({3, 6, 7, 20, 42, 100}); + const PostingList L1({1, 3, 5, 6, 7, 30, 100}); + const PostingList L2({0, 3, 5, 7, 8, 100}); + + auto DocIterator = C.limit(L0.iterator(), 42); + EXPECT_THAT(consumeIDs(*DocIterator), ElementsAre(3, 6, 7, 20, 42, 100)); + + DocIterator = C.limit(L0.iterator(), 3); + EXPECT_THAT(consumeIDs(*DocIterator), ElementsAre(3, 6, 7)); + + DocIterator = C.limit(L0.iterator(), 0); + EXPECT_THAT(consumeIDs(*DocIterator), ElementsAre()); + + auto AndIterator = + C.intersect(C.limit(C.all(), 343), C.limit(L0.iterator(), 2), + C.limit(L1.iterator(), 3), C.limit(L2.iterator(), 42)); + EXPECT_THAT(consumeIDs(*AndIterator), ElementsAre(3, 7)); +} + +TEST(DexIterators, True) { + EXPECT_TRUE(Corpus{0}.all()->reachedEnd()); + EXPECT_THAT(consumeIDs(*Corpus{4}.all()), ElementsAre(0, 1, 2, 3)); +} + +TEST(DexIterators, Boost) { + Corpus C{5}; + auto BoostIterator = C.boost(C.all(), 42U); + EXPECT_FALSE(BoostIterator->reachedEnd()); + auto ElementBoost = BoostIterator->consume(); + EXPECT_THAT(ElementBoost, 42U); + + const PostingList L0({2, 4}); + const PostingList L1({1, 4}); + auto Root = C.unionOf(C.all(), C.boost(L0.iterator(), 2U), + C.boost(L1.iterator(), 3U)); + + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 1); + Root->advance(); + EXPECT_THAT(Root->peek(), 1U); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 3); + + Root->advance(); + EXPECT_THAT(Root->peek(), 2U); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 2); + + Root->advanceTo(4); + ElementBoost = Root->consume(); + EXPECT_THAT(ElementBoost, 3); +} + +TEST(DexIterators, Optimizations) { + Corpus C{5}; + const PostingList L1{1}; + const PostingList L2{2}; + const PostingList L3{3}; + + // empty and/or yield true/false + EXPECT_EQ(llvm::to_string(*C.intersect()), "true"); + EXPECT_EQ(llvm::to_string(*C.unionOf()), "false"); + + // true/false inside and/or short-circuit + EXPECT_EQ(llvm::to_string(*C.intersect(L1.iterator(), C.all())), "[1]"); + EXPECT_EQ(llvm::to_string(*C.intersect(L1.iterator(), C.none())), "false"); + // Not optimized to avoid breaking boosts. + EXPECT_EQ(llvm::to_string(*C.unionOf(L1.iterator(), C.all())), + "(| [1] true)"); + EXPECT_EQ(llvm::to_string(*C.unionOf(L1.iterator(), C.none())), "[1]"); + + // and/or nested inside and/or are flattened + EXPECT_EQ(llvm::to_string(*C.intersect( + L1.iterator(), C.intersect(L1.iterator(), L1.iterator()))), + "(& [1] [1] [1])"); + EXPECT_EQ(llvm::to_string(*C.unionOf( + L1.iterator(), C.unionOf(L2.iterator(), L3.iterator()))), + "(| [1] [2] [3])"); + + // optimizations combine over multiple levels + EXPECT_EQ(llvm::to_string(*C.intersect( + C.intersect(L1.iterator(), C.intersect()), C.unionOf(C.all()))), + "[1]"); +} + +//===----------------------------------------------------------------------===// +// Search token tests. +//===----------------------------------------------------------------------===// + +::testing::Matcher<std::vector<Token>> +tokensAre(std::initializer_list<std::string> Strings, Token::Kind Kind) { + std::vector<Token> Tokens; + for (const auto &TokenData : Strings) { + Tokens.push_back(Token(Kind, TokenData)); + } + return ::testing::UnorderedElementsAreArray(Tokens); +} + +::testing::Matcher<std::vector<Token>> +trigramsAre(std::initializer_list<std::string> Trigrams) { + return tokensAre(Trigrams, Token::Kind::Trigram); +} + +TEST(DexTrigrams, IdentifierTrigrams) { + EXPECT_THAT(generateIdentifierTrigrams("X86"), + trigramsAre({"x86", "x", "x8"})); + + EXPECT_THAT(generateIdentifierTrigrams("nl"), trigramsAre({"nl", "n"})); + + EXPECT_THAT(generateIdentifierTrigrams("n"), trigramsAre({"n"})); + + EXPECT_THAT(generateIdentifierTrigrams("clangd"), + trigramsAre({"c", "cl", "cla", "lan", "ang", "ngd"})); + + EXPECT_THAT(generateIdentifierTrigrams("abc_def"), + trigramsAre({"a", "ab", "ad", "abc", "abd", "ade", "bcd", "bde", + "cde", "def"})); + + EXPECT_THAT(generateIdentifierTrigrams("a_b_c_d_e_"), + trigramsAre({"a", "a_", "ab", "abc", "bcd", "cde"})); + + EXPECT_THAT(generateIdentifierTrigrams("unique_ptr"), + trigramsAre({"u", "un", "up", "uni", "unp", "upt", "niq", "nip", + "npt", "iqu", "iqp", "ipt", "que", "qup", "qpt", + "uep", "ept", "ptr"})); + + EXPECT_THAT( + generateIdentifierTrigrams("TUDecl"), + trigramsAre({"t", "tu", "td", "tud", "tde", "ude", "dec", "ecl"})); + + EXPECT_THAT(generateIdentifierTrigrams("IsOK"), + trigramsAre({"i", "is", "io", "iso", "iok", "sok"})); + + EXPECT_THAT( + generateIdentifierTrigrams("abc_defGhij__klm"), + trigramsAre({"a", "ab", "ad", "abc", "abd", "ade", "adg", "bcd", + "bde", "bdg", "cde", "cdg", "def", "deg", "dgh", "dgk", + "efg", "egh", "egk", "fgh", "fgk", "ghi", "ghk", "gkl", + "hij", "hik", "hkl", "ijk", "ikl", "jkl", "klm"})); +} + +TEST(DexTrigrams, QueryTrigrams) { + EXPECT_THAT(generateQueryTrigrams("c"), trigramsAre({"c"})); + EXPECT_THAT(generateQueryTrigrams("cl"), trigramsAre({"cl"})); + EXPECT_THAT(generateQueryTrigrams("cla"), trigramsAre({"cla"})); + + EXPECT_THAT(generateQueryTrigrams(""), trigramsAre({})); + EXPECT_THAT(generateQueryTrigrams("_"), trigramsAre({"_"})); + EXPECT_THAT(generateQueryTrigrams("__"), trigramsAre({"__"})); + EXPECT_THAT(generateQueryTrigrams("___"), trigramsAre({})); + + EXPECT_THAT(generateQueryTrigrams("X86"), trigramsAre({"x86"})); + + EXPECT_THAT(generateQueryTrigrams("clangd"), + trigramsAre({"cla", "lan", "ang", "ngd"})); + + EXPECT_THAT(generateQueryTrigrams("abc_def"), + trigramsAre({"abc", "bcd", "cde", "def"})); + + EXPECT_THAT(generateQueryTrigrams("a_b_c_d_e_"), + trigramsAre({"abc", "bcd", "cde"})); + + EXPECT_THAT(generateQueryTrigrams("unique_ptr"), + trigramsAre({"uni", "niq", "iqu", "que", "uep", "ept", "ptr"})); + + EXPECT_THAT(generateQueryTrigrams("TUDecl"), + trigramsAre({"tud", "ude", "dec", "ecl"})); + + EXPECT_THAT(generateQueryTrigrams("IsOK"), trigramsAre({"iso", "sok"})); + + EXPECT_THAT(generateQueryTrigrams("abc_defGhij__klm"), + trigramsAre({"abc", "bcd", "cde", "def", "efg", "fgh", "ghi", + "hij", "ijk", "jkl", "klm"})); +} + +TEST(DexSearchTokens, SymbolPath) { + EXPECT_THAT(generateProximityURIs( + "unittest:///clang-tools-extra/clangd/index/Token.h"), + ElementsAre("unittest:///clang-tools-extra/clangd/index/Token.h", + "unittest:///clang-tools-extra/clangd/index", + "unittest:///clang-tools-extra/clangd", + "unittest:///clang-tools-extra", "unittest:///")); + + EXPECT_THAT(generateProximityURIs("unittest:///a/b/c.h"), + ElementsAre("unittest:///a/b/c.h", "unittest:///a/b", + "unittest:///a", "unittest:///")); +} + +//===----------------------------------------------------------------------===// +// Index tests. +//===----------------------------------------------------------------------===// + +TEST(Dex, Lookup) { + auto I = Dex::build(generateSymbols({"ns::abc", "ns::xyz"}), RefSlab()); + EXPECT_THAT(lookup(*I, SymbolID("ns::abc")), UnorderedElementsAre("ns::abc")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::abc"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::abc", "ns::xyz")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::nonono"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::xyz")); + EXPECT_THAT(lookup(*I, SymbolID("ns::nonono")), UnorderedElementsAre()); +} + +TEST(Dex, FuzzyFind) { + auto Index = + Dex::build(generateSymbols({"ns::ABC", "ns::BCD", "::ABC", + "ns::nested::ABC", "other::ABC", "other::A"}), + RefSlab()); + FuzzyFindRequest Req; + Req.Query = "ABC"; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(*Index, Req), UnorderedElementsAre("ns::ABC")); + Req.Scopes = {"ns::", "ns::nested::"}; + EXPECT_THAT(match(*Index, Req), + UnorderedElementsAre("ns::ABC", "ns::nested::ABC")); + Req.Query = "A"; + Req.Scopes = {"other::"}; + EXPECT_THAT(match(*Index, Req), + UnorderedElementsAre("other::A", "other::ABC")); + Req.Query = ""; + Req.Scopes = {}; + Req.AnyScope = true; + EXPECT_THAT(match(*Index, Req), + UnorderedElementsAre("ns::ABC", "ns::BCD", "::ABC", + "ns::nested::ABC", "other::ABC", + "other::A")); +} + +TEST(DexTest, DexLimitedNumMatches) { + auto I = Dex::build(generateNumSymbols(0, 100), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "5"; + Req.AnyScope = true; + Req.Limit = 3; + bool Incomplete; + auto Matches = match(*I, Req, &Incomplete); + EXPECT_TRUE(Req.Limit); + EXPECT_EQ(Matches.size(), *Req.Limit); + EXPECT_TRUE(Incomplete); +} + +TEST(DexTest, FuzzyMatch) { + auto I = Dex::build( + generateSymbols({"LaughingOutLoud", "LionPopulation", "LittleOldLady"}), + RefSlab()); + FuzzyFindRequest Req; + Req.Query = "lol"; + Req.AnyScope = true; + Req.Limit = 2; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("LaughingOutLoud", "LittleOldLady")); +} + +TEST(DexTest, ShortQuery) { + auto I = Dex::build(generateSymbols({"OneTwoThreeFour"}), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + bool Incomplete; + + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre("OneTwoThreeFour")); + EXPECT_FALSE(Incomplete) << "Empty string is not a short query"; + + Req.Query = "t"; + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre()); + EXPECT_TRUE(Incomplete) << "Short queries have different semantics"; + + Req.Query = "tt"; + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre()); + EXPECT_TRUE(Incomplete) << "Short queries have different semantics"; + + Req.Query = "ttf"; + EXPECT_THAT(match(*I, Req, &Incomplete), ElementsAre("OneTwoThreeFour")); + EXPECT_FALSE(Incomplete) << "3-char string is not a short query"; +} + +TEST(DexTest, MatchQualifiedNamesWithoutSpecificScope) { + auto I = Dex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "y"; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "b::y2", "y3")); +} + +TEST(DexTest, MatchQualifiedNamesWithGlobalScope) { + auto I = Dex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {""}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("y3")); +} + +TEST(DexTest, MatchQualifiedNamesWithOneScope) { + auto I = Dex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2")); +} + +TEST(DexTest, MatchQualifiedNamesWithMultipleScopes) { + auto I = Dex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y3", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::", "b::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2", "b::y3")); +} + +TEST(DexTest, NoMatchNestedScopes) { + auto I = Dex::build(generateSymbols({"a::y1", "a::b::y2"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1")); +} + +TEST(DexTest, WildcardScope) { + auto I = + Dex::build(generateSymbols({"a::y1", "a::b::y2", "c::y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("a::y1", "a::b::y2", "c::y3")); +} + +TEST(DexTest, IgnoreCases) { + auto I = Dex::build(generateSymbols({"ns::ABC", "ns::abc"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "AB"; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("ns::ABC", "ns::abc")); +} + +TEST(DexTest, UnknownPostingList) { + // Regression test: we used to ignore unknown scopes and accept any symbol. + auto I = Dex::build(generateSymbols({"ns::ABC", "ns::abc"}), RefSlab()); + FuzzyFindRequest Req; + Req.Scopes = {"ns2::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre()); +} + +TEST(DexTest, Lookup) { + auto I = Dex::build(generateSymbols({"ns::abc", "ns::xyz"}), RefSlab()); + EXPECT_THAT(lookup(*I, SymbolID("ns::abc")), UnorderedElementsAre("ns::abc")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::abc"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::abc", "ns::xyz")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::nonono"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::xyz")); + EXPECT_THAT(lookup(*I, SymbolID("ns::nonono")), UnorderedElementsAre()); +} + +TEST(DexTest, SymbolIndexOptionsFilter) { + auto CodeCompletionSymbol = symbol("Completion"); + auto NonCodeCompletionSymbol = symbol("NoCompletion"); + CodeCompletionSymbol.Flags = Symbol::SymbolFlag::IndexedForCodeCompletion; + NonCodeCompletionSymbol.Flags = Symbol::SymbolFlag::None; + std::vector<Symbol> Symbols{CodeCompletionSymbol, NonCodeCompletionSymbol}; + Dex I(Symbols, RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.RestrictForCodeCompletion = false; + EXPECT_THAT(match(I, Req), ElementsAre("Completion", "NoCompletion")); + Req.RestrictForCodeCompletion = true; + EXPECT_THAT(match(I, Req), ElementsAre("Completion")); +} + +TEST(DexTest, ProximityPathsBoosting) { + auto RootSymbol = symbol("root::abc"); + RootSymbol.CanonicalDeclaration.FileURI = "unittest:///file.h"; + auto CloseSymbol = symbol("close::abc"); + CloseSymbol.CanonicalDeclaration.FileURI = "unittest:///a/b/c/d/e/f/file.h"; + + std::vector<Symbol> Symbols{CloseSymbol, RootSymbol}; + Dex I(Symbols, RefSlab()); + + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "abc"; + // The best candidate can change depending on the proximity paths. + Req.Limit = 1; + + // FuzzyFind request comes from the file which is far from the root: expect + // CloseSymbol to come out. + Req.ProximityPaths = {testPath("a/b/c/d/e/f/file.h")}; + EXPECT_THAT(match(I, Req), ElementsAre("close::abc")); + + // FuzzyFind request comes from the file which is close to the root: expect + // RootSymbol to come out. + Req.ProximityPaths = {testPath("file.h")}; + EXPECT_THAT(match(I, Req), ElementsAre("root::abc")); +} + +TEST(DexTests, Refs) { + llvm::DenseMap<SymbolID, std::vector<Ref>> Refs; + auto AddRef = [&](const Symbol &Sym, const char *Filename, RefKind Kind) { + auto &SymbolRefs = Refs[Sym.ID]; + SymbolRefs.emplace_back(); + SymbolRefs.back().Kind = Kind; + SymbolRefs.back().Location.FileURI = Filename; + }; + auto Foo = symbol("foo"); + auto Bar = symbol("bar"); + AddRef(Foo, "foo.h", RefKind::Declaration); + AddRef(Foo, "foo.cc", RefKind::Definition); + AddRef(Foo, "reffoo.h", RefKind::Reference); + AddRef(Bar, "bar.h", RefKind::Declaration); + + RefsRequest Req; + Req.IDs.insert(Foo.ID); + Req.Filter = RefKind::Declaration | RefKind::Definition; + + std::vector<std::string> Files; + Dex(std::vector<Symbol>{Foo, Bar}, Refs).refs(Req, [&](const Ref &R) { + Files.push_back(R.Location.FileURI); + }); + EXPECT_THAT(Files, UnorderedElementsAre("foo.h", "foo.cc")); + + Req.Limit = 1; + Files.clear(); + Dex(std::vector<Symbol>{Foo, Bar}, Refs).refs(Req, [&](const Ref &R) { + Files.push_back(R.Location.FileURI); + }); + EXPECT_THAT(Files, ElementsAre(AnyOf("foo.h", "foo.cc"))); +} + +TEST(DexTest, PreferredTypesBoosting) { + auto Sym1 = symbol("t1"); + Sym1.Type = "T1"; + auto Sym2 = symbol("t2"); + Sym2.Type = "T2"; + + std::vector<Symbol> Symbols{Sym1, Sym2}; + Dex I(Symbols, RefSlab()); + + FuzzyFindRequest Req; + Req.AnyScope = true; + Req.Query = "t"; + // The best candidate can change depending on the preferred type. + Req.Limit = 1; + + Req.PreferredTypes = {Sym1.Type}; + EXPECT_THAT(match(I, Req), ElementsAre("t1")); + + Req.PreferredTypes = {Sym2.Type}; + EXPECT_THAT(match(I, Req), ElementsAre("t2")); +} + +TEST(DexTest, TemplateSpecialization) { + SymbolSlab::Builder B; + + Symbol S = symbol("TempSpec"); + S.ID = SymbolID("0"); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("1"); + S.TemplateSpecializationArgs = "<int, bool>"; + S.SymInfo.Properties = static_cast<index::SymbolPropertySet>( + index::SymbolProperty::TemplateSpecialization); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("2"); + S.TemplateSpecializationArgs = "<int, U>"; + S.SymInfo.Properties = static_cast<index::SymbolPropertySet>( + index::SymbolProperty::TemplatePartialSpecialization); + B.insert(S); + + auto I = dex::Dex::build(std::move(B).build(), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + + Req.Query = "TempSpec"; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("TempSpec", "TempSpec<int, bool>", + "TempSpec<int, U>")); + + // FIXME: Add filtering for template argument list. + Req.Query = "TempSpec<int"; + EXPECT_THAT(match(*I, Req), IsEmpty()); +} + +} // namespace +} // namespace dex +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/DiagnosticsTests.cpp b/clangd/unittests/DiagnosticsTests.cpp new file mode 100644 index 00000000..826428ca --- /dev/null +++ b/clangd/unittests/DiagnosticsTests.cpp @@ -0,0 +1,773 @@ +//===--- DiagnosticsTests.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 "ClangdUnit.h" +#include "Diagnostics.h" +#include "Path.h" +#include "Protocol.h" +#include "SourceCode.h" +#include "TestFS.h" +#include "TestIndex.h" +#include "TestTU.h" +#include "index/MemIndex.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/DiagnosticSema.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <algorithm> + +namespace clang { +namespace clangd { +namespace { + +using ::testing::_; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +::testing::Matcher<const Diag &> WithFix(::testing::Matcher<Fix> FixMatcher) { + return Field(&Diag::Fixes, ElementsAre(FixMatcher)); +} + +::testing::Matcher<const Diag &> WithFix(::testing::Matcher<Fix> FixMatcher1, + ::testing::Matcher<Fix> FixMatcher2) { + return Field(&Diag::Fixes, UnorderedElementsAre(FixMatcher1, FixMatcher2)); +} + +::testing::Matcher<const Diag &> +WithNote(::testing::Matcher<Note> NoteMatcher) { + return Field(&Diag::Notes, ElementsAre(NoteMatcher)); +} + +MATCHER_P2(Diag, Range, Message, + "Diag at " + llvm::to_string(Range) + " = [" + Message + "]") { + return arg.Range == Range && arg.Message == Message; +} + +MATCHER_P3(Fix, Range, Replacement, Message, + "Fix " + llvm::to_string(Range) + " => " + + ::testing::PrintToString(Replacement) + " = [" + Message + "]") { + return arg.Message == Message && arg.Edits.size() == 1 && + arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement; +} + +MATCHER_P(EqualToLSPDiag, LSPDiag, + "LSP diagnostic " + llvm::to_string(LSPDiag)) { + if (toJSON(arg) != toJSON(LSPDiag)) { + *result_listener << llvm::formatv("expected:\n{0:2}\ngot\n{1:2}", + toJSON(LSPDiag), toJSON(arg)) + .str(); + return false; + } + return true; +} + +MATCHER_P(DiagSource, S, "") { return arg.Source == S; } +MATCHER_P(DiagName, N, "") { return arg.Name == N; } + +MATCHER_P(EqualToFix, Fix, "LSP fix " + llvm::to_string(Fix)) { + if (arg.Message != Fix.Message) + return false; + if (arg.Edits.size() != Fix.Edits.size()) + return false; + for (std::size_t I = 0; I < arg.Edits.size(); ++I) { + if (arg.Edits[I].range != Fix.Edits[I].range || + arg.Edits[I].newText != Fix.Edits[I].newText) + return false; + } + return true; +} + +// Helper function to make tests shorter. +Position pos(int line, int character) { + Position Res; + Res.line = line; + Res.character = character; + return Res; +} + +TEST(DiagnosticsTest, DiagnosticRanges) { + // Check we report correct ranges, including various edge-cases. + Annotations Test(R"cpp( + namespace test{}; + void $decl[[foo]](); + int main() { + $typo[[go\ +o]](); + foo()$semicolon[[]]//with comments + $unk[[unknown]](); + double $type[[bar]] = "foo"; + struct Foo { int x; }; Foo a; + a.$nomember[[y]]; + test::$nomembernamespace[[test]]; + } + )cpp"); + EXPECT_THAT( + TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre( + // This range spans lines. + AllOf(Diag(Test.range("typo"), + "use of undeclared identifier 'goo'; did you mean 'foo'?"), + DiagSource(Diag::Clang), DiagName("undeclared_var_use_suggest"), + WithFix( + Fix(Test.range("typo"), "foo", "change 'go\\ o' to 'foo'")), + // This is a pretty normal range. + WithNote(Diag(Test.range("decl"), "'foo' declared here"))), + // This range is zero-width and insertion. Therefore make sure we are + // not expanding it into other tokens. Since we are not going to + // replace those. + AllOf(Diag(Test.range("semicolon"), "expected ';' after expression"), + WithFix(Fix(Test.range("semicolon"), ";", "insert ';'"))), + // This range isn't provided by clang, we expand to the token. + Diag(Test.range("unk"), "use of undeclared identifier 'unknown'"), + Diag(Test.range("type"), + "cannot initialize a variable of type 'double' with an lvalue " + "of type 'const char [4]'"), + Diag(Test.range("nomember"), "no member named 'y' in 'Foo'"), + Diag(Test.range("nomembernamespace"), + "no member named 'test' in namespace 'test'"))); +} + +TEST(DiagnosticsTest, FlagsMatter) { + Annotations Test("[[void]] main() {}"); + auto TU = TestTU::withCode(Test.code()); + EXPECT_THAT(TU.build().getDiagnostics(), + ElementsAre(AllOf(Diag(Test.range(), "'main' must return 'int'"), + WithFix(Fix(Test.range(), "int", + "change 'void' to 'int'"))))); + // Same code built as C gets different diagnostics. + TU.Filename = "Plain.c"; + EXPECT_THAT( + TU.build().getDiagnostics(), + ElementsAre(AllOf( + Diag(Test.range(), "return type of 'main' is not 'int'"), + WithFix(Fix(Test.range(), "int", "change return type to 'int'"))))); +} + +TEST(DiagnosticsTest, DiagnosticPreamble) { + Annotations Test(R"cpp( + #include $[["not-found.h"]] + )cpp"); + + auto TU = TestTU::withCode(Test.code()); + EXPECT_THAT(TU.build().getDiagnostics(), + ElementsAre(::testing::AllOf( + Diag(Test.range(), "'not-found.h' file not found"), + DiagSource(Diag::Clang), DiagName("pp_file_not_found")))); +} + +TEST(DiagnosticsTest, ClangTidy) { + Annotations Test(R"cpp( + #include $deprecated[["assert.h"]] + + #define $macrodef[[SQUARE]](X) (X)*(X) + int main() { + return $doubled[[sizeof]](sizeof(int)); + } + int square() { + int y = 4; + return SQUARE($macroarg[[++]]y); + } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + TU.HeaderFilename = "assert.h"; // Suppress "not found" error. + TU.ClangTidyChecks = + "-*, bugprone-sizeof-expression, bugprone-macro-repeated-side-effects, " + "modernize-deprecated-headers"; + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("deprecated"), + "inclusion of deprecated C++ header 'assert.h'; consider " + "using 'cassert' instead"), + DiagSource(Diag::ClangTidy), + DiagName("modernize-deprecated-headers"), + WithFix(Fix(Test.range("deprecated"), "<cassert>", + "change '\"assert.h\"' to '<cassert>'"))), + Diag(Test.range("doubled"), + "suspicious usage of 'sizeof(sizeof(...))'"), + AllOf( + Diag(Test.range("macroarg"), + "side effects in the 1st macro argument 'X' are repeated in " + "macro expansion"), + DiagSource(Diag::ClangTidy), + DiagName("bugprone-macro-repeated-side-effects"), + WithNote( + Diag(Test.range("macrodef"), "macro 'SQUARE' defined here"))), + Diag(Test.range("macroarg"), + "multiple unsequenced modifications to 'y'"))); +} + +TEST(DiagnosticsTest, Preprocessor) { + // This looks like a preamble, but there's an #else in the middle! + // Check that: + // - the #else doesn't generate diagnostics (we had this bug) + // - we get diagnostics from the taken branch + // - we get no diagnostics from the not taken branch + Annotations Test(R"cpp( + #ifndef FOO + #define FOO + int a = [[b]]; + #else + int x = y; + #endif + )cpp"); + EXPECT_THAT( + TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre(Diag(Test.range(), "use of undeclared identifier 'b'"))); +} + +TEST(DiagnosticsTest, InsideMacros) { + Annotations Test(R"cpp( + #define TEN 10 + #define RET(x) return x + 10 + + int* foo() { + RET($foo[[0]]); + } + int* bar() { + return $bar[[TEN]]; + } + )cpp"); + EXPECT_THAT(TestTU::withCode(Test.code()).build().getDiagnostics(), + ElementsAre(Diag(Test.range("foo"), + "cannot initialize return object of type " + "'int *' with an rvalue of type 'int'"), + Diag(Test.range("bar"), + "cannot initialize return object of type " + "'int *' with an rvalue of type 'int'"))); +} + +TEST(DiagnosticsTest, NoFixItInMacro) { + Annotations Test(R"cpp( + #define Define(name) void name() {} + + [[Define]](main) + )cpp"); + auto TU = TestTU::withCode(Test.code()); + EXPECT_THAT(TU.build().getDiagnostics(), + ElementsAre(AllOf(Diag(Test.range(), "'main' must return 'int'"), + Not(WithFix(_))))); +} + +TEST(DiagnosticsTest, ToLSP) { + URIForFile MainFile = + URIForFile::canonicalize(testPath("foo/bar/main.cpp"), ""); + URIForFile HeaderFile = + URIForFile::canonicalize(testPath("foo/bar/header.h"), ""); + + clangd::Diag D; + D.ID = clang::diag::err_enum_class_reference; + D.Name = "enum_class_reference"; + D.Source = clangd::Diag::Clang; + D.Message = "something terrible happened"; + D.Range = {pos(1, 2), pos(3, 4)}; + D.InsideMainFile = true; + D.Severity = DiagnosticsEngine::Error; + D.File = "foo/bar/main.cpp"; + D.AbsFile = MainFile.file(); + + clangd::Note NoteInMain; + NoteInMain.Message = "declared somewhere in the main file"; + NoteInMain.Range = {pos(5, 6), pos(7, 8)}; + NoteInMain.Severity = DiagnosticsEngine::Remark; + NoteInMain.File = "../foo/bar/main.cpp"; + NoteInMain.InsideMainFile = true; + NoteInMain.AbsFile = MainFile.file(); + + D.Notes.push_back(NoteInMain); + + clangd::Note NoteInHeader; + NoteInHeader.Message = "declared somewhere in the header file"; + NoteInHeader.Range = {pos(9, 10), pos(11, 12)}; + NoteInHeader.Severity = DiagnosticsEngine::Note; + NoteInHeader.File = "../foo/baz/header.h"; + NoteInHeader.InsideMainFile = false; + NoteInHeader.AbsFile = HeaderFile.file(); + D.Notes.push_back(NoteInHeader); + + clangd::Fix F; + F.Message = "do something"; + D.Fixes.push_back(F); + + // Diagnostics should turn into these: + clangd::Diagnostic MainLSP; + MainLSP.range = D.Range; + MainLSP.severity = getSeverity(DiagnosticsEngine::Error); + MainLSP.code = "enum_class_reference"; + MainLSP.source = "clang"; + MainLSP.message = + R"(Something terrible happened (fix available) + +main.cpp:6:7: remark: declared somewhere in the main file + +../foo/baz/header.h:10:11: +note: declared somewhere in the header file)"; + + clangd::Diagnostic NoteInMainLSP; + NoteInMainLSP.range = NoteInMain.Range; + NoteInMainLSP.severity = getSeverity(DiagnosticsEngine::Remark); + NoteInMainLSP.message = R"(Declared somewhere in the main file + +main.cpp:2:3: error: something terrible happened)"; + + ClangdDiagnosticOptions Opts; + // Transform diagnostics and check the results. + std::vector<std::pair<clangd::Diagnostic, std::vector<clangd::Fix>>> LSPDiags; + toLSPDiags(D, MainFile, Opts, + [&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix> Fixes) { + LSPDiags.push_back( + {std::move(LSPDiag), + std::vector<clangd::Fix>(Fixes.begin(), Fixes.end())}); + }); + + EXPECT_THAT( + LSPDiags, + ElementsAre(Pair(EqualToLSPDiag(MainLSP), ElementsAre(EqualToFix(F))), + Pair(EqualToLSPDiag(NoteInMainLSP), IsEmpty()))); + EXPECT_EQ(LSPDiags[0].first.code, "enum_class_reference"); + EXPECT_EQ(LSPDiags[0].first.source, "clang"); + EXPECT_EQ(LSPDiags[1].first.code, ""); + EXPECT_EQ(LSPDiags[1].first.source, ""); + + // Same thing, but don't flatten notes into the main list. + LSPDiags.clear(); + Opts.EmitRelatedLocations = true; + toLSPDiags(D, MainFile, Opts, + [&](clangd::Diagnostic LSPDiag, ArrayRef<clangd::Fix> Fixes) { + LSPDiags.push_back( + {std::move(LSPDiag), + std::vector<clangd::Fix>(Fixes.begin(), Fixes.end())}); + }); + MainLSP.message = "Something terrible happened (fix available)"; + DiagnosticRelatedInformation NoteInMainDRI; + NoteInMainDRI.message = "Declared somewhere in the main file"; + NoteInMainDRI.location.range = NoteInMain.Range; + NoteInMainDRI.location.uri = MainFile; + MainLSP.relatedInformation = {NoteInMainDRI}; + DiagnosticRelatedInformation NoteInHeaderDRI; + NoteInHeaderDRI.message = "Declared somewhere in the header file"; + NoteInHeaderDRI.location.range = NoteInHeader.Range; + NoteInHeaderDRI.location.uri = HeaderFile; + MainLSP.relatedInformation = {NoteInMainDRI, NoteInHeaderDRI}; + EXPECT_THAT(LSPDiags, ElementsAre(Pair(EqualToLSPDiag(MainLSP), + ElementsAre(EqualToFix(F))))); +} + +struct SymbolWithHeader { + std::string QName; + std::string DeclaringFile; + std::string IncludeHeader; +}; + +std::unique_ptr<SymbolIndex> +buildIndexWithSymbol(llvm::ArrayRef<SymbolWithHeader> Syms) { + SymbolSlab::Builder Slab; + for (const auto &S : Syms) { + Symbol Sym = cls(S.QName); + Sym.Flags |= Symbol::IndexedForCodeCompletion; + Sym.CanonicalDeclaration.FileURI = S.DeclaringFile.c_str(); + Sym.Definition.FileURI = S.DeclaringFile.c_str(); + Sym.IncludeHeaders.emplace_back(S.IncludeHeader, 1); + Slab.insert(Sym); + } + return MemIndex::build(std::move(Slab).build(), RefSlab()); +} + +TEST(IncludeFixerTest, IncompleteType) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { + class X; + $nested[[X::]]Nested n; +} +class Y : $base[[public ns::X]] {}; +int main() { + ns::X *x; + x$access[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("nested"), + "incomplete type 'ns::X' named in nested name specifier"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("base"), "base class has incomplete type"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("access"), + "member access into incomplete type 'ns::X'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))))); +} + +TEST(IncludeFixerTest, NoSuggestIncludeWhenNoDefinitionInHeader) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { + class X; +} +class Y : $base[[public ns::X]] {}; +int main() { + ns::X *x; + x$access[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + Symbol Sym = cls("ns::X"); + Sym.Flags |= Symbol::IndexedForCodeCompletion; + Sym.CanonicalDeclaration.FileURI = "unittest:///x.h"; + Sym.Definition.FileURI = "unittest:///x.cc"; + Sym.IncludeHeaders.emplace_back("\"x.h\"", 1); + + SymbolSlab::Builder Slab; + Slab.insert(Sym); + auto Index = MemIndex::build(std::move(Slab).build(), RefSlab()); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre( + Diag(Test.range("base"), "base class has incomplete type"), + Diag(Test.range("access"), + "member access into incomplete type 'ns::X'"))); +} + +TEST(IncludeFixerTest, Typo) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { +void foo() { + $unqualified1[[X]] x; + // No fix if the unresolved type is used as specifier. (ns::)X::Nested will be + // considered the unresolved type. + $unqualified2[[X]]::Nested n; +} +} +void bar() { + ns::$qualified1[[X]] x; // ns:: is valid. + ns::$qualified2[[X]](); // Error: no member in namespace + + ::$global[[Global]] glob; +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"ns::X", "unittest:///x.h", "\"x.h\""}, + SymbolWithHeader{"Global", "unittest:///global.h", "\"global.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf(Diag(Test.range("unqualified1"), "unknown type name 'X'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + Diag(Test.range("unqualified2"), "use of undeclared identifier 'X'"), + AllOf(Diag(Test.range("qualified1"), + "no type named 'X' in namespace 'ns'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("qualified2"), + "no member named 'X' in namespace 'ns'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::X"))), + AllOf(Diag(Test.range("global"), + "no type named 'Global' in the global namespace"), + WithFix(Fix(Test.range("insert"), "#include \"global.h\"\n", + "Add include \"global.h\" for symbol Global"))))); +} + +TEST(IncludeFixerTest, MultipleMatchedSymbols) { + Annotations Test(R"cpp( +$insert[[]]namespace na { +namespace nb { +void foo() { + $unqualified[[X]] x; +} +} +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"na::X", "unittest:///a.h", "\"a.h\""}, + SymbolWithHeader{"na::nb::X", "unittest:///b.h", "\"b.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range("unqualified"), "unknown type name 'X'"), + WithFix(Fix(Test.range("insert"), "#include \"a.h\"\n", + "Add include \"a.h\" for symbol na::X"), + Fix(Test.range("insert"), "#include \"b.h\"\n", + "Add include \"b.h\" for symbol na::nb::X"))))); +} + +TEST(IncludeFixerTest, NoCrashMemebrAccess) { + Annotations Test(R"cpp( + struct X { int xyz; }; + void g() { X x; x.$[[xy]] } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + SymbolWithHeader{"na::X", "unittest:///a.h", "\"a.h\""}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre(Diag(Test.range(), "no member named 'xy' in 'X'"))); +} + +TEST(IncludeFixerTest, UseCachedIndexResults) { + // As index results for the identical request are cached, more than 5 fixes + // are generated. + Annotations Test(R"cpp( +$insert[[]]void foo() { + $x1[[X]] x; + $x2[[X]] x; + $x3[[X]] x; + $x4[[X]] x; + $x5[[X]] x; + $x6[[X]] x; + $x7[[X]] x; +} + +class X; +void bar(X *x) { + x$a1[[->]]f(); + x$a2[[->]]f(); + x$a3[[->]]f(); + x$a4[[->]]f(); + x$a5[[->]]f(); + x$a6[[->]]f(); + x$a7[[->]]f(); +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = + buildIndexWithSymbol(SymbolWithHeader{"X", "unittest:///a.h", "\"a.h\""}); + TU.ExternalIndex = Index.get(); + + auto Parsed = TU.build(); + for (const auto &D : Parsed.getDiagnostics()) { + EXPECT_EQ(D.Fixes.size(), 1u); + EXPECT_EQ(D.Fixes[0].Message, + std::string("Add include \"a.h\" for symbol X")); + } +} + +TEST(IncludeFixerTest, UnresolvedNameAsSpecifier) { + Annotations Test(R"cpp( +$insert[[]]namespace ns { +} +void g() { ns::$[[scope]]::X_Y(); } + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + SymbolWithHeader{"ns::scope::X_Y", "unittest:///x.h", "\"x.h\""}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range(), "no member named 'scope' in namespace 'ns'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol ns::scope::X_Y"))))); +} + +TEST(IncludeFixerTest, UnresolvedSpecifierWithSemaCorrection) { + Annotations Test(R"cpp( +$insert[[]]namespace clang { +void f() { + // "clangd::" will be corrected to "clang::" by Sema. + $q1[[clangd]]::$x[[X]] x; + $q2[[clangd]]::$ns[[ns]]::Y y; +} +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"clang::clangd::X", "unittest:///x.h", "\"x.h\""}, + SymbolWithHeader{"clang::clangd::ns::Y", "unittest:///y.h", "\"y.h\""}}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT( + TU.build().getDiagnostics(), + UnorderedElementsAre( + AllOf( + Diag(Test.range("q1"), "use of undeclared identifier 'clangd'; " + "did you mean 'clang'?"), + WithFix(_, // change clangd to clang + Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol clang::clangd::X"))), + AllOf( + Diag(Test.range("x"), "no type named 'X' in namespace 'clang'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol clang::clangd::X"))), + AllOf( + Diag(Test.range("q2"), "use of undeclared identifier 'clangd'; " + "did you mean 'clang'?"), + WithFix( + _, // change clangd to clangd + Fix(Test.range("insert"), "#include \"y.h\"\n", + "Add include \"y.h\" for symbol clang::clangd::ns::Y"))), + AllOf(Diag(Test.range("ns"), + "no member named 'ns' in namespace 'clang'"), + WithFix(Fix( + Test.range("insert"), "#include \"y.h\"\n", + "Add include \"y.h\" for symbol clang::clangd::ns::Y"))))); +} + +TEST(IncludeFixerTest, SpecifiedScopeIsNamespaceAlias) { + Annotations Test(R"cpp( +$insert[[]]namespace a {} +namespace b = a; +namespace c { + b::$[[X]] x; +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + SymbolWithHeader{"a::X", "unittest:///x.h", "\"x.h\""}); + TU.ExternalIndex = Index.get(); + + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range(), "no type named 'X' in namespace 'a'"), + WithFix(Fix(Test.range("insert"), "#include \"x.h\"\n", + "Add include \"x.h\" for symbol a::X"))))); +} + +TEST(DiagsInHeaders, DiagInsideHeader) { + Annotations Main(R"cpp( + #include [["a.h"]] + void foo() {})cpp"); + Annotations Header("[[no_type_spec]];"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", Header.code()}}; + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Main.range(), "in included file: C++ requires a " + "type specifier for all declarations"), + WithNote(Diag(Header.range(), "error occurred here"))))); +} + +TEST(DiagsInHeaders, DiagInTransitiveInclude) { + Annotations Main(R"cpp( + #include [["a.h"]] + void foo() {})cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", "#include \"b.h\""}, {"b.h", "no_type_spec;"}}; + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre( + Diag(Main.range(), "in included file: C++ requires a " + "type specifier for all declarations"))); +} + +TEST(DiagsInHeaders, DiagInMultipleHeaders) { + Annotations Main(R"cpp( + #include $a[["a.h"]] + #include $b[["b.h"]] + void foo() {})cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", "no_type_spec;"}, {"b.h", "no_type_spec;"}}; + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre( + Diag(Main.range("a"), "in included file: C++ requires a type " + "specifier for all declarations"), + Diag(Main.range("b"), "in included file: C++ requires a type " + "specifier for all declarations"))); +} + +TEST(DiagsInHeaders, PreferExpansionLocation) { + Annotations Main(R"cpp( + #include [["a.h"]] + #include "b.h" + void foo() {})cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", "#include \"b.h\"\n"}, + {"b.h", "#ifndef X\n#define X\nno_type_spec;\n#endif"}}; + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(Diag(Main.range(), + "in included file: C++ requires a type " + "specifier for all declarations"))); +} + +TEST(DiagsInHeaders, PreferExpansionLocationMacros) { + Annotations Main(R"cpp( + #define X + #include "a.h" + #undef X + #include [["b.h"]] + void foo() {})cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", "#include \"c.h\"\n"}, + {"b.h", "#include \"c.h\"\n"}, + {"c.h", "#ifndef X\n#define X\nno_type_spec;\n#endif"}}; + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre( + Diag(Main.range(), "in included file: C++ requires a " + "type specifier for all declarations"))); +} + +TEST(DiagsInHeaders, LimitDiagsOutsideMainFile) { + Annotations Main(R"cpp( + #include [["a.h"]] + #include "b.h" + void foo() {})cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", "#include \"c.h\"\n"}, + {"b.h", "#include \"c.h\"\n"}, + {"c.h", R"cpp( + #ifndef X + #define X + no_type_spec_0; + no_type_spec_1; + no_type_spec_2; + no_type_spec_3; + no_type_spec_4; + no_type_spec_5; + no_type_spec_6; + no_type_spec_7; + no_type_spec_8; + no_type_spec_9; + no_type_spec_10; + #endif)cpp"}}; + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre( + Diag(Main.range(), "in included file: C++ requires a " + "type specifier for all declarations"))); +} + +TEST(DiagsInHeaders, OnlyErrorOrFatal) { + Annotations Main(R"cpp( + #include [["a.h"]] + void foo() {})cpp"); + Annotations Header(R"cpp( + [[no_type_spec]]; + int x = 5/0;)cpp"); + TestTU TU = TestTU::withCode(Main.code()); + TU.AdditionalFiles = {{"a.h", Header.code()}}; + auto diags = TU.build().getDiagnostics(); + EXPECT_THAT(TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Main.range(), "in included file: C++ requires " + "a type specifier for all declarations"), + WithNote(Diag(Header.range(), "error occurred here"))))); +} +} // namespace + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/DraftStoreTests.cpp b/clangd/unittests/DraftStoreTests.cpp new file mode 100644 index 00000000..1840892c --- /dev/null +++ b/clangd/unittests/DraftStoreTests.cpp @@ -0,0 +1,347 @@ +//===-- DraftStoreTests.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 "DraftStore.h" +#include "SourceCode.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +struct IncrementalTestStep { + llvm::StringRef Src; + llvm::StringRef Contents; +}; + +int rangeLength(llvm::StringRef Code, const Range &Rng) { + llvm::Expected<size_t> Start = positionToOffset(Code, Rng.start); + llvm::Expected<size_t> End = positionToOffset(Code, Rng.end); + assert(Start); + assert(End); + return *End - *Start; +} + +/// Send the changes one by one to updateDraft, verify the intermediate results. +void stepByStep(llvm::ArrayRef<IncrementalTestStep> Steps) { + DraftStore DS; + Annotations InitialSrc(Steps.front().Src); + constexpr llvm::StringLiteral Path("/hello.cpp"); + + // Set the initial content. + DS.addDraft(Path, InitialSrc.code()); + + for (size_t i = 1; i < Steps.size(); i++) { + Annotations SrcBefore(Steps[i - 1].Src); + Annotations SrcAfter(Steps[i].Src); + llvm::StringRef Contents = Steps[i - 1].Contents; + TextDocumentContentChangeEvent Event{ + SrcBefore.range(), + rangeLength(SrcBefore.code(), SrcBefore.range()), + Contents.str(), + }; + + llvm::Expected<std::string> Result = DS.updateDraft(Path, {Event}); + ASSERT_TRUE(!!Result); + EXPECT_EQ(*Result, SrcAfter.code()); + EXPECT_EQ(*DS.getDraft(Path), SrcAfter.code()); + } +} + +/// Send all the changes at once to updateDraft, check only the final result. +void allAtOnce(llvm::ArrayRef<IncrementalTestStep> Steps) { + DraftStore DS; + Annotations InitialSrc(Steps.front().Src); + Annotations FinalSrc(Steps.back().Src); + constexpr llvm::StringLiteral Path("/hello.cpp"); + std::vector<TextDocumentContentChangeEvent> Changes; + + for (size_t i = 0; i < Steps.size() - 1; i++) { + Annotations Src(Steps[i].Src); + llvm::StringRef Contents = Steps[i].Contents; + + Changes.push_back({ + Src.range(), + rangeLength(Src.code(), Src.range()), + Contents.str(), + }); + } + + // Set the initial content. + DS.addDraft(Path, InitialSrc.code()); + + llvm::Expected<std::string> Result = DS.updateDraft(Path, Changes); + + ASSERT_TRUE(!!Result) << llvm::toString(Result.takeError()); + EXPECT_EQ(*Result, FinalSrc.code()); + EXPECT_EQ(*DS.getDraft(Path), FinalSrc.code()); +} + +TEST(DraftStoreIncrementalUpdateTest, Simple) { + // clang-format off + IncrementalTestStep Steps[] = + { + // Replace a range + { +R"cpp(static int +hello[[World]]() +{})cpp", + "Universe" + }, + // Delete a range + { +R"cpp(static int +hello[[Universe]]() +{})cpp", + "" + }, + // Add a range + { +R"cpp(static int +hello[[]]() +{})cpp", + "Monde" + }, + { +R"cpp(static int +helloMonde() +{})cpp", + "" + } + }; + // clang-format on + + stepByStep(Steps); + allAtOnce(Steps); +} + +TEST(DraftStoreIncrementalUpdateTest, MultiLine) { + // clang-format off + IncrementalTestStep Steps[] = + { + // Replace a range + { +R"cpp(static [[int +helloWorld]]() +{})cpp", +R"cpp(char +welcome)cpp" + }, + // Delete a range + { +R"cpp(static char[[ +welcome]]() +{})cpp", + "" + }, + // Add a range + { +R"cpp(static char[[]]() +{})cpp", + R"cpp( +cookies)cpp" + }, + // Replace the whole file + { +R"cpp([[static char +cookies() +{}]])cpp", + R"cpp(#include <stdio.h> +)cpp" + }, + // Delete the whole file + { + R"cpp([[#include <stdio.h> +]])cpp", + "", + }, + // Add something to an empty file + { + "[[]]", + R"cpp(int main() { +)cpp", + }, + { + R"cpp(int main() { +)cpp", + "" + } + }; + // clang-format on + + stepByStep(Steps); + allAtOnce(Steps); +} + +TEST(DraftStoreIncrementalUpdateTest, WrongRangeLength) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 0; + Change.range->end.line = 0; + Change.range->end.character = 2; + Change.rangeLength = 10; + + Expected<std::string> Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ( + toString(Result.takeError()), + "Change's rangeLength (10) doesn't match the computed range length (2)."); +} + +TEST(DraftStoreIncrementalUpdateTest, EndBeforeStart) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 5; + Change.range->end.line = 0; + Change.range->end.character = 3; + + Expected<std::string> Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "Range's end position (0:3) is before start position (0:5)"); +} + +TEST(DraftStoreIncrementalUpdateTest, StartCharOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 100; + Change.range->end.line = 0; + Change.range->end.character = 100; + Change.text = "foo"; + + Expected<std::string> Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "utf-16 offset 100 is invalid for line 0"); +} + +TEST(DraftStoreIncrementalUpdateTest, EndCharOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 0; + Change.range->end.line = 0; + Change.range->end.character = 100; + Change.text = "foo"; + + Expected<std::string> Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "utf-16 offset 100 is invalid for line 0"); +} + +TEST(DraftStoreIncrementalUpdateTest, StartLineOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 100; + Change.range->start.character = 0; + Change.range->end.line = 100; + Change.range->end.character = 0; + Change.text = "foo"; + + Expected<std::string> Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)"); +} + +TEST(DraftStoreIncrementalUpdateTest, EndLineOutOfRange) { + DraftStore DS; + Path File = "foo.cpp"; + + DS.addDraft(File, "int main() {}\n"); + + TextDocumentContentChangeEvent Change; + Change.range.emplace(); + Change.range->start.line = 0; + Change.range->start.character = 0; + Change.range->end.line = 100; + Change.range->end.character = 0; + Change.text = "foo"; + + Expected<std::string> Result = DS.updateDraft(File, {Change}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), "Line value is out of range (100)"); +} + +/// Check that if a valid change is followed by an invalid change, the original +/// version of the document (prior to all changes) is kept. +TEST(DraftStoreIncrementalUpdateTest, InvalidRangeInASequence) { + DraftStore DS; + Path File = "foo.cpp"; + + StringRef OriginalContents = "int main() {}\n"; + DS.addDraft(File, OriginalContents); + + // The valid change + TextDocumentContentChangeEvent Change1; + Change1.range.emplace(); + Change1.range->start.line = 0; + Change1.range->start.character = 0; + Change1.range->end.line = 0; + Change1.range->end.character = 0; + Change1.text = "Hello "; + + // The invalid change + TextDocumentContentChangeEvent Change2; + Change2.range.emplace(); + Change2.range->start.line = 0; + Change2.range->start.character = 5; + Change2.range->end.line = 0; + Change2.range->end.character = 100; + Change2.text = "something"; + + Expected<std::string> Result = DS.updateDraft(File, {Change1, Change2}); + + EXPECT_TRUE(!Result); + EXPECT_EQ(toString(Result.takeError()), + "utf-16 offset 100 is invalid for line 0"); + + Optional<std::string> Contents = DS.getDraft(File); + EXPECT_TRUE(Contents); + EXPECT_EQ(*Contents, OriginalContents); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/ExpectedTypeTest.cpp b/clangd/unittests/ExpectedTypeTest.cpp new file mode 100644 index 00000000..8d2d60eb --- /dev/null +++ b/clangd/unittests/ExpectedTypeTest.cpp @@ -0,0 +1,153 @@ +//===-- ExpectedTypeTest.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 "ClangdUnit.h" +#include "ExpectedTypes.h" +#include "TestTU.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "llvm/ADT/StringRef.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::Field; +using ::testing::Matcher; +using ::testing::SizeIs; +using ::testing::UnorderedElementsAreArray; + +class ExpectedTypeConversionTest : public ::testing::Test { +protected: + void build(llvm::StringRef Code) { + assert(!AST && "AST built twice"); + AST = TestTU::withCode(Code).build(); + } + + const ValueDecl *decl(llvm::StringRef Name) { + return &cast<ValueDecl>(findDecl(*AST, Name)); + } + + QualType typeOf(llvm::StringRef Name) { + return decl(Name)->getType().getCanonicalType(); + } + + /// An overload for convenience. + llvm::Optional<OpaqueType> fromCompletionResult(const ValueDecl *D) { + return OpaqueType::fromCompletionResult( + ASTCtx(), CodeCompletionResult(D, CCP_Declaration)); + } + + /// A set of DeclNames whose type match each other computed by + /// OpaqueType::fromCompletionResult. + using EquivClass = std::set<std::string>; + + Matcher<std::map<std::string, EquivClass>> + ClassesAre(llvm::ArrayRef<EquivClass> Classes) { + using MapEntry = std::map<std::string, EquivClass>::value_type; + + std::vector<Matcher<MapEntry>> Elements; + Elements.reserve(Classes.size()); + for (auto &Cls : Classes) + Elements.push_back(Field(&MapEntry::second, Cls)); + return UnorderedElementsAreArray(Elements); + } + + // Groups \p Decls into equivalence classes based on the result of + // 'OpaqueType::fromCompletionResult'. + std::map<std::string, EquivClass> + buildEquivClasses(llvm::ArrayRef<llvm::StringRef> DeclNames) { + std::map<std::string, EquivClass> Classes; + for (llvm::StringRef Name : DeclNames) { + auto Type = OpaqueType::fromType(ASTCtx(), typeOf(Name)); + Classes[Type->raw()].insert(Name); + } + return Classes; + } + + ASTContext &ASTCtx() { return AST->getASTContext(); } + +private: + // Set after calling build(). + llvm::Optional<ParsedAST> AST; +}; + +TEST_F(ExpectedTypeConversionTest, BasicTypes) { + build(R"cpp( + // ints. + bool b; + int i; + unsigned int ui; + long long ll; + + // floats. + float f; + double d; + + // pointers + int* iptr; + bool* bptr; + + // user-defined types. + struct X {}; + X user_type; + )cpp"); + + EXPECT_THAT(buildEquivClasses({"b", "i", "ui", "ll", "f", "d", "iptr", "bptr", + "user_type"}), + ClassesAre({{"b"}, + {"i", "ui", "ll"}, + {"f", "d"}, + {"iptr"}, + {"bptr"}, + {"user_type"}})); +} + +TEST_F(ExpectedTypeConversionTest, ReferencesDontMatter) { + build(R"cpp( + int noref; + int & ref = noref; + const int & const_ref = noref; + int && rv_ref = 10; + )cpp"); + + EXPECT_THAT(buildEquivClasses({"noref", "ref", "const_ref", "rv_ref"}), + SizeIs(1)); +} + +TEST_F(ExpectedTypeConversionTest, ArraysDecay) { + build(R"cpp( + int arr[2]; + int (&arr_ref)[2] = arr; + int *ptr; + )cpp"); + + EXPECT_THAT(buildEquivClasses({"arr", "arr_ref", "ptr"}), SizeIs(1)); +} + +TEST_F(ExpectedTypeConversionTest, FunctionReturns) { + build(R"cpp( + int returns_int(); + int* returns_ptr(); + + int int_; + int* int_ptr; + )cpp"); + + OpaqueType IntTy = *OpaqueType::fromType(ASTCtx(), typeOf("int_")); + EXPECT_EQ(fromCompletionResult(decl("returns_int")), IntTy); + + OpaqueType IntPtrTy = *OpaqueType::fromType(ASTCtx(), typeOf("int_ptr")); + EXPECT_EQ(fromCompletionResult(decl("returns_ptr")), IntPtrTy); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/FSTests.cpp b/clangd/unittests/FSTests.cpp new file mode 100644 index 00000000..044452ca --- /dev/null +++ b/clangd/unittests/FSTests.cpp @@ -0,0 +1,50 @@ +//===-- FSTests.cpp - File system related tests -----------------*- 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 "FS.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(FSTests, PreambleStatusCache) { + llvm::StringMap<std::string> Files; + Files["x"] = ""; + Files["y"] = ""; + Files["main"] = ""; + auto FS = buildTestFS(Files); + FS->setCurrentWorkingDirectory(testRoot()); + + PreambleFileStatusCache StatCache(testPath("main")); + auto ProduceFS = StatCache.getProducingFS(FS); + EXPECT_TRUE(ProduceFS->openFileForRead("x")); + EXPECT_TRUE(ProduceFS->status("y")); + EXPECT_TRUE(ProduceFS->status("main")); + + EXPECT_TRUE(StatCache.lookup(testPath("x")).hasValue()); + EXPECT_TRUE(StatCache.lookup(testPath("y")).hasValue()); + // Main file is not cached. + EXPECT_FALSE(StatCache.lookup(testPath("main")).hasValue()); + + llvm::vfs::Status S("fake", llvm::sys::fs::UniqueID(0, 0), + std::chrono::system_clock::now(), 0, 0, 1024, + llvm::sys::fs::file_type::regular_file, + llvm::sys::fs::all_all); + StatCache.update(*FS, S); + auto ConsumeFS = StatCache.getConsumingFS(FS); + auto Cached = ConsumeFS->status(testPath("fake")); + EXPECT_TRUE(Cached); + EXPECT_EQ(Cached->getName(), S.getName()); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/FileDistanceTests.cpp b/clangd/unittests/FileDistanceTests.cpp new file mode 100644 index 00000000..30035829 --- /dev/null +++ b/clangd/unittests/FileDistanceTests.cpp @@ -0,0 +1,123 @@ +//===-- FileDistanceTests.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 "FileDistance.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(FileDistanceTests, Distance) { + FileDistanceOptions Opts; + Opts.UpCost = 5; + Opts.DownCost = 3; + SourceParams CostTwo; + CostTwo.Cost = 2; + FileDistance D( + {{"tools/clang/lib/Format/FormatToken.cpp", SourceParams()}, + {"tools/clang/include/clang/Format/FormatToken.h", SourceParams()}, + {"include/llvm/ADT/StringRef.h", CostTwo}}, + Opts); + + // Source + EXPECT_EQ(D.distance("tools/clang/lib/Format/FormatToken.cpp"), 0u); + EXPECT_EQ(D.distance("include/llvm/ADT/StringRef.h"), 2u); + // Parent + EXPECT_EQ(D.distance("tools/clang/lib/Format/"), 5u); + // Child + EXPECT_EQ(D.distance("tools/clang/lib/Format/FormatToken.cpp/Oops"), 3u); + // Ancestor (up+up+up+up) + EXPECT_EQ(D.distance("/"), 22u); + // Sibling (up+down) + EXPECT_EQ(D.distance("tools/clang/lib/Format/AnotherFile.cpp"), 8u); + // Cousin (up+up+down+down) + EXPECT_EQ(D.distance("include/llvm/Support/Allocator.h"), 18u); + // First cousin, once removed (up+up+up+down+down) + EXPECT_EQ(D.distance("include/llvm-c/Core.h"), 23u); +} + +TEST(FileDistanceTests, BadSource) { + // We mustn't assume that paths above sources are best reached via them. + FileDistanceOptions Opts; + Opts.UpCost = 5; + Opts.DownCost = 3; + SourceParams CostLots; + CostLots.Cost = 100; + FileDistance D({{"a", SourceParams()}, {"b/b/b", CostLots}}, Opts); + EXPECT_EQ(D.distance("b"), 8u); // a+up+down, not b+up+up + EXPECT_EQ(D.distance("b/b/b"), 14u); // a+up+down+down+down, not b + EXPECT_EQ(D.distance("b/b/b/c"), 17u); // a+up+down+down+down+down, not b+down +} + +// Force the unittest URI scheme to be linked, +static int LLVM_ATTRIBUTE_UNUSED UseUnittestScheme = UnittestSchemeAnchorSource; + +TEST(FileDistanceTests, URI) { + FileDistanceOptions Opts; + Opts.UpCost = 5; + Opts.DownCost = 3; + SourceParams CostLots; + CostLots.Cost = 1000; + + URIDistance D({{testPath("foo"), CostLots}, + {"/not/a/testpath", SourceParams()}, + {"C:\\not\\a\\testpath", SourceParams()}}, + Opts); +#ifdef _WIN32 + EXPECT_EQ(D.distance("file:///C%3a/not/a/testpath/either"), 3u); +#else + EXPECT_EQ(D.distance("file:///not/a/testpath/either"), 3u); +#endif + EXPECT_EQ(D.distance("unittest:///foo"), 1000u); + EXPECT_EQ(D.distance("unittest:///bar"), 1008u); +} + +TEST(FileDistance, LimitUpTraversals) { + FileDistanceOptions Opts; + Opts.UpCost = Opts.DownCost = 1; + SourceParams CheapButLimited, CostLots; + CheapButLimited.MaxUpTraversals = 1; + CostLots.Cost = 100; + + FileDistance D({{"/", CostLots}, {"/a/b/c", CheapButLimited}}, Opts); + EXPECT_EQ(D.distance("/a"), 101u); + EXPECT_EQ(D.distance("/a/z"), 102u); + EXPECT_EQ(D.distance("/a/b"), 1u); + EXPECT_EQ(D.distance("/a/b/z"), 2u); +} + +TEST(FileDistance, DisallowDownTraversalsFromRoot) { + FileDistanceOptions Opts; + Opts.UpCost = Opts.DownCost = 1; + Opts.AllowDownTraversalFromRoot = false; + SourceParams CostLots; + CostLots.Cost = 100; + + FileDistance D({{"/", SourceParams()}, {"/a/b/c", CostLots}}, Opts); + EXPECT_EQ(D.distance("/"), 0u); + EXPECT_EQ(D.distance("/a"), 102u); + EXPECT_EQ(D.distance("/a/b"), 101u); + EXPECT_EQ(D.distance("/x"), FileDistance::Unreachable); +} + +TEST(ScopeDistance, Smoke) { + ScopeDistance D({"x::y::z", "x::", "", "a::"}); + EXPECT_EQ(D.distance("x::y::z::"), 0u); + EXPECT_GT(D.distance("x::y::"), D.distance("x::y::z::")); + EXPECT_GT(D.distance("x::"), D.distance("x::y::")); + EXPECT_GT(D.distance("x::y::z::down::"), D.distance("x::y::")); + EXPECT_GT(D.distance(""), D.distance("a::")); + EXPECT_GT(D.distance("x::"), D.distance("a::")); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/FileIndexTests.cpp b/clangd/unittests/FileIndexTests.cpp new file mode 100644 index 00000000..142e2554 --- /dev/null +++ b/clangd/unittests/FileIndexTests.cpp @@ -0,0 +1,371 @@ +//===-- FileIndexTests.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 "AST.h" +#include "Annotations.h" +#include "ClangdUnit.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestTU.h" +#include "index/CanonicalIncludes.h" +#include "index/FileIndex.h" +#include "index/Index.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/Utils.h" +#include "clang/Index/IndexSymbol.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Tooling/CompilationDatabase.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::_; +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +MATCHER_P(RefRange, Range, "") { + return std::make_tuple(arg.Location.Start.line(), arg.Location.Start.column(), + arg.Location.End.line(), arg.Location.End.column()) == + std::make_tuple(Range.start.line, Range.start.character, + Range.end.line, Range.end.character); +} +MATCHER_P(FileURI, F, "") { return llvm::StringRef(arg.Location.FileURI) == F; } +MATCHER_P(DeclURI, U, "") { + return llvm::StringRef(arg.CanonicalDeclaration.FileURI) == U; +} +MATCHER_P(DefURI, U, "") { + return llvm::StringRef(arg.Definition.FileURI) == U; +} +MATCHER_P(QName, N, "") { return (arg.Scope + arg.Name).str() == N; } + +namespace clang { +namespace clangd { +namespace { +::testing::Matcher<const RefSlab &> +RefsAre(std::vector<::testing::Matcher<Ref>> Matchers) { + return ElementsAre(::testing::Pair(_, UnorderedElementsAreArray(Matchers))); +} + +Symbol symbol(llvm::StringRef ID) { + Symbol Sym; + Sym.ID = SymbolID(ID); + Sym.Name = ID; + return Sym; +} + +std::unique_ptr<SymbolSlab> numSlab(int Begin, int End) { + SymbolSlab::Builder Slab; + for (int i = Begin; i <= End; i++) + Slab.insert(symbol(std::to_string(i))); + return llvm::make_unique<SymbolSlab>(std::move(Slab).build()); +} + +std::unique_ptr<RefSlab> refSlab(const SymbolID &ID, const char *Path) { + RefSlab::Builder Slab; + Ref R; + R.Location.FileURI = Path; + R.Kind = RefKind::Reference; + Slab.insert(ID, R); + return llvm::make_unique<RefSlab>(std::move(Slab).build()); +} + +TEST(FileSymbolsTest, UpdateAndGet) { + FileSymbols FS; + EXPECT_THAT(runFuzzyFind(*FS.buildIndex(IndexType::Light), ""), IsEmpty()); + + FS.update("f1", numSlab(1, 3), refSlab(SymbolID("1"), "f1.cc")); + EXPECT_THAT(runFuzzyFind(*FS.buildIndex(IndexType::Light), ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"))); + EXPECT_THAT(getRefs(*FS.buildIndex(IndexType::Light), SymbolID("1")), + RefsAre({FileURI("f1.cc")})); +} + +TEST(FileSymbolsTest, Overlap) { + FileSymbols FS; + FS.update("f1", numSlab(1, 3), nullptr); + FS.update("f2", numSlab(3, 5), nullptr); + for (auto Type : {IndexType::Light, IndexType::Heavy}) + EXPECT_THAT(runFuzzyFind(*FS.buildIndex(Type), ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"), + QName("4"), QName("5"))); +} + +TEST(FileSymbolsTest, MergeOverlap) { + FileSymbols FS; + auto OneSymboSlab = [](Symbol Sym) { + SymbolSlab::Builder S; + S.insert(Sym); + return llvm::make_unique<SymbolSlab>(std::move(S).build()); + }; + auto X1 = symbol("x"); + X1.CanonicalDeclaration.FileURI = "file:///x1"; + auto X2 = symbol("x"); + X2.Definition.FileURI = "file:///x2"; + + FS.update("f1", OneSymboSlab(X1), nullptr); + FS.update("f2", OneSymboSlab(X2), nullptr); + for (auto Type : {IndexType::Light, IndexType::Heavy}) + EXPECT_THAT( + runFuzzyFind(*FS.buildIndex(Type, DuplicateHandling::Merge), "x"), + UnorderedElementsAre( + AllOf(QName("x"), DeclURI("file:///x1"), DefURI("file:///x2")))); +} + +TEST(FileSymbolsTest, SnapshotAliveAfterRemove) { + FileSymbols FS; + + SymbolID ID("1"); + FS.update("f1", numSlab(1, 3), refSlab(ID, "f1.cc")); + + auto Symbols = FS.buildIndex(IndexType::Light); + EXPECT_THAT(runFuzzyFind(*Symbols, ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"))); + EXPECT_THAT(getRefs(*Symbols, ID), RefsAre({FileURI("f1.cc")})); + + FS.update("f1", nullptr, nullptr); + auto Empty = FS.buildIndex(IndexType::Light); + EXPECT_THAT(runFuzzyFind(*Empty, ""), IsEmpty()); + EXPECT_THAT(getRefs(*Empty, ID), ElementsAre()); + + EXPECT_THAT(runFuzzyFind(*Symbols, ""), + UnorderedElementsAre(QName("1"), QName("2"), QName("3"))); + EXPECT_THAT(getRefs(*Symbols, ID), RefsAre({FileURI("f1.cc")})); +} + +// Adds Basename.cpp, which includes Basename.h, which contains Code. +void update(FileIndex &M, llvm::StringRef Basename, llvm::StringRef Code) { + TestTU File; + File.Filename = (Basename + ".cpp").str(); + File.HeaderFilename = (Basename + ".h").str(); + File.HeaderCode = Code; + auto AST = File.build(); + M.updatePreamble(File.Filename, AST.getASTContext(), AST.getPreprocessorPtr(), + AST.getCanonicalIncludes()); +} + +TEST(FileIndexTest, CustomizedURIScheme) { + FileIndex M; + update(M, "f", "class string {};"); + + EXPECT_THAT(runFuzzyFind(M, ""), ElementsAre(DeclURI("unittest:///f.h"))); +} + +TEST(FileIndexTest, IndexAST) { + FileIndex M; + update(M, "f1", "namespace ns { void f() {} class X {}; }"); + + FuzzyFindRequest Req; + Req.Query = ""; + Req.Scopes = {"ns::"}; + EXPECT_THAT(runFuzzyFind(M, Req), + UnorderedElementsAre(QName("ns::f"), QName("ns::X"))); +} + +TEST(FileIndexTest, NoLocal) { + FileIndex M; + update(M, "f1", "namespace ns { void f() { int local = 0; } class X {}; }"); + + EXPECT_THAT( + runFuzzyFind(M, ""), + UnorderedElementsAre(QName("ns"), QName("ns::f"), QName("ns::X"))); +} + +TEST(FileIndexTest, IndexMultiASTAndDeduplicate) { + FileIndex M; + update(M, "f1", "namespace ns { void f() {} class X {}; }"); + update(M, "f2", "namespace ns { void ff() {} class X {}; }"); + + FuzzyFindRequest Req; + Req.Scopes = {"ns::"}; + EXPECT_THAT( + runFuzzyFind(M, Req), + UnorderedElementsAre(QName("ns::f"), QName("ns::X"), QName("ns::ff"))); +} + +TEST(FileIndexTest, ClassMembers) { + FileIndex M; + update(M, "f1", "class X { static int m1; int m2; static void f(); };"); + + EXPECT_THAT(runFuzzyFind(M, ""), + UnorderedElementsAre(QName("X"), QName("X::m1"), QName("X::m2"), + QName("X::f"))); +} + +TEST(FileIndexTest, IncludeCollected) { + FileIndex M; + update( + M, "f", + "// IWYU pragma: private, include <the/good/header.h>\nclass string {};"); + + auto Symbols = runFuzzyFind(M, ""); + EXPECT_THAT(Symbols, ElementsAre(_)); + EXPECT_THAT(Symbols.begin()->IncludeHeaders.front().IncludeHeader, + "<the/good/header.h>"); +} + +TEST(FileIndexTest, HasSystemHeaderMappingsInPreamble) { + TestTU TU; + TU.HeaderCode = "class Foo{};"; + TU.HeaderFilename = "algorithm"; + + auto Symbols = runFuzzyFind(*TU.index(), ""); + EXPECT_THAT(Symbols, ElementsAre(_)); + EXPECT_THAT(Symbols.begin()->IncludeHeaders.front().IncludeHeader, + "<algorithm>"); +} + +TEST(FileIndexTest, TemplateParamsInLabel) { + auto Source = R"cpp( +template <class Ty> +class vector { +}; + +template <class Ty, class Arg> +vector<Ty> make_vector(Arg A) {} +)cpp"; + + FileIndex M; + update(M, "f", Source); + + auto Symbols = runFuzzyFind(M, ""); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("vector"), QName("make_vector"))); + auto It = Symbols.begin(); + Symbol Vector = *It++; + Symbol MakeVector = *It++; + if (MakeVector.Name == "vector") + std::swap(MakeVector, Vector); + + EXPECT_EQ(Vector.Signature, "<class Ty>"); + EXPECT_EQ(Vector.CompletionSnippetSuffix, "<${1:class Ty}>"); + + EXPECT_EQ(MakeVector.Signature, "<class Ty>(Arg A)"); + EXPECT_EQ(MakeVector.CompletionSnippetSuffix, "<${1:class Ty}>(${2:Arg A})"); +} + +TEST(FileIndexTest, RebuildWithPreamble) { + auto FooCpp = testPath("foo.cpp"); + auto FooH = testPath("foo.h"); + // Preparse ParseInputs. + ParseInputs PI; + PI.CompileCommand.Directory = testRoot(); + PI.CompileCommand.Filename = FooCpp; + PI.CompileCommand.CommandLine = {"clang", "-xc++", FooCpp}; + + llvm::StringMap<std::string> Files; + Files[FooCpp] = ""; + Files[FooH] = R"cpp( + namespace ns_in_header { + int func_in_header(); + } + )cpp"; + PI.FS = buildTestFS(std::move(Files)); + + PI.Contents = R"cpp( + #include "foo.h" + namespace ns_in_source { + int func_in_source(); + } + )cpp"; + + // Rebuild the file. + auto CI = buildCompilerInvocation(PI); + + FileIndex Index; + bool IndexUpdated = false; + buildPreamble( + FooCpp, *CI, /*OldPreamble=*/nullptr, tooling::CompileCommand(), PI, + /*StoreInMemory=*/true, + [&](ASTContext &Ctx, std::shared_ptr<Preprocessor> PP, + const CanonicalIncludes &CanonIncludes) { + EXPECT_FALSE(IndexUpdated) << "Expected only a single index update"; + IndexUpdated = true; + Index.updatePreamble(FooCpp, Ctx, std::move(PP), CanonIncludes); + }); + ASSERT_TRUE(IndexUpdated); + + // Check the index contains symbols from the preamble, but not from the main + // file. + FuzzyFindRequest Req; + Req.Query = ""; + Req.Scopes = {"", "ns_in_header::"}; + + EXPECT_THAT(runFuzzyFind(Index, Req), + UnorderedElementsAre(QName("ns_in_header"), + QName("ns_in_header::func_in_header"))); +} + +TEST(FileIndexTest, Refs) { + const char *HeaderCode = "class Foo {};"; + Annotations MainCode(R"cpp( + void f() { + $foo[[Foo]] foo; + } + )cpp"); + + auto Foo = + findSymbol(TestTU::withHeaderCode(HeaderCode).headerSymbols(), "Foo"); + + RefsRequest Request; + Request.IDs = {Foo.ID}; + + FileIndex Index; + // Add test.cc + TestTU Test; + Test.HeaderCode = HeaderCode; + Test.Code = MainCode.code(); + Test.Filename = "test.cc"; + auto AST = Test.build(); + Index.updateMain(Test.Filename, AST); + // Add test2.cc + TestTU Test2; + Test2.HeaderCode = HeaderCode; + Test2.Code = MainCode.code(); + Test2.Filename = "test2.cc"; + AST = Test2.build(); + Index.updateMain(Test2.Filename, AST); + + EXPECT_THAT(getRefs(Index, Foo.ID), + RefsAre({AllOf(RefRange(MainCode.range("foo")), + FileURI("unittest:///test.cc")), + AllOf(RefRange(MainCode.range("foo")), + FileURI("unittest:///test2.cc"))})); +} + +TEST(FileIndexTest, CollectMacros) { + FileIndex M; + update(M, "f", "#define CLANGD 1"); + EXPECT_THAT(runFuzzyFind(M, ""), Contains(QName("CLANGD"))); +} + +TEST(FileIndexTest, ReferencesInMainFileWithPreamble) { + TestTU TU; + TU.HeaderCode = "class Foo{};"; + Annotations Main(R"cpp( + #include "foo.h" + void f() { + [[Foo]] foo; + } + )cpp"); + TU.Code = Main.code(); + auto AST = TU.build(); + FileIndex Index; + Index.updateMain(testPath(TU.Filename), AST); + + // Expect to see references in main file, references in headers are excluded + // because we only index main AST. + EXPECT_THAT(getRefs(Index, findSymbol(TU.headerSymbols(), "Foo").ID), + RefsAre({RefRange(Main.range())})); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/FindSymbolsTests.cpp b/clangd/unittests/FindSymbolsTests.cpp new file mode 100644 index 00000000..f3112371 --- /dev/null +++ b/clangd/unittests/FindSymbolsTests.cpp @@ -0,0 +1,688 @@ +//===-- FindSymbolsTests.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 "ClangdServer.h" +#include "FindSymbols.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override {} +}; + +// GMock helpers for matching SymbolInfos items. +MATCHER_P(QName, Name, "") { + if (arg.containerName.empty()) + return arg.name == Name; + return (arg.containerName + "::" + arg.name) == Name; +} +MATCHER_P(WithName, N, "") { return arg.name == N; } +MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; } +MATCHER_P(SymRange, Range, "") { return arg.location.range == Range; } + +// GMock helpers for matching DocumentSymbol. +MATCHER_P(SymNameRange, Range, "") { return arg.selectionRange == Range; } +template <class... ChildMatchers> +::testing::Matcher<DocumentSymbol> Children(ChildMatchers... ChildrenM) { + return Field(&DocumentSymbol::children, ElementsAre(ChildrenM...)); +} + +ClangdServer::Options optsForTests() { + auto ServerOpts = ClangdServer::optsForTest(); + ServerOpts.WorkspaceRoot = testRoot(); + ServerOpts.BuildDynamicSymbolIndex = true; + return ServerOpts; +} + +class WorkspaceSymbolsTest : public ::testing::Test { +public: + WorkspaceSymbolsTest() + : Server(CDB, FSProvider, DiagConsumer, optsForTests()) { + // Make sure the test root directory is created. + FSProvider.Files[testPath("unused")] = ""; + CDB.ExtraClangFlags = {"-xc++"}; + } + +protected: + MockFSProvider FSProvider; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server; + int Limit = 0; + + std::vector<SymbolInformation> getSymbols(llvm::StringRef Query) { + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runWorkspaceSymbols(Server, Query, Limit); + EXPECT_TRUE(bool(SymbolInfos)) << "workspaceSymbols returned an error"; + return *SymbolInfos; + } + + void addFile(llvm::StringRef FileName, llvm::StringRef Contents) { + auto Path = testPath(FileName); + FSProvider.Files[Path] = Contents; + Server.addDocument(Path, Contents); + } +}; + +} // namespace + +TEST_F(WorkspaceSymbolsTest, Macros) { + addFile("foo.cpp", R"cpp( + #define MACRO X + )cpp"); + + // LSP's SymbolKind doesn't have a "Macro" kind, and + // indexSymbolKindToSymbolKind() currently maps macros + // to SymbolKind::String. + EXPECT_THAT(getSymbols("macro"), + ElementsAre(AllOf(QName("MACRO"), WithKind(SymbolKind::String)))); +} + +TEST_F(WorkspaceSymbolsTest, NoLocals) { + addFile("foo.cpp", R"cpp( + void test(int FirstParam, int SecondParam) { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols("l"), IsEmpty()); + EXPECT_THAT(getSymbols("p"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Globals) { + addFile("foo.h", R"cpp( + int global_var; + + int global_func(); + + struct GlobalStruct {};)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("global"), + UnorderedElementsAre( + AllOf(QName("GlobalStruct"), WithKind(SymbolKind::Struct)), + AllOf(QName("global_func"), WithKind(SymbolKind::Function)), + AllOf(QName("global_var"), WithKind(SymbolKind::Variable)))); +} + +TEST_F(WorkspaceSymbolsTest, Unnamed) { + addFile("foo.h", R"cpp( + struct { + int InUnnamed; + } UnnamedStruct;)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("UnnamedStruct"), + ElementsAre(AllOf(QName("UnnamedStruct"), + WithKind(SymbolKind::Variable)))); + EXPECT_THAT(getSymbols("InUnnamed"), + ElementsAre(AllOf(QName("(anonymous struct)::InUnnamed"), + WithKind(SymbolKind::Field)))); +} + +TEST_F(WorkspaceSymbolsTest, InMainFile) { + addFile("foo.cpp", R"cpp( + int test() {} + static test2() {} + )cpp"); + EXPECT_THAT(getSymbols("test"), ElementsAre(QName("test"), QName("test2"))); +} + +TEST_F(WorkspaceSymbolsTest, Namespaces) { + addFile("foo.h", R"cpp( + namespace ans1 { + int ai1; + namespace ans2 { + int ai2; + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("a"), + UnorderedElementsAre(QName("ans1"), QName("ans1::ai1"), + QName("ans1::ans2"), + QName("ans1::ans2::ai2"))); + EXPECT_THAT(getSymbols("::"), ElementsAre(QName("ans1"))); + EXPECT_THAT(getSymbols("::a"), ElementsAre(QName("ans1"))); + EXPECT_THAT(getSymbols("ans1::"), + UnorderedElementsAre(QName("ans1::ai1"), QName("ans1::ans2"))); + EXPECT_THAT(getSymbols("::ans1"), ElementsAre(QName("ans1"))); + EXPECT_THAT(getSymbols("::ans1::"), + UnorderedElementsAre(QName("ans1::ai1"), QName("ans1::ans2"))); + EXPECT_THAT(getSymbols("::ans1::ans2"), ElementsAre(QName("ans1::ans2"))); + EXPECT_THAT(getSymbols("::ans1::ans2::"), + ElementsAre(QName("ans1::ans2::ai2"))); +} + +TEST_F(WorkspaceSymbolsTest, AnonymousNamespace) { + addFile("foo.h", R"cpp( + namespace { + void test() {} + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("test"), ElementsAre(QName("test"))); +} + +TEST_F(WorkspaceSymbolsTest, MultiFile) { + addFile("foo.h", R"cpp( + int foo() { + } + )cpp"); + addFile("foo2.h", R"cpp( + int foo2() { + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + #include "foo2.h" + )cpp"); + EXPECT_THAT(getSymbols("foo"), + UnorderedElementsAre(QName("foo"), QName("foo2"))); +} + +TEST_F(WorkspaceSymbolsTest, GlobalNamespaceQueries) { + addFile("foo.h", R"cpp( + int foo() { + } + class Foo { + int a; + }; + namespace ns { + int foo2() { + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("::"), + UnorderedElementsAre( + AllOf(QName("Foo"), WithKind(SymbolKind::Class)), + AllOf(QName("foo"), WithKind(SymbolKind::Function)), + AllOf(QName("ns"), WithKind(SymbolKind::Namespace)))); + EXPECT_THAT(getSymbols(":"), IsEmpty()); + EXPECT_THAT(getSymbols(""), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Enums) { + addFile("foo.h", R"cpp( + enum { + Red + }; + enum Color { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + enum Color3 { + Blue + }; + enum class Color4 { + White + }; + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("Red"), ElementsAre(QName("Red"))); + EXPECT_THAT(getSymbols("::Red"), ElementsAre(QName("Red"))); + EXPECT_THAT(getSymbols("Green"), ElementsAre(QName("Green"))); + EXPECT_THAT(getSymbols("Green"), ElementsAre(QName("Green"))); + EXPECT_THAT(getSymbols("Color2::Yellow"), + ElementsAre(QName("Color2::Yellow"))); + EXPECT_THAT(getSymbols("Yellow"), ElementsAre(QName("Color2::Yellow"))); + + EXPECT_THAT(getSymbols("ns::Black"), ElementsAre(QName("ns::Black"))); + EXPECT_THAT(getSymbols("ns::Blue"), ElementsAre(QName("ns::Blue"))); + EXPECT_THAT(getSymbols("ns::Color4::White"), + ElementsAre(QName("ns::Color4::White"))); +} + +TEST_F(WorkspaceSymbolsTest, Ranking) { + addFile("foo.h", R"cpp( + namespace ns{} + void func(); + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("::"), ElementsAre(QName("func"), QName("ns"))); +} + +TEST_F(WorkspaceSymbolsTest, WithLimit) { + addFile("foo.h", R"cpp( + int foo; + int foo2; + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + // Foo is higher ranked because of exact name match. + EXPECT_THAT(getSymbols("foo"), + UnorderedElementsAre( + AllOf(QName("foo"), WithKind(SymbolKind::Variable)), + AllOf(QName("foo2"), WithKind(SymbolKind::Variable)))); + + Limit = 1; + EXPECT_THAT(getSymbols("foo"), ElementsAre(QName("foo"))); +} + +TEST_F(WorkspaceSymbolsTest, TempSpecs) { + addFile("foo.h", R"cpp( + template <typename T, typename U, int X = 5> class Foo {}; + template <typename T> class Foo<int, T> {}; + template <> class Foo<bool, int> {}; + template <> class Foo<bool, int, 3> {}; + )cpp"); + // Foo is higher ranked because of exact name match. + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(QName("Foo"), WithKind(SymbolKind::Class)), + AllOf(QName("Foo<int, T>"), WithKind(SymbolKind::Class)), + AllOf(QName("Foo<bool, int>"), WithKind(SymbolKind::Class)), + AllOf(QName("Foo<bool, int, 3>"), WithKind(SymbolKind::Class)))); +} + +namespace { +class DocumentSymbolsTest : public ::testing::Test { +public: + DocumentSymbolsTest() + : Server(CDB, FSProvider, DiagConsumer, optsForTests()) {} + +protected: + MockFSProvider FSProvider; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server; + + std::vector<DocumentSymbol> getSymbols(PathRef File) { + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runDocumentSymbols(Server, File); + EXPECT_TRUE(bool(SymbolInfos)) << "documentSymbols returned an error"; + return *SymbolInfos; + } + + void addFile(llvm::StringRef FilePath, llvm::StringRef Contents) { + FSProvider.Files[FilePath] = Contents; + Server.addDocument(FilePath, Contents); + } +}; +} // namespace + +TEST_F(DocumentSymbolsTest, BasicSymbols) { + std::string FilePath = testPath("foo.cpp"); + Annotations Main(R"( + class Foo; + class Foo { + Foo() {} + Foo(int a) {} + void $decl[[f]](); + friend void f1(); + friend class Friend; + Foo& operator=(const Foo&); + ~Foo(); + class Nested { + void f(); + }; + }; + class Friend { + }; + + void f1(); + inline void f2() {} + static const int KInt = 2; + const char* kStr = "123"; + + void f1() {} + + namespace foo { + // Type alias + typedef int int32; + using int32_t = int32; + + // Variable + int v1; + + // Namespace + namespace bar { + int v2; + } + // Namespace alias + namespace baz = bar; + + using bar::v2; + } // namespace foo + )"); + + addFile(FilePath, Main.code()); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAreArray( + {AllOf(WithName("Foo"), WithKind(SymbolKind::Class), Children()), + AllOf(WithName("Foo"), WithKind(SymbolKind::Class), + Children(AllOf(WithName("Foo"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("Foo"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("f"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("operator="), + WithKind(SymbolKind::Method), Children()), + AllOf(WithName("~Foo"), WithKind(SymbolKind::Method), + Children()), + AllOf(WithName("Nested"), WithKind(SymbolKind::Class), + Children(AllOf(WithName("f"), + WithKind(SymbolKind::Method), + Children()))))), + AllOf(WithName("Friend"), WithKind(SymbolKind::Class), Children()), + AllOf(WithName("f1"), WithKind(SymbolKind::Function), Children()), + AllOf(WithName("f2"), WithKind(SymbolKind::Function), Children()), + AllOf(WithName("KInt"), WithKind(SymbolKind::Variable), Children()), + AllOf(WithName("kStr"), WithKind(SymbolKind::Variable), Children()), + AllOf(WithName("f1"), WithKind(SymbolKind::Function), Children()), + AllOf(WithName("foo"), WithKind(SymbolKind::Namespace), + Children( + AllOf(WithName("int32"), WithKind(SymbolKind::Class), + Children()), + AllOf(WithName("int32_t"), WithKind(SymbolKind::Class), + Children()), + AllOf(WithName("v1"), WithKind(SymbolKind::Variable), + Children()), + AllOf(WithName("bar"), WithKind(SymbolKind::Namespace), + Children(AllOf(WithName("v2"), + WithKind(SymbolKind::Variable), + Children()))), + AllOf(WithName("baz"), WithKind(SymbolKind::Namespace), + Children()), + AllOf(WithName("v2"), WithKind(SymbolKind::Namespace))))})); +} + +TEST_F(DocumentSymbolsTest, DeclarationDefinition) { + std::string FilePath = testPath("foo.cpp"); + Annotations Main(R"( + class Foo { + void $decl[[f]](); + }; + void Foo::$def[[f]]() { + } + )"); + + addFile(FilePath, Main.code()); + EXPECT_THAT(getSymbols(FilePath), + ElementsAre(AllOf(WithName("Foo"), WithKind(SymbolKind::Class), + Children(AllOf( + WithName("f"), WithKind(SymbolKind::Method), + SymNameRange(Main.range("decl"))))), + AllOf(WithName("f"), WithKind(SymbolKind::Method), + SymNameRange(Main.range("def"))))); +} + +TEST_F(DocumentSymbolsTest, ExternSymbol) { + std::string FilePath = testPath("foo.cpp"); + addFile(testPath("foo.h"), R"cpp( + extern int var; + )cpp"); + addFile(FilePath, R"cpp( + #include "foo.h" + )cpp"); + + EXPECT_THAT(getSymbols(FilePath), IsEmpty()); +} + +TEST_F(DocumentSymbolsTest, NoLocals) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, + R"cpp( + void test(int FirstParam, int SecondParam) { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols(FilePath), ElementsAre(WithName("test"))); +} + +TEST_F(DocumentSymbolsTest, Unnamed) { + std::string FilePath = testPath("foo.h"); + addFile(FilePath, + R"cpp( + struct { + int InUnnamed; + } UnnamedStruct; + )cpp"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("(anonymous struct)"), WithKind(SymbolKind::Struct), + Children(AllOf(WithName("InUnnamed"), + WithKind(SymbolKind::Field), Children()))), + AllOf(WithName("UnnamedStruct"), WithKind(SymbolKind::Variable), + Children()))); +} + +TEST_F(DocumentSymbolsTest, InHeaderFile) { + addFile(testPath("bar.h"), R"cpp( + int foo() { + } + )cpp"); + std::string FilePath = testPath("foo.h"); + addFile(FilePath, R"cpp( + #include "bar.h" + int test() { + } + )cpp"); + addFile(testPath("foo.cpp"), R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols(FilePath), ElementsAre(WithName("test"))); +} + +TEST_F(DocumentSymbolsTest, Template) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, R"( + template <class T> struct Tmpl {T x = 0;}; + template <> struct Tmpl<int> { + int y = 0; + }; + extern template struct Tmpl<float>; + template struct Tmpl<double>; + + template <class T, class U, class Z = float> + int funcTmpl(U a); + template <> + int funcTmpl<int>(double a); + + template <class T, class U = double> + int varTmpl = T(); + template <> + double varTmpl<int> = 10.0; + )"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("Tmpl"), WithKind(SymbolKind::Struct), + Children(AllOf(WithName("x"), WithKind(SymbolKind::Field)))), + AllOf(WithName("Tmpl<int>"), WithKind(SymbolKind::Struct), + Children(WithName("y"))), + AllOf(WithName("Tmpl<float>"), WithKind(SymbolKind::Struct), + Children()), + AllOf(WithName("Tmpl<double>"), WithKind(SymbolKind::Struct), + Children()), + AllOf(WithName("funcTmpl"), Children()), + AllOf(WithName("funcTmpl<int>"), Children()), + AllOf(WithName("varTmpl"), Children()), + AllOf(WithName("varTmpl<int>"), Children()))); +} + +TEST_F(DocumentSymbolsTest, Namespaces) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, R"cpp( + namespace ans1 { + int ai1; + namespace ans2 { + int ai2; + } + } + namespace { + void test() {} + } + + namespace na { + inline namespace nb { + class Foo {}; + } + } + namespace na { + // This is still inlined. + namespace nb { + class Bar {}; + } + } + )cpp"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAreArray<::testing::Matcher<DocumentSymbol>>( + {AllOf(WithName("ans1"), + Children(AllOf(WithName("ai1"), Children()), + AllOf(WithName("ans2"), Children(WithName("ai2"))))), + AllOf(WithName("(anonymous namespace)"), Children(WithName("test"))), + AllOf(WithName("na"), + Children(AllOf(WithName("nb"), Children(WithName("Foo"))))), + AllOf(WithName("na"), + Children(AllOf(WithName("nb"), Children(WithName("Bar")))))})); +} + +TEST_F(DocumentSymbolsTest, Enums) { + std::string FilePath = testPath("foo.cpp"); + addFile(FilePath, R"( + enum { + Red + }; + enum Color { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + } + )"); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("(anonymous enum)"), Children(WithName("Red"))), + AllOf(WithName("Color"), Children(WithName("Green"))), + AllOf(WithName("Color2"), Children(WithName("Yellow"))), + AllOf(WithName("ns"), Children(AllOf(WithName("(anonymous enum)"), + Children(WithName("Black"))))))); +} + +TEST_F(DocumentSymbolsTest, FromMacro) { + std::string FilePath = testPath("foo.cpp"); + Annotations Main(R"( + #define FF(name) \ + class name##_Test {}; + + $expansion[[FF]](abc); + + #define FF2() \ + class $spelling[[Test]] {}; + + FF2(); + )"); + addFile(FilePath, Main.code()); + EXPECT_THAT( + getSymbols(FilePath), + ElementsAre( + AllOf(WithName("abc_Test"), SymNameRange(Main.range("expansion"))), + AllOf(WithName("Test"), SymNameRange(Main.range("spelling"))))); +} + +TEST_F(DocumentSymbolsTest, FuncTemplates) { + std::string FilePath = testPath("foo.cpp"); + Annotations Source(R"cpp( + template <class T> + T foo() {} + + auto x = foo<int>(); + auto y = foo<double>() + )cpp"); + addFile(FilePath, Source.code()); + // Make sure we only see the template declaration, not instantiations. + EXPECT_THAT(getSymbols(FilePath), + ElementsAre(WithName("foo"), WithName("x"), WithName("y"))); +} + +TEST_F(DocumentSymbolsTest, UsingDirectives) { + std::string FilePath = testPath("foo.cpp"); + Annotations Source(R"cpp( + namespace ns { + int foo; + } + + namespace ns_alias = ns; + + using namespace ::ns; // check we don't loose qualifiers. + using namespace ns_alias; // and namespace aliases. + )cpp"); + addFile(FilePath, Source.code()); + EXPECT_THAT(getSymbols(FilePath), + ElementsAre(WithName("ns"), WithName("ns_alias"), + WithName("using namespace ::ns"), + WithName("using namespace ns_alias"))); +} + +TEST_F(DocumentSymbolsTest, TempSpecs) { + addFile("foo.cpp", R"cpp( + template <typename T, typename U, int X = 5> class Foo {}; + template <typename T> class Foo<int, T> {}; + template <> class Foo<bool, int> {}; + template <> class Foo<bool, int, 3> {}; + )cpp"); + // Foo is higher ranked because of exact name match. + EXPECT_THAT( + getSymbols("foo.cpp"), + UnorderedElementsAre( + AllOf(WithName("Foo"), WithKind(SymbolKind::Class)), + AllOf(WithName("Foo<int, T>"), WithKind(SymbolKind::Class)), + AllOf(WithName("Foo<bool, int>"), WithKind(SymbolKind::Class)), + AllOf(WithName("Foo<bool, int, 3>"), WithKind(SymbolKind::Class)))); +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/FunctionTests.cpp b/clangd/unittests/FunctionTests.cpp new file mode 100644 index 00000000..0cd8b791 --- /dev/null +++ b/clangd/unittests/FunctionTests.cpp @@ -0,0 +1,51 @@ +//===-- FunctionTests.cpp -------------------------------------------------===// +// +// 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 "Function.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(EventTest, Subscriptions) { + Event<int> E; + int N = 0; + { + Event<int>::Subscription SubA; + // No subscriptions are active. + E.broadcast(42); + EXPECT_EQ(0, N); + + Event<int>::Subscription SubB = E.observe([&](int) { ++N; }); + // Now one is active. + E.broadcast(42); + EXPECT_EQ(1, N); + + SubA = E.observe([&](int) { ++N; }); + // Both are active. + EXPECT_EQ(1, N); + E.broadcast(42); + EXPECT_EQ(3, N); + + SubA = std::move(SubB); + // One is active. + EXPECT_EQ(3, N); + E.broadcast(42); + EXPECT_EQ(4, N); + } + // None are active. + EXPECT_EQ(4, N); + E.broadcast(42); + EXPECT_EQ(4, N); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/FuzzyMatchTests.cpp b/clangd/unittests/FuzzyMatchTests.cpp new file mode 100644 index 00000000..33d202b4 --- /dev/null +++ b/clangd/unittests/FuzzyMatchTests.cpp @@ -0,0 +1,312 @@ +//===-- FuzzyMatchTests.cpp - String fuzzy matcher tests --------*- 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 "FuzzyMatch.h" + +#include "llvm/ADT/StringExtras.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using ::testing::Not; + +struct ExpectedMatch { + // Annotations are optional, and will not be asserted if absent. + ExpectedMatch(llvm::StringRef Match) : Word(Match), Annotated(Match) { + for (char C : "[]") + Word.erase(std::remove(Word.begin(), Word.end(), C), Word.end()); + if (Word.size() == Annotated->size()) + Annotated = llvm::None; + } + bool accepts(llvm::StringRef ActualAnnotated) const { + return !Annotated || ActualAnnotated == *Annotated; + } + + friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, + const ExpectedMatch &M) { + OS << "'" << M.Word; + if (M.Annotated) + OS << "' as " << *M.Annotated; + return OS; + } + + std::string Word; + +private: + llvm::Optional<llvm::StringRef> Annotated; +}; + +struct MatchesMatcher : public ::testing::MatcherInterface<llvm::StringRef> { + ExpectedMatch Candidate; + llvm::Optional<float> Score; + MatchesMatcher(ExpectedMatch Candidate, llvm::Optional<float> Score) + : Candidate(std::move(Candidate)), Score(Score) {} + + void DescribeTo(::std::ostream *OS) const override { + llvm::raw_os_ostream(*OS) << "Matches " << Candidate; + if (Score) + *OS << " with score " << *Score; + } + + bool MatchAndExplain(llvm::StringRef Pattern, + ::testing::MatchResultListener *L) const override { + std::unique_ptr<llvm::raw_ostream> OS( + L->stream() + ? (llvm::raw_ostream *)(new llvm::raw_os_ostream(*L->stream())) + : new llvm::raw_null_ostream()); + FuzzyMatcher Matcher(Pattern); + auto Result = Matcher.match(Candidate.Word); + auto AnnotatedMatch = Matcher.dumpLast(*OS << "\n"); + return Result && Candidate.accepts(AnnotatedMatch) && + (!Score || ::testing::Value(*Result, ::testing::FloatEq(*Score))); + } +}; + +// Accepts patterns that match a given word, optionally requiring a score. +// Dumps the debug tables on match failure. +::testing::Matcher<llvm::StringRef> matches(llvm::StringRef M, + llvm::Optional<float> Score = {}) { + return ::testing::MakeMatcher<llvm::StringRef>(new MatchesMatcher(M, Score)); +} + +TEST(FuzzyMatch, Matches) { + EXPECT_THAT("", matches("unique_ptr")); + EXPECT_THAT("u_p", matches("[u]nique[_p]tr")); + EXPECT_THAT("up", matches("[u]nique_[p]tr")); + EXPECT_THAT("uq", Not(matches("unique_ptr"))); + EXPECT_THAT("qp", Not(matches("unique_ptr"))); + EXPECT_THAT("log", Not(matches("SVGFEMorphologyElement"))); + + EXPECT_THAT("tit", matches("win.[tit]")); + EXPECT_THAT("title", matches("win.[title]")); + EXPECT_THAT("WordCla", matches("[Word]Character[Cla]ssifier")); + EXPECT_THAT("WordCCla", matches("[WordC]haracter[Cla]ssifier")); + + EXPECT_THAT("dete", Not(matches("editor.quickSuggestionsDelay"))); + + EXPECT_THAT("highlight", matches("editorHover[Highlight]")); + EXPECT_THAT("hhighlight", matches("editor[H]over[Highlight]")); + EXPECT_THAT("dhhighlight", Not(matches("editorHoverHighlight"))); + + EXPECT_THAT("-moz", matches("[-moz]-foo")); + EXPECT_THAT("moz", matches("-[moz]-foo")); + EXPECT_THAT("moza", matches("-[moz]-[a]nimation")); + + EXPECT_THAT("ab", matches("[ab]A")); + EXPECT_THAT("ccm", Not(matches("cacmelCase"))); + EXPECT_THAT("bti", Not(matches("the_black_knight"))); + EXPECT_THAT("ccm", Not(matches("camelCase"))); + EXPECT_THAT("cmcm", Not(matches("camelCase"))); + EXPECT_THAT("BK", matches("the_[b]lack_[k]night")); + EXPECT_THAT("KeyboardLayout=", Not(matches("KeyboardLayout"))); + EXPECT_THAT("LLL", matches("SVisual[L]ogger[L]ogs[L]ist")); + EXPECT_THAT("LLLL", Not(matches("SVilLoLosLi"))); + EXPECT_THAT("LLLL", Not(matches("SVisualLoggerLogsList"))); + EXPECT_THAT("TEdit", matches("[T]ext[Edit]")); + EXPECT_THAT("TEdit", matches("[T]ext[Edit]or")); + EXPECT_THAT("TEdit", Not(matches("[T]ext[edit]"))); + EXPECT_THAT("TEdit", matches("[t]ext_[edit]")); + EXPECT_THAT("TEditDt", matches("[T]ext[Edit]or[D]ecoration[T]ype")); + EXPECT_THAT("TEdit", matches("[T]ext[Edit]orDecorationType")); + EXPECT_THAT("Tedit", matches("[T]ext[Edit]")); + EXPECT_THAT("ba", Not(matches("?AB?"))); + EXPECT_THAT("bkn", matches("the_[b]lack_[kn]ight")); + EXPECT_THAT("bt", Not(matches("the_[b]lack_knigh[t]"))); + EXPECT_THAT("ccm", Not(matches("[c]amelCase[cm]"))); + EXPECT_THAT("fdm", Not(matches("[f]in[dM]odel"))); + EXPECT_THAT("fob", Not(matches("[fo]o[b]ar"))); + EXPECT_THAT("fobz", Not(matches("foobar"))); + EXPECT_THAT("foobar", matches("[foobar]")); + EXPECT_THAT("form", matches("editor.[form]atOnSave")); + EXPECT_THAT("g p", matches("[G]it:[ P]ull")); + EXPECT_THAT("g p", matches("[G]it:[ P]ull")); + EXPECT_THAT("gip", matches("[Gi]t: [P]ull")); + EXPECT_THAT("gip", matches("[Gi]t: [P]ull")); + EXPECT_THAT("gp", matches("[G]it: [P]ull")); + EXPECT_THAT("gp", matches("[G]it_Git_[P]ull")); + EXPECT_THAT("is", matches("[I]mport[S]tatement")); + EXPECT_THAT("is", matches("[is]Valid")); + EXPECT_THAT("lowrd", Not(matches("[low]Wo[rd]"))); + EXPECT_THAT("myvable", Not(matches("[myva]ria[ble]"))); + EXPECT_THAT("no", Not(matches(""))); + EXPECT_THAT("no", Not(matches("match"))); + EXPECT_THAT("ob", Not(matches("foobar"))); + EXPECT_THAT("sl", matches("[S]Visual[L]oggerLogsList")); + EXPECT_THAT("sllll", matches("[S]Visua[L]ogger[Ll]ama[L]ist")); + EXPECT_THAT("THRE", matches("H[T]ML[HRE]lement")); + EXPECT_THAT("b", Not(matches("NDEBUG"))); + EXPECT_THAT("Three", matches("[Three]")); + EXPECT_THAT("fo", Not(matches("barfoo"))); + EXPECT_THAT("fo", matches("bar_[fo]o")); + EXPECT_THAT("fo", matches("bar_[Fo]o")); + EXPECT_THAT("fo", matches("bar [fo]o")); + EXPECT_THAT("fo", matches("bar.[fo]o")); + EXPECT_THAT("fo", matches("bar/[fo]o")); + EXPECT_THAT("fo", matches("bar\\[fo]o")); + + EXPECT_THAT( + "aaaaaa", + matches("[aaaaaa]aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + EXPECT_THAT("baba", Not(matches("ababababab"))); + EXPECT_THAT("fsfsfs", Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsa"))); + EXPECT_THAT("fsfsfsfsfsfsfsf", + Not(matches("dsafdsafdsafdsafdsafdsafdsafasdfdsafdsafdsafdsafdsfd" + "safdsfdfdfasdnfdsajfndsjnafjndsajlknfdsa"))); + + EXPECT_THAT(" g", matches("[ g]roup")); + EXPECT_THAT("g", matches(" [g]roup")); + EXPECT_THAT("g g", Not(matches(" groupGroup"))); + EXPECT_THAT("g g", matches(" [g]roup[ G]roup")); + EXPECT_THAT(" g g", matches("[ ] [g]roup[ G]roup")); + EXPECT_THAT("zz", matches("[zz]Group")); + EXPECT_THAT("zzg", matches("[zzG]roup")); + EXPECT_THAT("g", matches("zz[G]roup")); + + EXPECT_THAT("aaaa", matches("_a_[aaaa]")); // Prefer consecutive. + // These would ideally match, but would need special segmentation rules. + EXPECT_THAT("printf", Not(matches("s[printf]"))); + EXPECT_THAT("str", Not(matches("o[str]eam"))); + EXPECT_THAT("strcpy", Not(matches("strncpy"))); + EXPECT_THAT("std", Not(matches("PTHREAD_MUTEX_STALLED"))); + EXPECT_THAT("std", Not(matches("pthread_condattr_setpshared"))); +} + +struct RankMatcher : public ::testing::MatcherInterface<llvm::StringRef> { + std::vector<ExpectedMatch> RankedStrings; + RankMatcher(std::initializer_list<ExpectedMatch> RankedStrings) + : RankedStrings(RankedStrings) {} + + void DescribeTo(::std::ostream *OS) const override { + llvm::raw_os_ostream O(*OS); + O << "Ranks strings in order: ["; + for (const auto &Str : RankedStrings) + O << "\n\t" << Str; + O << "\n]"; + } + + bool MatchAndExplain(llvm::StringRef Pattern, + ::testing::MatchResultListener *L) const override { + std::unique_ptr<llvm::raw_ostream> OS( + L->stream() + ? (llvm::raw_ostream *)(new llvm::raw_os_ostream(*L->stream())) + : new llvm::raw_null_ostream()); + FuzzyMatcher Matcher(Pattern); + const ExpectedMatch *LastMatch; + llvm::Optional<float> LastScore; + bool Ok = true; + for (const auto &Str : RankedStrings) { + auto Score = Matcher.match(Str.Word); + if (!Score) { + *OS << "\nDoesn't match '" << Str.Word << "'"; + Matcher.dumpLast(*OS << "\n"); + Ok = false; + } else { + std::string Buf; + llvm::raw_string_ostream Info(Buf); + auto AnnotatedMatch = Matcher.dumpLast(Info); + + if (!Str.accepts(AnnotatedMatch)) { + *OS << "\nDoesn't match " << Str << ", but " << AnnotatedMatch << "\n" + << Info.str(); + Ok = false; + } else if (LastScore && *LastScore < *Score) { + *OS << "\nRanks '" << Str.Word << "'=" << *Score << " above '" + << LastMatch->Word << "'=" << *LastScore << "\n" + << Info.str(); + Matcher.match(LastMatch->Word); + Matcher.dumpLast(*OS << "\n"); + Ok = false; + } + } + LastMatch = &Str; + LastScore = Score; + } + return Ok; + } +}; + +// Accepts patterns that match all the strings and rank them in the given order. +// Dumps the debug tables on match failure. +template <typename... T> +::testing::Matcher<llvm::StringRef> ranks(T... RankedStrings) { + return ::testing::MakeMatcher<llvm::StringRef>( + new RankMatcher{ExpectedMatch(RankedStrings)...}); +} + +TEST(FuzzyMatch, Ranking) { + EXPECT_THAT("cons", + ranks("[cons]ole", "[Cons]ole", "ArrayBuffer[Cons]tructor")); + EXPECT_THAT("foo", ranks("[foo]", "[Foo]")); + EXPECT_THAT("onMes", + ranks("[onMes]sage", "[onmes]sage", "[on]This[M]ega[Es]capes")); + EXPECT_THAT("onmes", + ranks("[onmes]sage", "[onMes]sage", "[on]This[M]ega[Es]capes")); + EXPECT_THAT("CC", ranks("[C]amel[C]ase", "[c]amel[C]ase")); + EXPECT_THAT("cC", ranks("[c]amel[C]ase", "[C]amel[C]ase")); + EXPECT_THAT("p", ranks("[p]", "[p]arse", "[p]osix", "[p]afdsa", "[p]ath")); + EXPECT_THAT("pa", ranks("[pa]rse", "[pa]th", "[pa]fdsa")); + EXPECT_THAT("log", ranks("[log]", "Scroll[Log]icalPosition")); + EXPECT_THAT("e", ranks("[e]lse", "Abstract[E]lement")); + EXPECT_THAT("workbench.sideb", + ranks("[workbench.sideB]ar.location", + "[workbench.]editor.default[SideB]ySideLayout")); + EXPECT_THAT("editor.r", ranks("[editor.r]enderControlCharacter", + "[editor.]overview[R]ulerlanes", + "diff[Editor.r]enderSideBySide")); + EXPECT_THAT("-mo", ranks("[-mo]z-columns", "[-]ms-ime-[mo]de")); + EXPECT_THAT("convertModelPosition", + ranks("[convertModelPosition]ToViewPosition", + "[convert]ViewTo[ModelPosition]")); + EXPECT_THAT("is", ranks("[is]ValidViewletId", "[i]mport [s]tatement")); + EXPECT_THAT("strcpy", ranks("[strcpy]", "[strcpy]_s")); +} + +// Verify some bounds so we know scores fall in the right range. +// Testing exact scores is fragile, so we prefer Ranking tests. +TEST(FuzzyMatch, Scoring) { + EXPECT_THAT("abs", matches("[a]w[B]xYz[S]", 7.f / 12.f)); + EXPECT_THAT("abs", matches("[abs]l", 1.f)); + EXPECT_THAT("abs", matches("[abs]", 2.f)); + EXPECT_THAT("Abs", matches("[abs]", 2.f)); +} + +TEST(FuzzyMatch, InitialismAndPrefix) { + // We want these scores to be roughly the same. + EXPECT_THAT("up", matches("[u]nique_[p]tr", 3.f / 4.f)); + EXPECT_THAT("up", matches("[up]per_bound", 1.f)); +} + +// Returns pretty-printed segmentation of Text. +// e.g. std::basic_string --> +-- +---- +----- +std::string segment(llvm::StringRef Text) { + std::vector<CharRole> Roles(Text.size()); + calculateRoles(Text, Roles); + std::string Printed; + for (unsigned I = 0; I < Text.size(); ++I) + Printed.push_back("?-+ "[static_cast<unsigned>(Roles[I])]); + return Printed; +} + +// this is a no-op hack so clang-format will vertically align our testcases. +llvm::StringRef returns(llvm::StringRef Text) { return Text; } + +TEST(FuzzyMatch, Segmentation) { + EXPECT_THAT(segment("std::basic_string"), // + returns("+-- +---- +-----")); + EXPECT_THAT(segment("XMLHttpRequest"), // + returns("+--+---+------")); + EXPECT_THAT(segment("t3h PeNgU1N oF d00m!!!!!!!!"), // + returns("+-- +-+-+-+ ++ +--- ")); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/GlobalCompilationDatabaseTests.cpp b/clangd/unittests/GlobalCompilationDatabaseTests.cpp new file mode 100644 index 00000000..7c7993cc --- /dev/null +++ b/clangd/unittests/GlobalCompilationDatabaseTests.cpp @@ -0,0 +1,151 @@ +//===-- GlobalCompilationDatabaseTests.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 "GlobalCompilationDatabase.h" + +#include "TestFS.h" +#include "llvm/ADT/StringExtras.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::EndsWith; +using ::testing::Not; + +TEST(GlobalCompilationDatabaseTest, FallbackCommand) { + DirectoryBasedGlobalCompilationDatabase DB(None); + auto Cmd = DB.getFallbackCommand(testPath("foo/bar.cc")); + EXPECT_EQ(Cmd.Directory, testPath("foo")); + EXPECT_THAT(Cmd.CommandLine, + ElementsAre(EndsWith("clang"), testPath("foo/bar.cc"))); + EXPECT_EQ(Cmd.Output, ""); + + // .h files have unknown language, so they are parsed liberally as obj-c++. + Cmd = DB.getFallbackCommand(testPath("foo/bar.h")); + EXPECT_THAT(Cmd.CommandLine, + ElementsAre(EndsWith("clang"), "-xobjective-c++-header", + testPath("foo/bar.h"))); +} + +static tooling::CompileCommand cmd(llvm::StringRef File, llvm::StringRef Arg) { + return tooling::CompileCommand(testRoot(), File, {"clang", Arg, File}, ""); +} + +class OverlayCDBTest : public ::testing::Test { + class BaseCDB : public GlobalCompilationDatabase { + public: + llvm::Optional<tooling::CompileCommand> + getCompileCommand(llvm::StringRef File, + ProjectInfo *Project) const override { + if (File == testPath("foo.cc")) { + if (Project) + Project->SourceRoot = testRoot(); + return cmd(File, "-DA=1"); + } + return None; + } + + tooling::CompileCommand + getFallbackCommand(llvm::StringRef File) const override { + return cmd(File, "-DA=2"); + } + }; + +protected: + OverlayCDBTest() : Base(llvm::make_unique<BaseCDB>()) {} + std::unique_ptr<GlobalCompilationDatabase> Base; +}; + +TEST_F(OverlayCDBTest, GetCompileCommand) { + OverlayCDB CDB(Base.get(), {}, std::string("")); + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc"))->CommandLine, + AllOf(Contains(testPath("foo.cc")), Contains("-DA=1"))); + EXPECT_EQ(CDB.getCompileCommand(testPath("missing.cc")), llvm::None); + + auto Override = cmd(testPath("foo.cc"), "-DA=3"); + CDB.setCompileCommand(testPath("foo.cc"), Override); + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc"))->CommandLine, + Contains("-DA=3")); + EXPECT_EQ(CDB.getCompileCommand(testPath("missing.cc")), llvm::None); + CDB.setCompileCommand(testPath("missing.cc"), Override); + EXPECT_THAT(CDB.getCompileCommand(testPath("missing.cc"))->CommandLine, + Contains("-DA=3")); +} + +TEST_F(OverlayCDBTest, GetFallbackCommand) { + OverlayCDB CDB(Base.get(), {"-DA=4"}); + EXPECT_THAT(CDB.getFallbackCommand(testPath("bar.cc")).CommandLine, + ElementsAre("clang", "-DA=2", testPath("bar.cc"), "-DA=4")); +} + +TEST_F(OverlayCDBTest, NoBase) { + OverlayCDB CDB(nullptr, {"-DA=6"}, std::string("")); + EXPECT_EQ(CDB.getCompileCommand(testPath("bar.cc")), None); + auto Override = cmd(testPath("bar.cc"), "-DA=5"); + CDB.setCompileCommand(testPath("bar.cc"), Override); + EXPECT_THAT(CDB.getCompileCommand(testPath("bar.cc"))->CommandLine, + Contains("-DA=5")); + + EXPECT_THAT(CDB.getFallbackCommand(testPath("foo.cc")).CommandLine, + ElementsAre(EndsWith("clang"), testPath("foo.cc"), "-DA=6")); +} + +TEST_F(OverlayCDBTest, Watch) { + OverlayCDB Inner(nullptr); + OverlayCDB Outer(&Inner); + + std::vector<std::vector<std::string>> Changes; + auto Sub = Outer.watch([&](const std::vector<std::string> &ChangedFiles) { + Changes.push_back(ChangedFiles); + }); + + Inner.setCompileCommand("A.cpp", tooling::CompileCommand()); + Outer.setCompileCommand("B.cpp", tooling::CompileCommand()); + Inner.setCompileCommand("A.cpp", llvm::None); + Outer.setCompileCommand("C.cpp", llvm::None); + EXPECT_THAT(Changes, ElementsAre(ElementsAre("A.cpp"), ElementsAre("B.cpp"), + ElementsAre("A.cpp"), ElementsAre("C.cpp"))); +} + +TEST_F(OverlayCDBTest, Adjustments) { + OverlayCDB CDB(Base.get(), {}, std::string("")); + auto Cmd = CDB.getCompileCommand(testPath("foo.cc")).getValue(); + // Delete the file name. + Cmd.CommandLine.pop_back(); + + // Check dependency file commands are dropped. + Cmd.CommandLine.push_back("-MF"); + Cmd.CommandLine.push_back("random-dependency"); + + // Check plugin-related commands are dropped. + Cmd.CommandLine.push_back("-Xclang"); + Cmd.CommandLine.push_back("-load"); + Cmd.CommandLine.push_back("-Xclang"); + Cmd.CommandLine.push_back("random-plugin"); + + Cmd.CommandLine.push_back("-DA=5"); + Cmd.CommandLine.push_back(Cmd.Filename); + + CDB.setCompileCommand(testPath("foo.cc"), Cmd); + + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc"))->CommandLine, + AllOf(Contains("-fsyntax-only"), Contains("-DA=5"), + Contains(testPath("foo.cc")), Not(Contains("-MF")), + Not(Contains("random-dependency")), + Not(Contains("-Xclang")), Not(Contains("-load")), + Not(Contains("random-plugin")))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/HeadersTests.cpp b/clangd/unittests/HeadersTests.cpp new file mode 100644 index 00000000..e1591abb --- /dev/null +++ b/clangd/unittests/HeadersTests.cpp @@ -0,0 +1,279 @@ +//===-- HeadersTests.cpp - Include headers unit tests -----------*- 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 "Headers.h" + +#include "Compiler.h" +#include "TestFS.h" +#include "TestTU.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/FrontendActions.h" +#include "clang/Lex/PreprocessorOptions.h" +#include "llvm/Support/Path.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::UnorderedElementsAre; + +class HeadersTest : public ::testing::Test { +public: + HeadersTest() { + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + FS.Files[MainFile] = ""; + // Make sure directory sub/ exists. + FS.Files[testPath("sub/EMPTY")] = ""; + } + +private: + std::unique_ptr<CompilerInstance> setupClang() { + auto Cmd = CDB.getCompileCommand(MainFile); + assert(static_cast<bool>(Cmd)); + auto VFS = FS.getFileSystem(); + VFS->setCurrentWorkingDirectory(Cmd->Directory); + + ParseInputs PI; + PI.CompileCommand = *Cmd; + PI.FS = VFS; + auto CI = buildCompilerInvocation(PI); + EXPECT_TRUE(static_cast<bool>(CI)); + // The diagnostic options must be set before creating a CompilerInstance. + CI->getDiagnosticOpts().IgnoreWarnings = true; + auto Clang = prepareCompilerInstance( + std::move(CI), /*Preamble=*/nullptr, + llvm::MemoryBuffer::getMemBuffer(FS.Files[MainFile], MainFile), VFS, + IgnoreDiags); + + EXPECT_FALSE(Clang->getFrontendOpts().Inputs.empty()); + return Clang; + } + +protected: + IncludeStructure collectIncludes() { + auto Clang = setupClang(); + PreprocessOnlyAction Action; + EXPECT_TRUE( + Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])); + IncludeStructure Includes; + Clang->getPreprocessor().addPPCallbacks( + collectIncludeStructureCallback(Clang->getSourceManager(), &Includes)); + EXPECT_TRUE(Action.Execute()); + Action.EndSourceFile(); + return Includes; + } + + // Calculates the include path, or returns "" on error or header should not be + // inserted. + std::string calculate(PathRef Original, PathRef Preferred = "", + const std::vector<Inclusion> &Inclusions = {}) { + auto Clang = setupClang(); + PreprocessOnlyAction Action; + EXPECT_TRUE( + Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])); + + if (Preferred.empty()) + Preferred = Original; + auto ToHeaderFile = [](llvm::StringRef Header) { + return HeaderFile{Header, + /*Verbatim=*/!llvm::sys::path::is_absolute(Header)}; + }; + + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + CDB.getCompileCommand(MainFile)->Directory, + &Clang->getPreprocessor().getHeaderSearchInfo()); + for (const auto &Inc : Inclusions) + Inserter.addExisting(Inc); + auto Inserted = ToHeaderFile(Preferred); + if (!Inserter.shouldInsertInclude(Original, Inserted)) + return ""; + std::string Path = Inserter.calculateIncludePath(Inserted); + Action.EndSourceFile(); + return Path; + } + + llvm::Optional<TextEdit> insert(llvm::StringRef VerbatimHeader) { + auto Clang = setupClang(); + PreprocessOnlyAction Action; + EXPECT_TRUE( + Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])); + + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + CDB.getCompileCommand(MainFile)->Directory, + &Clang->getPreprocessor().getHeaderSearchInfo()); + auto Edit = Inserter.insert(VerbatimHeader); + Action.EndSourceFile(); + return Edit; + } + + MockFSProvider FS; + MockCompilationDatabase CDB; + std::string MainFile = testPath("main.cpp"); + std::string Subdir = testPath("sub"); + std::string SearchDirArg = (llvm::Twine("-I") + Subdir).str(); + IgnoringDiagConsumer IgnoreDiags; +}; + +MATCHER_P(Written, Name, "") { return arg.Written == Name; } +MATCHER_P(Resolved, Name, "") { return arg.Resolved == Name; } +MATCHER_P(IncludeLine, N, "") { return arg.R.start.line == N; } + +MATCHER_P2(Distance, File, D, "") { + if (arg.getKey() != File) + *result_listener << "file =" << arg.getKey().str(); + if (arg.getValue() != D) + *result_listener << "distance =" << arg.getValue(); + return arg.getKey() == File && arg.getValue() == D; +} + +TEST_F(HeadersTest, CollectRewrittenAndResolved) { + FS.Files[MainFile] = R"cpp( +#include "sub/bar.h" // not shortest +)cpp"; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = ""; + + EXPECT_THAT(collectIncludes().MainFileIncludes, + UnorderedElementsAre( + AllOf(Written("\"sub/bar.h\""), Resolved(BarHeader)))); + EXPECT_THAT(collectIncludes().includeDepth(MainFile), + UnorderedElementsAre(Distance(MainFile, 0u), + Distance(testPath("sub/bar.h"), 1u))); +} + +TEST_F(HeadersTest, OnlyCollectInclusionsInMain) { + std::string BazHeader = testPath("sub/baz.h"); + FS.Files[BazHeader] = ""; + std::string BarHeader = testPath("sub/bar.h"); + FS.Files[BarHeader] = R"cpp( +#include "baz.h" +)cpp"; + FS.Files[MainFile] = R"cpp( +#include "bar.h" +)cpp"; + EXPECT_THAT( + collectIncludes().MainFileIncludes, + UnorderedElementsAre(AllOf(Written("\"bar.h\""), Resolved(BarHeader)))); + EXPECT_THAT(collectIncludes().includeDepth(MainFile), + UnorderedElementsAre(Distance(MainFile, 0u), + Distance(testPath("sub/bar.h"), 1u), + Distance(testPath("sub/baz.h"), 2u))); + // includeDepth() also works for non-main files. + EXPECT_THAT(collectIncludes().includeDepth(testPath("sub/bar.h")), + UnorderedElementsAre(Distance(testPath("sub/bar.h"), 0u), + Distance(testPath("sub/baz.h"), 1u))); +} + +TEST_F(HeadersTest, PreambleIncludesPresentOnce) { + // We use TestTU here, to ensure we use the preamble replay logic. + // We're testing that the logic doesn't crash, and doesn't result in duplicate + // includes. (We'd test more directly, but it's pretty well encapsulated!) + auto TU = TestTU::withCode(R"cpp( + #include "a.h" + #include "a.h" + void foo(); + #include "a.h" + )cpp"); + TU.HeaderFilename = "a.h"; // suppress "not found". + EXPECT_THAT(TU.build().getIncludeStructure().MainFileIncludes, + ElementsAre(IncludeLine(1), IncludeLine(2), IncludeLine(4))); +} + +TEST_F(HeadersTest, UnResolvedInclusion) { + FS.Files[MainFile] = R"cpp( +#include "foo.h" +)cpp"; + + EXPECT_THAT(collectIncludes().MainFileIncludes, + UnorderedElementsAre(AllOf(Written("\"foo.h\""), Resolved("")))); + EXPECT_THAT(collectIncludes().includeDepth(MainFile), + UnorderedElementsAre(Distance(MainFile, 0u))); +} + +TEST_F(HeadersTest, InsertInclude) { + std::string Path = testPath("sub/bar.h"); + FS.Files[Path] = ""; + EXPECT_EQ(calculate(Path), "\"bar.h\""); +} + +TEST_F(HeadersTest, DoNotInsertIfInSameFile) { + MainFile = testPath("main.h"); + EXPECT_EQ(calculate(MainFile), ""); +} + +TEST_F(HeadersTest, ShortenedInclude) { + std::string BarHeader = testPath("sub/bar.h"); + EXPECT_EQ(calculate(BarHeader), "\"bar.h\""); + + SearchDirArg = (llvm::Twine("-I") + Subdir + "/..").str(); + CDB.ExtraClangFlags = {SearchDirArg.c_str()}; + BarHeader = testPath("sub/bar.h"); + EXPECT_EQ(calculate(BarHeader), "\"sub/bar.h\""); +} + +TEST_F(HeadersTest, NotShortenedInclude) { + std::string BarHeader = + llvm::sys::path::convert_to_slash(testPath("sub-2/bar.h")); + EXPECT_EQ(calculate(BarHeader, ""), "\"" + BarHeader + "\""); +} + +TEST_F(HeadersTest, PreferredHeader) { + std::string BarHeader = testPath("sub/bar.h"); + EXPECT_EQ(calculate(BarHeader, "<bar>"), "<bar>"); + + std::string BazHeader = testPath("sub/baz.h"); + EXPECT_EQ(calculate(BarHeader, BazHeader), "\"baz.h\""); +} + +TEST_F(HeadersTest, DontInsertDuplicatePreferred) { + Inclusion Inc; + Inc.Written = "\"bar.h\""; + Inc.Resolved = ""; + EXPECT_EQ(calculate(testPath("sub/bar.h"), "\"bar.h\"", {Inc}), ""); + EXPECT_EQ(calculate("\"x.h\"", "\"bar.h\"", {Inc}), ""); +} + +TEST_F(HeadersTest, DontInsertDuplicateResolved) { + Inclusion Inc; + Inc.Written = "fake-bar.h"; + Inc.Resolved = testPath("sub/bar.h"); + EXPECT_EQ(calculate(Inc.Resolved, "", {Inc}), ""); + // Do not insert preferred. + EXPECT_EQ(calculate(Inc.Resolved, "\"BAR.h\"", {Inc}), ""); +} + +TEST_F(HeadersTest, PreferInserted) { + auto Edit = insert("<y>"); + EXPECT_TRUE(Edit.hasValue()); + EXPECT_TRUE(StringRef(Edit->newText).contains("<y>")); +} + +TEST(Headers, NoHeaderSearchInfo) { + std::string MainFile = testPath("main.cpp"); + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + /*BuildDir=*/"", /*HeaderSearchInfo=*/nullptr); + + auto HeaderPath = testPath("sub/bar.h"); + auto Inserting = HeaderFile{HeaderPath, /*Verbatim=*/false}; + auto Verbatim = HeaderFile{"<x>", /*Verbatim=*/true}; + + EXPECT_EQ(Inserter.calculateIncludePath(Inserting), "\"" + HeaderPath + "\""); + EXPECT_EQ(Inserter.shouldInsertInclude(HeaderPath, Inserting), false); + + EXPECT_EQ(Inserter.calculateIncludePath(Verbatim), "<x>"); + EXPECT_EQ(Inserter.shouldInsertInclude(HeaderPath, Verbatim), true); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/IndexActionTests.cpp b/clangd/unittests/IndexActionTests.cpp new file mode 100644 index 00000000..6adc8cc1 --- /dev/null +++ b/clangd/unittests/IndexActionTests.cpp @@ -0,0 +1,253 @@ +//===------ IndexActionTests.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 "TestFS.h" +#include "index/IndexAction.h" +#include "clang/Tooling/Tooling.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::Not; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedPointwise; + +std::string toUri(llvm::StringRef Path) { return URI::create(Path).toString(); } + +MATCHER(IsTU, "") { return arg.IsTU; } + +MATCHER_P(HasDigest, Digest, "") { return arg.Digest == Digest; } + +MATCHER_P(HasName, Name, "") { return arg.Name == Name; } + +MATCHER(HasSameURI, "") { + llvm::StringRef URI = ::testing::get<0>(arg); + const std::string &Path = ::testing::get<1>(arg); + return toUri(Path) == URI; +} + +::testing::Matcher<const IncludeGraphNode &> +IncludesAre(const std::vector<std::string> &Includes) { + return ::testing::Field(&IncludeGraphNode::DirectIncludes, + UnorderedPointwise(HasSameURI(), Includes)); +} + +void checkNodesAreInitialized(const IndexFileIn &IndexFile, + const std::vector<std::string> &Paths) { + ASSERT_TRUE(IndexFile.Sources); + EXPECT_THAT(Paths.size(), IndexFile.Sources->size()); + for (llvm::StringRef Path : Paths) { + auto URI = toUri(Path); + const auto &Node = IndexFile.Sources->lookup(URI); + // Uninitialized nodes will have an empty URI. + EXPECT_EQ(Node.URI.data(), IndexFile.Sources->find(URI)->getKeyData()); + } +} + +std::map<std::string, const IncludeGraphNode &> toMap(const IncludeGraph &IG) { + std::map<std::string, const IncludeGraphNode &> Nodes; + for (auto &I : IG) + Nodes.emplace(I.getKey(), I.getValue()); + return Nodes; +} + +class IndexActionTest : public ::testing::Test { +public: + IndexActionTest() : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem) {} + + IndexFileIn + runIndexingAction(llvm::StringRef MainFilePath, + const std::vector<std::string> &ExtraArgs = {}) { + IndexFileIn IndexFile; + llvm::IntrusiveRefCntPtr<FileManager> Files( + new FileManager(FileSystemOptions(), InMemoryFileSystem)); + + auto Action = createStaticIndexingAction( + SymbolCollector::Options(), + [&](SymbolSlab S) { IndexFile.Symbols = std::move(S); }, + [&](RefSlab R) { IndexFile.Refs = std::move(R); }, + [&](IncludeGraph IG) { IndexFile.Sources = std::move(IG); }); + + std::vector<std::string> Args = {"index_action", "-fsyntax-only", + "-xc++", "-std=c++11", + "-iquote", testRoot()}; + Args.insert(Args.end(), ExtraArgs.begin(), ExtraArgs.end()); + Args.push_back(MainFilePath); + + tooling::ToolInvocation Invocation( + Args, Action.release(), Files.get(), + std::make_shared<PCHContainerOperations>()); + + Invocation.run(); + + checkNodesAreInitialized(IndexFile, FilePaths); + return IndexFile; + } + + void addFile(llvm::StringRef Path, llvm::StringRef Content) { + InMemoryFileSystem->addFile(Path, 0, + llvm::MemoryBuffer::getMemBuffer(Content)); + FilePaths.push_back(Path); + } + +protected: + std::vector<std::string> FilePaths; + llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem; +}; + +TEST_F(IndexActionTest, CollectIncludeGraph) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = "#include \"level1.h\""; + std::string Level1HeaderPath = testPath("level1.h"); + std::string Level1HeaderCode = "#include \"level2.h\""; + std::string Level2HeaderPath = testPath("level2.h"); + std::string Level2HeaderCode = ""; + + addFile(MainFilePath, MainCode); + addFile(Level1HeaderPath, Level1HeaderCode); + addFile(Level2HeaderPath, Level2HeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT(Nodes, + UnorderedElementsAre( + Pair(toUri(MainFilePath), + AllOf(IsTU(), IncludesAre({Level1HeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(Level1HeaderPath), + AllOf(Not(IsTU()), IncludesAre({Level2HeaderPath}), + HasDigest(digest(Level1HeaderCode)))), + Pair(toUri(Level2HeaderPath), + AllOf(Not(IsTU()), IncludesAre({}), + HasDigest(digest(Level2HeaderCode)))))); +} + +TEST_F(IndexActionTest, IncludeGraphSelfInclude) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = "#include \"header.h\""; + std::string HeaderPath = testPath("header.h"); + std::string HeaderCode = R"cpp( + #ifndef _GUARD_ + #define _GUARD_ + #include "header.h" + #endif)cpp"; + + addFile(MainFilePath, MainCode); + addFile(HeaderPath, HeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT( + Nodes, + UnorderedElementsAre( + Pair(toUri(MainFilePath), AllOf(IsTU(), IncludesAre({HeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(HeaderPath), AllOf(Not(IsTU()), IncludesAre({HeaderPath}), + HasDigest(digest(HeaderCode)))))); +} + +TEST_F(IndexActionTest, IncludeGraphSkippedFile) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = R"cpp( + #include "common.h" + #include "header.h" + )cpp"; + + std::string CommonHeaderPath = testPath("common.h"); + std::string CommonHeaderCode = R"cpp( + #ifndef _GUARD_ + #define _GUARD_ + void f(); + #endif)cpp"; + + std::string HeaderPath = testPath("header.h"); + std::string HeaderCode = R"cpp( + #include "common.h" + void g();)cpp"; + + addFile(MainFilePath, MainCode); + addFile(HeaderPath, HeaderCode); + addFile(CommonHeaderPath, CommonHeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT( + Nodes, UnorderedElementsAre( + Pair(toUri(MainFilePath), + AllOf(IsTU(), IncludesAre({HeaderPath, CommonHeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(HeaderPath), + AllOf(Not(IsTU()), IncludesAre({CommonHeaderPath}), + HasDigest(digest(HeaderCode)))), + Pair(toUri(CommonHeaderPath), + AllOf(Not(IsTU()), IncludesAre({}), + HasDigest(digest(CommonHeaderCode)))))); +} + +TEST_F(IndexActionTest, IncludeGraphDynamicInclude) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = R"cpp( + #ifndef FOO + #define FOO "main.cpp" + #else + #define FOO "header.h" + #endif + + #include FOO)cpp"; + std::string HeaderPath = testPath("header.h"); + std::string HeaderCode = ""; + + addFile(MainFilePath, MainCode); + addFile(HeaderPath, HeaderCode); + + IndexFileIn IndexFile = runIndexingAction(MainFilePath); + auto Nodes = toMap(*IndexFile.Sources); + + EXPECT_THAT( + Nodes, + UnorderedElementsAre( + Pair(toUri(MainFilePath), + AllOf(IsTU(), IncludesAre({MainFilePath, HeaderPath}), + HasDigest(digest(MainCode)))), + Pair(toUri(HeaderPath), AllOf(Not(IsTU()), IncludesAre({}), + HasDigest(digest(HeaderCode)))))); +} + +TEST_F(IndexActionTest, NoWarnings) { + std::string MainFilePath = testPath("main.cpp"); + std::string MainCode = R"cpp( + void foo(int x) { + if (x = 1) // -Wparentheses + return; + if (x = 1) // -Wparentheses + return; + } + void bar() {} + )cpp"; + addFile(MainFilePath, MainCode); + // We set -ferror-limit so the warning-promoted-to-error would be fatal. + // This would cause indexing to stop (if warnings weren't disabled). + IndexFileIn IndexFile = runIndexingAction( + MainFilePath, {"-ferror-limit=1", "-Wparentheses", "-Werror"}); + ASSERT_TRUE(IndexFile.Sources); + ASSERT_NE(0u, IndexFile.Sources->size()); + EXPECT_THAT(*IndexFile.Symbols, ElementsAre(HasName("foo"), HasName("bar"))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/IndexTests.cpp b/clangd/unittests/IndexTests.cpp new file mode 100644 index 00000000..8d8c8da7 --- /dev/null +++ b/clangd/unittests/IndexTests.cpp @@ -0,0 +1,408 @@ +//===-- IndexTests.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 "TestIndex.h" +#include "TestTU.h" +#include "index/FileIndex.h" +#include "index/Index.h" +#include "index/MemIndex.h" +#include "index/Merge.h" +#include "index/Symbol.h" +#include "clang/Index/IndexSymbol.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::_; +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Pair; +using ::testing::Pointee; +using ::testing::UnorderedElementsAre; + +namespace clang { +namespace clangd { +namespace { + +MATCHER_P(Named, N, "") { return arg.Name == N; } +MATCHER_P(RefRange, Range, "") { + return std::make_tuple(arg.Location.Start.line(), arg.Location.Start.column(), + arg.Location.End.line(), arg.Location.End.column()) == + std::make_tuple(Range.start.line, Range.start.character, + Range.end.line, Range.end.character); +} +MATCHER_P(FileURI, F, "") { return StringRef(arg.Location.FileURI) == F; } + +TEST(SymbolLocation, Position) { + using Position = SymbolLocation::Position; + Position Pos; + + Pos.setLine(1); + EXPECT_EQ(1u, Pos.line()); + Pos.setColumn(2); + EXPECT_EQ(2u, Pos.column()); + EXPECT_FALSE(Pos.hasOverflow()); + + Pos.setLine(Position::MaxLine + 1); // overflow + EXPECT_TRUE(Pos.hasOverflow()); + EXPECT_EQ(Pos.line(), Position::MaxLine); + Pos.setLine(1); // reset the overflowed line. + + Pos.setColumn(Position::MaxColumn + 1); // overflow + EXPECT_TRUE(Pos.hasOverflow()); + EXPECT_EQ(Pos.column(), Position::MaxColumn); +} + +TEST(SymbolSlab, FindAndIterate) { + SymbolSlab::Builder B; + B.insert(symbol("Z")); + B.insert(symbol("Y")); + B.insert(symbol("X")); + EXPECT_EQ(nullptr, B.find(SymbolID("W"))); + for (const char *Sym : {"X", "Y", "Z"}) + EXPECT_THAT(B.find(SymbolID(Sym)), Pointee(Named(Sym))); + + SymbolSlab S = std::move(B).build(); + EXPECT_THAT(S, UnorderedElementsAre(Named("X"), Named("Y"), Named("Z"))); + EXPECT_EQ(S.end(), S.find(SymbolID("W"))); + for (const char *Sym : {"X", "Y", "Z"}) + EXPECT_THAT(*S.find(SymbolID(Sym)), Named(Sym)); +} + +TEST(SwapIndexTest, OldIndexRecycled) { + auto Token = std::make_shared<int>(); + std::weak_ptr<int> WeakToken = Token; + + SwapIndex S(llvm::make_unique<MemIndex>( + SymbolSlab(), RefSlab(), std::move(Token), /*BackingDataSize=*/0)); + EXPECT_FALSE(WeakToken.expired()); // Current MemIndex keeps it alive. + S.reset(llvm::make_unique<MemIndex>()); // Now the MemIndex is destroyed. + EXPECT_TRUE(WeakToken.expired()); // So the token is too. +} + +TEST(MemIndexTest, MemIndexDeduplicate) { + std::vector<Symbol> Symbols = {symbol("1"), symbol("2"), symbol("3"), + symbol("2") /* duplicate */}; + FuzzyFindRequest Req; + Req.Query = "2"; + Req.AnyScope = true; + MemIndex I(Symbols, RefSlab()); + EXPECT_THAT(match(I, Req), ElementsAre("2")); +} + +TEST(MemIndexTest, MemIndexLimitedNumMatches) { + auto I = MemIndex::build(generateNumSymbols(0, 100), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "5"; + Req.AnyScope = true; + Req.Limit = 3; + bool Incomplete; + auto Matches = match(*I, Req, &Incomplete); + EXPECT_TRUE(Req.Limit); + EXPECT_EQ(Matches.size(), *Req.Limit); + EXPECT_TRUE(Incomplete); +} + +TEST(MemIndexTest, FuzzyMatch) { + auto I = MemIndex::build( + generateSymbols({"LaughingOutLoud", "LionPopulation", "LittleOldLady"}), + RefSlab()); + FuzzyFindRequest Req; + Req.Query = "lol"; + Req.AnyScope = true; + Req.Limit = 2; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("LaughingOutLoud", "LittleOldLady")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithoutSpecificScope) { + auto I = + MemIndex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.AnyScope = true; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "b::y2", "y3")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithGlobalScope) { + auto I = + MemIndex::build(generateSymbols({"a::y1", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {""}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("y3")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithOneScope) { + auto I = MemIndex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y2", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2")); +} + +TEST(MemIndexTest, MatchQualifiedNamesWithMultipleScopes) { + auto I = MemIndex::build( + generateSymbols({"a::y1", "a::y2", "a::x", "b::y3", "y3"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::", "b::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1", "a::y2", "b::y3")); +} + +TEST(MemIndexTest, NoMatchNestedScopes) { + auto I = MemIndex::build(generateSymbols({"a::y1", "a::b::y2"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "y"; + Req.Scopes = {"a::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("a::y1")); +} + +TEST(MemIndexTest, IgnoreCases) { + auto I = MemIndex::build(generateSymbols({"ns::ABC", "ns::abc"}), RefSlab()); + FuzzyFindRequest Req; + Req.Query = "AB"; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(*I, Req), UnorderedElementsAre("ns::ABC", "ns::abc")); +} + +TEST(MemIndexTest, Lookup) { + auto I = MemIndex::build(generateSymbols({"ns::abc", "ns::xyz"}), RefSlab()); + EXPECT_THAT(lookup(*I, SymbolID("ns::abc")), UnorderedElementsAre("ns::abc")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::abc"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::abc", "ns::xyz")); + EXPECT_THAT(lookup(*I, {SymbolID("ns::nonono"), SymbolID("ns::xyz")}), + UnorderedElementsAre("ns::xyz")); + EXPECT_THAT(lookup(*I, SymbolID("ns::nonono")), UnorderedElementsAre()); +} + +TEST(MemIndexTest, TemplateSpecialization) { + SymbolSlab::Builder B; + + Symbol S = symbol("TempSpec"); + S.ID = SymbolID("1"); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("2"); + S.TemplateSpecializationArgs = "<int, bool>"; + S.SymInfo.Properties = static_cast<index::SymbolPropertySet>( + index::SymbolProperty::TemplateSpecialization); + B.insert(S); + + S = symbol("TempSpec"); + S.ID = SymbolID("3"); + S.TemplateSpecializationArgs = "<int, U>"; + S.SymInfo.Properties = static_cast<index::SymbolPropertySet>( + index::SymbolProperty::TemplatePartialSpecialization); + B.insert(S); + + auto I = MemIndex::build(std::move(B).build(), RefSlab()); + FuzzyFindRequest Req; + Req.AnyScope = true; + + Req.Query = "TempSpec"; + EXPECT_THAT(match(*I, Req), + UnorderedElementsAre("TempSpec", "TempSpec<int, bool>", + "TempSpec<int, U>")); + + // FIXME: Add filtering for template argument list. + Req.Query = "TempSpec<int"; + EXPECT_THAT(match(*I, Req), IsEmpty()); +} + +TEST(MergeIndexTest, Lookup) { + auto I = MemIndex::build(generateSymbols({"ns::A", "ns::B"}), RefSlab()), + J = MemIndex::build(generateSymbols({"ns::B", "ns::C"}), RefSlab()); + MergedIndex M(I.get(), J.get()); + EXPECT_THAT(lookup(M, SymbolID("ns::A")), UnorderedElementsAre("ns::A")); + EXPECT_THAT(lookup(M, SymbolID("ns::B")), UnorderedElementsAre("ns::B")); + EXPECT_THAT(lookup(M, SymbolID("ns::C")), UnorderedElementsAre("ns::C")); + EXPECT_THAT(lookup(M, {SymbolID("ns::A"), SymbolID("ns::B")}), + UnorderedElementsAre("ns::A", "ns::B")); + EXPECT_THAT(lookup(M, {SymbolID("ns::A"), SymbolID("ns::C")}), + UnorderedElementsAre("ns::A", "ns::C")); + EXPECT_THAT(lookup(M, SymbolID("ns::D")), UnorderedElementsAre()); + EXPECT_THAT(lookup(M, {}), UnorderedElementsAre()); +} + +TEST(MergeIndexTest, FuzzyFind) { + auto I = MemIndex::build(generateSymbols({"ns::A", "ns::B"}), RefSlab()), + J = MemIndex::build(generateSymbols({"ns::B", "ns::C"}), RefSlab()); + FuzzyFindRequest Req; + Req.Scopes = {"ns::"}; + EXPECT_THAT(match(MergedIndex(I.get(), J.get()), Req), + UnorderedElementsAre("ns::A", "ns::B", "ns::C")); +} + +TEST(MergeTest, Merge) { + Symbol L, R; + L.ID = R.ID = SymbolID("hello"); + L.Name = R.Name = "Foo"; // same in both + L.CanonicalDeclaration.FileURI = "file:///left.h"; // differs + R.CanonicalDeclaration.FileURI = "file:///right.h"; + L.References = 1; + R.References = 2; + L.Signature = "()"; // present in left only + R.CompletionSnippetSuffix = "{$1:0}"; // present in right only + R.Documentation = "--doc--"; + L.Origin = SymbolOrigin::Dynamic; + R.Origin = SymbolOrigin::Static; + R.Type = "expectedType"; + + Symbol M = mergeSymbol(L, R); + EXPECT_EQ(M.Name, "Foo"); + EXPECT_EQ(StringRef(M.CanonicalDeclaration.FileURI), "file:///left.h"); + EXPECT_EQ(M.References, 3u); + EXPECT_EQ(M.Signature, "()"); + EXPECT_EQ(M.CompletionSnippetSuffix, "{$1:0}"); + EXPECT_EQ(M.Documentation, "--doc--"); + EXPECT_EQ(M.Type, "expectedType"); + EXPECT_EQ(M.Origin, + SymbolOrigin::Dynamic | SymbolOrigin::Static | SymbolOrigin::Merge); +} + +TEST(MergeTest, PreferSymbolWithDefn) { + Symbol L, R; + + L.ID = R.ID = SymbolID("hello"); + L.CanonicalDeclaration.FileURI = "file:/left.h"; + R.CanonicalDeclaration.FileURI = "file:/right.h"; + L.Name = "left"; + R.Name = "right"; + + Symbol M = mergeSymbol(L, R); + EXPECT_EQ(StringRef(M.CanonicalDeclaration.FileURI), "file:/left.h"); + EXPECT_EQ(StringRef(M.Definition.FileURI), ""); + EXPECT_EQ(M.Name, "left"); + + R.Definition.FileURI = "file:/right.cpp"; // Now right will be favored. + M = mergeSymbol(L, R); + EXPECT_EQ(StringRef(M.CanonicalDeclaration.FileURI), "file:/right.h"); + EXPECT_EQ(StringRef(M.Definition.FileURI), "file:/right.cpp"); + EXPECT_EQ(M.Name, "right"); +} + +TEST(MergeTest, PreferSymbolLocationInCodegenFile) { + Symbol L, R; + + L.ID = R.ID = SymbolID("hello"); + L.CanonicalDeclaration.FileURI = "file:/x.proto.h"; + R.CanonicalDeclaration.FileURI = "file:/x.proto"; + + Symbol M = mergeSymbol(L, R); + EXPECT_EQ(StringRef(M.CanonicalDeclaration.FileURI), "file:/x.proto"); + + // Prefer L if both have codegen suffix. + L.CanonicalDeclaration.FileURI = "file:/y.proto"; + M = mergeSymbol(L, R); + EXPECT_EQ(StringRef(M.CanonicalDeclaration.FileURI), "file:/y.proto"); +} + +TEST(MergeIndexTest, Refs) { + FileIndex Dyn; + FileIndex StaticIndex; + MergedIndex Merge(&Dyn, &StaticIndex); + + const char *HeaderCode = "class Foo;"; + auto HeaderSymbols = TestTU::withHeaderCode("class Foo;").headerSymbols(); + auto Foo = findSymbol(HeaderSymbols, "Foo"); + + // Build dynamic index for test.cc. + Annotations Test1Code(R"(class $Foo[[Foo]];)"); + TestTU Test; + Test.HeaderCode = HeaderCode; + Test.Code = Test1Code.code(); + Test.Filename = "test.cc"; + auto AST = Test.build(); + Dyn.updateMain(Test.Filename, AST); + + // Build static index for test.cc. + Test.HeaderCode = HeaderCode; + Test.Code = "// static\nclass Foo {};"; + Test.Filename = "test.cc"; + auto StaticAST = Test.build(); + // Add stale refs for test.cc. + StaticIndex.updateMain(Test.Filename, StaticAST); + + // Add refs for test2.cc + Annotations Test2Code(R"(class $Foo[[Foo]] {};)"); + TestTU Test2; + Test2.HeaderCode = HeaderCode; + Test2.Code = Test2Code.code(); + Test2.Filename = "test2.cc"; + StaticAST = Test2.build(); + StaticIndex.updateMain(Test2.Filename, StaticAST); + + RefsRequest Request; + Request.IDs = {Foo.ID}; + RefSlab::Builder Results; + Merge.refs(Request, [&](const Ref &O) { Results.insert(Foo.ID, O); }); + EXPECT_THAT( + std::move(Results).build(), + ElementsAre(Pair( + _, UnorderedElementsAre(AllOf(RefRange(Test1Code.range("Foo")), + FileURI("unittest:///test.cc")), + AllOf(RefRange(Test2Code.range("Foo")), + FileURI("unittest:///test2.cc")))))); + + Request.Limit = 1; + RefSlab::Builder Results2; + Merge.refs(Request, [&](const Ref &O) { Results2.insert(Foo.ID, O); }); + EXPECT_THAT(std::move(Results2).build(), + ElementsAre(Pair( + _, ElementsAre(AnyOf(FileURI("unittest:///test.cc"), + FileURI("unittest:///test2.cc")))))); +} + +MATCHER_P2(IncludeHeaderWithRef, IncludeHeader, References, "") { + return (arg.IncludeHeader == IncludeHeader) && (arg.References == References); +} + +TEST(MergeTest, MergeIncludesOnDifferentDefinitions) { + Symbol L, R; + L.Name = "left"; + R.Name = "right"; + L.ID = R.ID = SymbolID("hello"); + L.IncludeHeaders.emplace_back("common", 1); + R.IncludeHeaders.emplace_back("common", 1); + R.IncludeHeaders.emplace_back("new", 1); + + // Both have no definition. + Symbol M = mergeSymbol(L, R); + EXPECT_THAT(M.IncludeHeaders, + UnorderedElementsAre(IncludeHeaderWithRef("common", 2u), + IncludeHeaderWithRef("new", 1u))); + + // Only merge references of the same includes but do not merge new #includes. + L.Definition.FileURI = "file:/left.h"; + M = mergeSymbol(L, R); + EXPECT_THAT(M.IncludeHeaders, + UnorderedElementsAre(IncludeHeaderWithRef("common", 2u))); + + // Definitions are the same. + R.Definition.FileURI = "file:/right.h"; + M = mergeSymbol(L, R); + EXPECT_THAT(M.IncludeHeaders, + UnorderedElementsAre(IncludeHeaderWithRef("common", 2u), + IncludeHeaderWithRef("new", 1u))); + + // Definitions are different. + R.Definition.FileURI = "file:/right.h"; + M = mergeSymbol(L, R); + EXPECT_THAT(M.IncludeHeaders, + UnorderedElementsAre(IncludeHeaderWithRef("common", 2u), + IncludeHeaderWithRef("new", 1u))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/JSONTransportTests.cpp b/clangd/unittests/JSONTransportTests.cpp new file mode 100644 index 00000000..3f71a10c --- /dev/null +++ b/clangd/unittests/JSONTransportTests.cpp @@ -0,0 +1,205 @@ +//===-- JSONTransportTests.cpp -------------------------------------------===// +// +// 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 "Protocol.h" +#include "Transport.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <cstdio> + +namespace clang { +namespace clangd { +namespace { + +// No fmemopen on windows or on versions of MacOS X earlier than 10.13, so we +// can't easily run this test. +#if !(defined(_WIN32) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && \ + __MAC_OS_X_VERSION_MIN_REQUIRED < 101300)) + +// Fixture takes care of managing the input/output buffers for the transport. +class JSONTransportTest : public ::testing::Test { + std::string InBuf, OutBuf, MirrorBuf; + llvm::raw_string_ostream Out, Mirror; + std::unique_ptr<FILE, int (*)(FILE *)> In; + +protected: + JSONTransportTest() : Out(OutBuf), Mirror(MirrorBuf), In(nullptr, nullptr) {} + + template <typename... Args> + std::unique_ptr<Transport> transport(std::string InData, bool Pretty, + JSONStreamStyle Style) { + InBuf = std::move(InData); + In = {fmemopen(&InBuf[0], InBuf.size(), "r"), &fclose}; + return newJSONTransport(In.get(), Out, &Mirror, Pretty, Style); + } + + std::string input() const { return InBuf; } + std::string output() { return Out.str(); } + std::string input_mirror() { return Mirror.str(); } +}; + +// Echo is a simple server running on a transport: +// - logs each message it gets. +// - when it gets a call, replies to it +// - when it gets a notification for method "call", makes a call on Target +// Hangs up when it gets an exit notification. +class Echo : public Transport::MessageHandler { + Transport &Target; + std::string LogBuf; + llvm::raw_string_ostream Log; + +public: + Echo(Transport &Target) : Target(Target), Log(LogBuf) {} + + std::string log() { return Log.str(); } + + bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override { + Log << "Notification " << Method << ": " << Params << "\n"; + if (Method == "call") + Target.call("echo call", std::move(Params), 42); + return Method != "exit"; + } + + bool onCall(llvm::StringRef Method, llvm::json::Value Params, + llvm::json::Value ID) override { + Log << "Call " << Method << "(" << ID << "): " << Params << "\n"; + if (Method == "err") + Target.reply( + ID, llvm::make_error<LSPError>("trouble at mill", ErrorCode(88))); + else + Target.reply(ID, std::move(Params)); + return true; + } + + bool onReply(llvm::json::Value ID, + llvm::Expected<llvm::json::Value> Params) override { + if (Params) + Log << "Reply(" << ID << "): " << *Params << "\n"; + else + Log << "Reply(" << ID + << "): error = " << llvm::toString(Params.takeError()) << "\n"; + return true; + } +}; + +std::string trim(llvm::StringRef S) { return S.trim().str(); } + +// Runs an Echo session using the standard JSON-RPC format we use in production. +TEST_F(JSONTransportTest, StandardDense) { + auto T = transport( + "Content-Length: 52\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "call", "params": 1234})" + "Content-Length: 46\r\n\r\n" + R"({"jsonrpc": "2.0", "id": 1234, "result": 5678})" + "Content-Length: 67\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"})" + "Content-Length: 73\r\n\r\n" + R"({"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}})" + "Content-Length: 68\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"})" + "Content-Length: 36\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "exit"})", + /*Pretty=*/false, JSONStreamStyle::Standard); + Echo E(*T); + auto Err = T->loop(E); + EXPECT_FALSE(bool(Err)) << toString(std::move(Err)); + + const char *WantLog = R"( +Notification call: 1234 +Reply(1234): 5678 +Call foo("abcd"): "efgh" +Reply("xyz"): error = 99: bad! +Call err("wxyz"): "boom!" +Notification exit: null + )"; + EXPECT_EQ(trim(E.log()), trim(WantLog)); + const char *WantOutput = + "Content-Length: 60\r\n\r\n" + R"({"id":42,"jsonrpc":"2.0","method":"echo call","params":1234})" + "Content-Length: 45\r\n\r\n" + R"({"id":"abcd","jsonrpc":"2.0","result":"efgh"})" + "Content-Length: 77\r\n\r\n" + R"({"error":{"code":88,"message":"trouble at mill"},"id":"wxyz","jsonrpc":"2.0"})"; + EXPECT_EQ(output(), WantOutput); + EXPECT_EQ(trim(input_mirror()), trim(input())); +} + +// Runs an Echo session using the "delimited" input and pretty-printed output +// that we use in lit tests. +TEST_F(JSONTransportTest, DelimitedPretty) { + auto T = transport(R"jsonrpc( +{"jsonrpc": "2.0", "method": "call", "params": 1234} +--- +{"jsonrpc": "2.0", "id": 1234, "result": 5678} +--- +{"jsonrpc": "2.0", "method": "foo", "id": "abcd", "params": "efgh"} +--- +{"jsonrpc": "2.0", "id": "xyz", "error": {"code": 99, "message": "bad!"}} +--- +{"jsonrpc": "2.0", "method": "err", "id": "wxyz", "params": "boom!"} +--- +{"jsonrpc": "2.0", "method": "exit"} + )jsonrpc", + /*Pretty=*/true, JSONStreamStyle::Delimited); + Echo E(*T); + auto Err = T->loop(E); + EXPECT_FALSE(bool(Err)) << toString(std::move(Err)); + + const char *WantLog = R"( +Notification call: 1234 +Reply(1234): 5678 +Call foo("abcd"): "efgh" +Reply("xyz"): error = 99: bad! +Call err("wxyz"): "boom!" +Notification exit: null + )"; + EXPECT_EQ(trim(E.log()), trim(WantLog)); + const char *WantOutput = "Content-Length: 77\r\n\r\n" + R"({ + "id": 42, + "jsonrpc": "2.0", + "method": "echo call", + "params": 1234 +})" + "Content-Length: 58\r\n\r\n" + R"({ + "id": "abcd", + "jsonrpc": "2.0", + "result": "efgh" +})" + "Content-Length: 105\r\n\r\n" + R"({ + "error": { + "code": 88, + "message": "trouble at mill" + }, + "id": "wxyz", + "jsonrpc": "2.0" +})"; + EXPECT_EQ(output(), WantOutput); + EXPECT_EQ(trim(input_mirror()), trim(input())); +} + +// IO errors such as EOF ane reported. +// The only successful return from loop() is if a handler returned false. +TEST_F(JSONTransportTest, EndOfFile) { + auto T = transport("Content-Length: 52\r\n\r\n" + R"({"jsonrpc": "2.0", "method": "call", "params": 1234})", + /*Pretty=*/false, JSONStreamStyle::Standard); + Echo E(*T); + auto Err = T->loop(E); + EXPECT_EQ(trim(E.log()), "Notification call: 1234"); + EXPECT_TRUE(bool(Err)); // Ran into EOF with no handler signalling done. + consumeError(std::move(Err)); + EXPECT_EQ(trim(input_mirror()), trim(input())); +} + +#endif + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/Matchers.h b/clangd/unittests/Matchers.h new file mode 100644 index 00000000..0946398d --- /dev/null +++ b/clangd/unittests/Matchers.h @@ -0,0 +1,199 @@ +//===-- Matchers.h ----------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// +// +// GMock matchers that aren't specific to particular tests. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_MATCHERS_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_MATCHERS_H +#include "Protocol.h" +#include "gmock/gmock.h" + +namespace clang { +namespace clangd { +using ::testing::Matcher; + +// EXPECT_IFF expects matcher if condition is true, and Not(matcher) if false. +// This is hard to write as a function, because matchers may be polymorphic. +#define EXPECT_IFF(condition, value, matcher) \ + do { \ + if (condition) \ + EXPECT_THAT(value, matcher); \ + else \ + EXPECT_THAT(value, ::testing::Not(matcher)); \ + } while (0) + +// HasSubsequence(m1, m2, ...) matches a vector containing elements that match +// m1, m2 ... in that order. +// +// SubsequenceMatcher implements this once the type of vector is known. +template <typename T> +class SubsequenceMatcher + : public ::testing::MatcherInterface<const std::vector<T> &> { + std::vector<Matcher<T>> Matchers; + +public: + SubsequenceMatcher(std::vector<Matcher<T>> M) : Matchers(M) {} + + void DescribeTo(std::ostream *OS) const override { + *OS << "Contains the subsequence ["; + const char *Sep = ""; + for (const auto &M : Matchers) { + *OS << Sep; + M.DescribeTo(OS); + Sep = ", "; + } + *OS << "]"; + } + + bool MatchAndExplain(const std::vector<T> &V, + ::testing::MatchResultListener *L) const override { + std::vector<int> Matches(Matchers.size()); + size_t I = 0; + for (size_t J = 0; I < Matchers.size() && J < V.size(); ++J) + if (Matchers[I].Matches(V[J])) + Matches[I++] = J; + if (I == Matchers.size()) // We exhausted all matchers. + return true; + if (L->IsInterested()) { + *L << "\n Matched:"; + for (size_t K = 0; K < I; ++K) { + *L << "\n\t"; + Matchers[K].DescribeTo(L->stream()); + *L << " ==> " << ::testing::PrintToString(V[Matches[K]]); + } + *L << "\n\t"; + Matchers[I].DescribeTo(L->stream()); + *L << " ==> no subsequent match"; + } + return false; + } +}; + +// PolySubsequenceMatcher implements a "polymorphic" SubsequenceMatcher. +// It captures the types of the element matchers, and can be converted to +// Matcher<vector<T>> if each matcher can be converted to Matcher<T>. +// This allows HasSubsequence() to accept polymorphic matchers like Not(). +template <typename... M> class PolySubsequenceMatcher { + std::tuple<M...> Matchers; + +public: + PolySubsequenceMatcher(M &&... Args) + : Matchers(std::make_tuple(std::forward<M>(Args)...)) {} + + template <typename T> operator Matcher<const std::vector<T> &>() const { + return ::testing::MakeMatcher(new SubsequenceMatcher<T>( + TypedMatchers<T>(llvm::index_sequence_for<M...>{}))); + } + +private: + template <typename T, size_t... I> + std::vector<Matcher<T>> TypedMatchers(llvm::index_sequence<I...>) const { + return {std::get<I>(Matchers)...}; + } +}; + +// HasSubsequence(m1, m2, ...) matches a vector containing elements that match +// m1, m2 ... in that order. +// The real implementation is in SubsequenceMatcher. +template <typename... Args> +PolySubsequenceMatcher<Args...> HasSubsequence(Args &&... M) { + return PolySubsequenceMatcher<Args...>(std::forward<Args>(M)...); +} + +// EXPECT_ERROR seems like a pretty generic name, make sure it's not defined +// already. +#ifdef EXPECT_ERROR +#error "Refusing to redefine EXPECT_ERROR" +#endif + +// Consumes llvm::Expected<T>, checks it contains an error and marks it as +// handled. +#define EXPECT_ERROR(expectedValue) \ + do { \ + auto &&ComputedValue = (expectedValue); \ + if (ComputedValue) { \ + ADD_FAILURE() << "expected an error from " << #expectedValue \ + << " but got " \ + << ::testing::PrintToString(*ComputedValue); \ + break; \ + } \ + llvm::consumeError(ComputedValue.takeError()); \ + } while (false) + +// Implements the HasValue(m) matcher for matching an Optional whose +// value matches matcher m. +template <typename InnerMatcher> class OptionalMatcher { +public: + explicit OptionalMatcher(const InnerMatcher &matcher) : matcher_(matcher) {} + + // This type conversion operator template allows Optional(m) to be + // used as a matcher for any Optional type whose value type is + // compatible with the inner matcher. + // + // The reason we do this instead of relying on + // MakePolymorphicMatcher() is that the latter is not flexible + // enough for implementing the DescribeTo() method of Optional(). + template <typename Optional> operator Matcher<Optional>() const { + return MakeMatcher(new Impl<Optional>(matcher_)); + } + +private: + // The monomorphic implementation that works for a particular optional type. + template <typename Optional> + class Impl : public ::testing::MatcherInterface<Optional> { + public: + using Value = typename std::remove_const< + typename std::remove_reference<Optional>::type>::type::value_type; + + explicit Impl(const InnerMatcher &matcher) + : matcher_(::testing::MatcherCast<const Value &>(matcher)) {} + + virtual void DescribeTo(::std::ostream *os) const { + *os << "has a value that "; + matcher_.DescribeTo(os); + } + + virtual void DescribeNegationTo(::std::ostream *os) const { + *os << "does not have a value that "; + matcher_.DescribeTo(os); + } + + virtual bool + MatchAndExplain(Optional optional, + ::testing::MatchResultListener *listener) const { + if (!optional.hasValue()) + return false; + + *listener << "which has a value "; + return MatchPrintAndExplain(*optional, matcher_, listener); + } + + private: + const Matcher<const Value &> matcher_; + + GTEST_DISALLOW_ASSIGN_(Impl); + }; + + const InnerMatcher matcher_; + + GTEST_DISALLOW_ASSIGN_(OptionalMatcher); +}; + +// Creates a matcher that matches an Optional that has a value +// that matches inner_matcher. +template <typename InnerMatcher> +inline OptionalMatcher<InnerMatcher> +HasValue(const InnerMatcher &inner_matcher) { + return OptionalMatcher<InnerMatcher>(inner_matcher); +} + +} // namespace clangd +} // namespace clang +#endif diff --git a/clangd/unittests/PrintASTTests.cpp b/clangd/unittests/PrintASTTests.cpp new file mode 100644 index 00000000..9cf2e7d3 --- /dev/null +++ b/clangd/unittests/PrintASTTests.cpp @@ -0,0 +1,102 @@ +//===--- PrintASTTests.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 "AST.h" +#include "Annotations.h" +#include "Protocol.h" +#include "SourceCode.h" +#include "TestTU.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "gmock/gmock.h" +#include "gtest/gtest-param-test.h" +#include "gtest/gtest.h" +#include "gtest/internal/gtest-param-util-generated.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::ElementsAreArray; + +struct Case { + const char *AnnotatedCode; + std::vector<const char *> Expected; +}; +class ASTUtils : public ::testing::Test, + public ::testing::WithParamInterface<Case> {}; + +TEST_P(ASTUtils, PrintTemplateArgs) { + auto Pair = GetParam(); + Annotations Test(Pair.AnnotatedCode); + auto AST = TestTU::withCode(Test.code()).build(); + struct Visitor : RecursiveASTVisitor<Visitor> { + Visitor(std::vector<Position> Points) : Points(std::move(Points)) {} + bool VisitNamedDecl(const NamedDecl *ND) { + if (TemplateArgsAtPoints.size() == Points.size()) + return true; + auto Pos = sourceLocToPosition(ND->getASTContext().getSourceManager(), + ND->getLocation()); + if (Pos != Points[TemplateArgsAtPoints.size()]) + return true; + TemplateArgsAtPoints.push_back(printTemplateSpecializationArgs(*ND)); + return true; + } + std::vector<std::string> TemplateArgsAtPoints; + const std::vector<Position> Points; + }; + Visitor V(Test.points()); + V.TraverseDecl(AST.getASTContext().getTranslationUnitDecl()); + EXPECT_THAT(V.TemplateArgsAtPoints, ElementsAreArray(Pair.Expected)); +} + +INSTANTIATE_TEST_CASE_P(ASTUtilsTests, ASTUtils, + ::testing::ValuesIn(std::vector<Case>({ + { + R"cpp( + template <class X> class Bar {}; + template <> class ^Bar<double> {};)cpp", + {"<double>"}}, + { + R"cpp( + template <class X> class Bar {}; + template <class T, class U, + template<typename> class Z, int Q> + struct Foo {}; + template struct ^Foo<int, bool, Bar, 8>; + template <typename T> + struct ^Foo<T *, T, Bar, 3> {};)cpp", + {"<int, bool, Bar, 8>", "<T *, T, Bar, 3>"}}, + { + R"cpp( + template <int ...> void Foz() {}; + template <> void ^Foz<3, 5, 8>() {};)cpp", + {"<3, 5, 8>"}}, + { + R"cpp( + template <class X> class Bar {}; + template <template <class> class ...> + class Aux {}; + template <> class ^Aux<Bar, Bar> {}; + template <template <class> T> + class ^Aux<T, T> {};)cpp", + {"<Bar, Bar>", "<T, T>"}}, + { + R"cpp( + template <typename T> T var = 1234; + template <> int ^var<int> = 1;)cpp", + {"<int>"}}, + { + R"cpp( + template <typename T> struct Foo; + struct Bar { friend class Foo<int>; }; + template <> struct ^Foo<int> {};)cpp", + {"<int>"}}, + }))); +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/QualityTests.cpp b/clangd/unittests/QualityTests.cpp new file mode 100644 index 00000000..0c739d7d --- /dev/null +++ b/clangd/unittests/QualityTests.cpp @@ -0,0 +1,493 @@ +//===-- QualityTests.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 +// +//===----------------------------------------------------------------------===// +// +// Evaluating scoring functions isn't a great fit for assert-based tests. +// For interesting cases, both exact scores and "X beats Y" are too brittle to +// make good hard assertions. +// +// Here we test the signal extraction and sanity-check that signals point in +// the right direction. This should be supplemented by quality metrics which +// we can compute from a corpus of queries and preferred rankings. +// +//===----------------------------------------------------------------------===// + +#include "FileDistance.h" +#include "Quality.h" +#include "TestFS.h" +#include "TestTU.h" +#include "clang/AST/Decl.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/Type.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "llvm/Support/Casting.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <vector> + +namespace clang { +namespace clangd { + +// Force the unittest URI scheme to be linked, +static int LLVM_ATTRIBUTE_UNUSED UnittestSchemeAnchorDest = + UnittestSchemeAnchorSource; + +namespace { + +TEST(QualityTests, SymbolQualitySignalExtraction) { + auto Header = TestTU::withHeaderCode(R"cpp( + int _X; + + [[deprecated]] + int _f() { return _X; } + + #define DECL_NAME(x, y) x##_##y##_Decl + #define DECL(x, y) class DECL_NAME(x, y) {}; + DECL(X, Y); // X_Y_Decl + )cpp"); + + auto Symbols = Header.headerSymbols(); + auto AST = Header.build(); + + SymbolQualitySignals Quality; + Quality.merge(findSymbol(Symbols, "_X")); + EXPECT_FALSE(Quality.Deprecated); + EXPECT_FALSE(Quality.ImplementationDetail); + EXPECT_TRUE(Quality.ReservedName); + EXPECT_EQ(Quality.References, SymbolQualitySignals().References); + EXPECT_EQ(Quality.Category, SymbolQualitySignals::Variable); + + Quality.merge(findSymbol(Symbols, "X_Y_Decl")); + EXPECT_TRUE(Quality.ImplementationDetail); + + Symbol F = findSymbol(Symbols, "_f"); + F.References = 24; // TestTU doesn't count references, so fake it. + Quality = {}; + Quality.merge(F); + EXPECT_TRUE(Quality.Deprecated); + EXPECT_FALSE(Quality.ReservedName); + EXPECT_EQ(Quality.References, 24u); + EXPECT_EQ(Quality.Category, SymbolQualitySignals::Function); + + Quality = {}; + Quality.merge(CodeCompletionResult(&findDecl(AST, "_f"), /*Priority=*/42)); + EXPECT_TRUE(Quality.Deprecated); + EXPECT_FALSE(Quality.ReservedName); + EXPECT_EQ(Quality.References, SymbolQualitySignals().References); + EXPECT_EQ(Quality.Category, SymbolQualitySignals::Function); + + Quality = {}; + Quality.merge(CodeCompletionResult("if")); + EXPECT_EQ(Quality.Category, SymbolQualitySignals::Keyword); +} + +TEST(QualityTests, SymbolRelevanceSignalExtraction) { + TestTU Test; + Test.HeaderCode = R"cpp( + int header(); + int header_main(); + + namespace hdr { class Bar {}; } // namespace hdr + + #define DEFINE_FLAG(X) \ + namespace flags { \ + int FLAGS_##X; \ + } \ + + DEFINE_FLAG(FOO) + )cpp"; + Test.Code = R"cpp( + using hdr::Bar; + + using flags::FLAGS_FOO; + + int ::header_main() {} + int main(); + + [[deprecated]] + int deprecated() { return 0; } + + namespace { struct X { void y() { int z; } }; } + struct S{} + )cpp"; + auto AST = Test.build(); + + SymbolRelevanceSignals Relevance; + Relevance.merge(CodeCompletionResult(&findDecl(AST, "deprecated"), + /*Priority=*/42, nullptr, false, + /*Accessible=*/false)); + EXPECT_EQ(Relevance.NameMatch, SymbolRelevanceSignals().NameMatch); + EXPECT_TRUE(Relevance.Forbidden); + EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::GlobalScope); + + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findDecl(AST, "main"), 42)); + EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f) + << "Decl in current file"; + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findDecl(AST, "header"), 42)); + EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 0.6f) << "Decl from header"; + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findDecl(AST, "header_main"), 42)); + EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f) + << "Current file and header"; + + auto constructShadowDeclCompletionResult = [&](const std::string DeclName) { + auto *Shadow = + *dyn_cast<UsingDecl>(&findDecl(AST, [&](const NamedDecl &ND) { + if (const UsingDecl *Using = dyn_cast<UsingDecl>(&ND)) + if (Using->shadow_size() && + Using->getQualifiedNameAsString() == DeclName) + return true; + return false; + }))->shadow_begin(); + CodeCompletionResult Result(Shadow->getTargetDecl(), 42); + Result.ShadowDecl = Shadow; + return Result; + }; + + Relevance = {}; + Relevance.merge(constructShadowDeclCompletionResult("Bar")); + EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f) + << "Using declaration in main file"; + Relevance.merge(constructShadowDeclCompletionResult("FLAGS_FOO")); + EXPECT_FLOAT_EQ(Relevance.SemaFileProximityScore, 1.0f) + << "Using declaration in main file"; + + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findUnqualifiedDecl(AST, "X"), 42)); + EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::FileScope); + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findUnqualifiedDecl(AST, "y"), 42)); + EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::ClassScope); + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findUnqualifiedDecl(AST, "z"), 42)); + EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::FunctionScope); + // The injected class name is treated as the outer class name. + Relevance = {}; + Relevance.merge(CodeCompletionResult(&findDecl(AST, "S::S"), 42)); + EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::GlobalScope); + + Relevance = {}; + EXPECT_FALSE(Relevance.InBaseClass); + auto BaseMember = CodeCompletionResult(&findUnqualifiedDecl(AST, "y"), 42); + BaseMember.InBaseClass = true; + Relevance.merge(BaseMember); + EXPECT_TRUE(Relevance.InBaseClass); + + auto Index = Test.index(); + FuzzyFindRequest Req; + Req.Query = "X"; + Req.AnyScope = true; + bool Matched = false; + Index->fuzzyFind(Req, [&](const Symbol &S) { + Matched = true; + Relevance = {}; + Relevance.merge(S); + EXPECT_EQ(Relevance.Scope, SymbolRelevanceSignals::FileScope); + }); + EXPECT_TRUE(Matched); +} + +// Do the signals move the scores in the direction we expect? +TEST(QualityTests, SymbolQualitySignalsSanity) { + SymbolQualitySignals Default; + EXPECT_EQ(Default.evaluate(), 1); + + SymbolQualitySignals Deprecated; + Deprecated.Deprecated = true; + EXPECT_LT(Deprecated.evaluate(), Default.evaluate()); + + SymbolQualitySignals ReservedName; + ReservedName.ReservedName = true; + EXPECT_LT(ReservedName.evaluate(), Default.evaluate()); + + SymbolQualitySignals ImplementationDetail; + ImplementationDetail.ImplementationDetail = true; + EXPECT_LT(ImplementationDetail.evaluate(), Default.evaluate()); + + SymbolQualitySignals WithReferences, ManyReferences; + WithReferences.References = 20; + ManyReferences.References = 1000; + EXPECT_GT(WithReferences.evaluate(), Default.evaluate()); + EXPECT_GT(ManyReferences.evaluate(), WithReferences.evaluate()); + + SymbolQualitySignals Keyword, Variable, Macro, Constructor, Function, + Destructor, Operator; + Keyword.Category = SymbolQualitySignals::Keyword; + Variable.Category = SymbolQualitySignals::Variable; + Macro.Category = SymbolQualitySignals::Macro; + Constructor.Category = SymbolQualitySignals::Constructor; + Destructor.Category = SymbolQualitySignals::Destructor; + Destructor.Category = SymbolQualitySignals::Destructor; + Operator.Category = SymbolQualitySignals::Operator; + Function.Category = SymbolQualitySignals::Function; + EXPECT_GT(Variable.evaluate(), Default.evaluate()); + EXPECT_GT(Keyword.evaluate(), Variable.evaluate()); + EXPECT_LT(Macro.evaluate(), Default.evaluate()); + EXPECT_LT(Operator.evaluate(), Default.evaluate()); + EXPECT_LT(Constructor.evaluate(), Function.evaluate()); + EXPECT_LT(Destructor.evaluate(), Constructor.evaluate()); +} + +TEST(QualityTests, SymbolRelevanceSignalsSanity) { + SymbolRelevanceSignals Default; + EXPECT_EQ(Default.evaluate(), 1); + + SymbolRelevanceSignals Forbidden; + Forbidden.Forbidden = true; + EXPECT_LT(Forbidden.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals PoorNameMatch; + PoorNameMatch.NameMatch = 0.2f; + EXPECT_LT(PoorNameMatch.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals WithSemaFileProximity; + WithSemaFileProximity.SemaFileProximityScore = 0.2f; + EXPECT_GT(WithSemaFileProximity.evaluate(), Default.evaluate()); + + ScopeDistance ScopeProximity({"x::y::"}); + + SymbolRelevanceSignals WithSemaScopeProximity; + WithSemaScopeProximity.ScopeProximityMatch = &ScopeProximity; + WithSemaScopeProximity.SemaSaysInScope = true; + EXPECT_GT(WithSemaScopeProximity.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals WithIndexScopeProximity; + WithIndexScopeProximity.ScopeProximityMatch = &ScopeProximity; + WithIndexScopeProximity.SymbolScope = "x::"; + EXPECT_GT(WithSemaScopeProximity.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals IndexProximate; + IndexProximate.SymbolURI = "unittest:/foo/bar.h"; + llvm::StringMap<SourceParams> ProxSources; + ProxSources.try_emplace(testPath("foo/baz.h")); + URIDistance Distance(ProxSources); + IndexProximate.FileProximityMatch = &Distance; + EXPECT_GT(IndexProximate.evaluate(), Default.evaluate()); + SymbolRelevanceSignals IndexDistant = IndexProximate; + IndexDistant.SymbolURI = "unittest:/elsewhere/path.h"; + EXPECT_GT(IndexProximate.evaluate(), IndexDistant.evaluate()) + << IndexProximate << IndexDistant; + EXPECT_GT(IndexDistant.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals Scoped; + Scoped.Scope = SymbolRelevanceSignals::FileScope; + EXPECT_LT(Scoped.evaluate(), Default.evaluate()); + Scoped.Query = SymbolRelevanceSignals::CodeComplete; + EXPECT_GT(Scoped.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals Instance; + Instance.IsInstanceMember = false; + EXPECT_EQ(Instance.evaluate(), Default.evaluate()); + Instance.Context = CodeCompletionContext::CCC_DotMemberAccess; + EXPECT_LT(Instance.evaluate(), Default.evaluate()); + Instance.IsInstanceMember = true; + EXPECT_EQ(Instance.evaluate(), Default.evaluate()); + + SymbolRelevanceSignals InBaseClass; + InBaseClass.InBaseClass = true; + EXPECT_LT(InBaseClass.evaluate(), Default.evaluate()); + + llvm::StringSet<> Words = {"one", "two", "three"}; + SymbolRelevanceSignals WithoutMatchingWord; + WithoutMatchingWord.ContextWords = &Words; + WithoutMatchingWord.Name = "four"; + EXPECT_EQ(WithoutMatchingWord.evaluate(), Default.evaluate()); + SymbolRelevanceSignals WithMatchingWord; + WithMatchingWord.ContextWords = &Words; + WithMatchingWord.Name = "TheTwoTowers"; + EXPECT_GT(WithMatchingWord.evaluate(), Default.evaluate()); +} + +TEST(QualityTests, ScopeProximity) { + SymbolRelevanceSignals Relevance; + ScopeDistance ScopeProximity({"x::y::z::", "x::", "llvm::", ""}); + Relevance.ScopeProximityMatch = &ScopeProximity; + + Relevance.SymbolScope = "other::"; + float NotMatched = Relevance.evaluate(); + + Relevance.SymbolScope = ""; + float Global = Relevance.evaluate(); + EXPECT_GT(Global, NotMatched); + + Relevance.SymbolScope = "llvm::"; + float NonParent = Relevance.evaluate(); + EXPECT_GT(NonParent, Global); + + Relevance.SymbolScope = "x::"; + float GrandParent = Relevance.evaluate(); + EXPECT_GT(GrandParent, Global); + + Relevance.SymbolScope = "x::y::"; + float Parent = Relevance.evaluate(); + EXPECT_GT(Parent, GrandParent); + + Relevance.SymbolScope = "x::y::z::"; + float Enclosing = Relevance.evaluate(); + EXPECT_GT(Enclosing, Parent); +} + +TEST(QualityTests, SortText) { + EXPECT_LT(sortText(std::numeric_limits<float>::infinity()), + sortText(1000.2f)); + EXPECT_LT(sortText(1000.2f), sortText(1)); + EXPECT_LT(sortText(1), sortText(0.3f)); + EXPECT_LT(sortText(0.3f), sortText(0)); + EXPECT_LT(sortText(0), sortText(-10)); + EXPECT_LT(sortText(-10), sortText(-std::numeric_limits<float>::infinity())); + + EXPECT_LT(sortText(1, "z"), sortText(0, "a")); + EXPECT_LT(sortText(0, "a"), sortText(0, "z")); +} + +TEST(QualityTests, NoBoostForClassConstructor) { + auto Header = TestTU::withHeaderCode(R"cpp( + class Foo { + public: + Foo(int); + }; + )cpp"); + auto Symbols = Header.headerSymbols(); + auto AST = Header.build(); + + const NamedDecl *Foo = &findDecl(AST, "Foo"); + SymbolRelevanceSignals Cls; + Cls.merge(CodeCompletionResult(Foo, /*Priority=*/0)); + + const NamedDecl *CtorDecl = &findDecl(AST, [](const NamedDecl &ND) { + return (ND.getQualifiedNameAsString() == "Foo::Foo") && + isa<CXXConstructorDecl>(&ND); + }); + SymbolRelevanceSignals Ctor; + Ctor.merge(CodeCompletionResult(CtorDecl, /*Priority=*/0)); + + EXPECT_EQ(Cls.Scope, SymbolRelevanceSignals::GlobalScope); + EXPECT_EQ(Ctor.Scope, SymbolRelevanceSignals::GlobalScope); +} + +TEST(QualityTests, IsInstanceMember) { + auto Header = TestTU::withHeaderCode(R"cpp( + class Foo { + public: + static void foo() {} + + template <typename T> void tpl(T *t) {} + + void bar() {} + }; + )cpp"); + auto Symbols = Header.headerSymbols(); + + SymbolRelevanceSignals Rel; + const Symbol &FooSym = findSymbol(Symbols, "Foo::foo"); + Rel.merge(FooSym); + EXPECT_FALSE(Rel.IsInstanceMember); + const Symbol &BarSym = findSymbol(Symbols, "Foo::bar"); + Rel.merge(BarSym); + EXPECT_TRUE(Rel.IsInstanceMember); + + Rel.IsInstanceMember = false; + const Symbol &TplSym = findSymbol(Symbols, "Foo::tpl"); + Rel.merge(TplSym); + EXPECT_TRUE(Rel.IsInstanceMember); + + auto AST = Header.build(); + const NamedDecl *Foo = &findDecl(AST, "Foo::foo"); + const NamedDecl *Bar = &findDecl(AST, "Foo::bar"); + const NamedDecl *Tpl = &findDecl(AST, "Foo::tpl"); + + Rel.IsInstanceMember = false; + Rel.merge(CodeCompletionResult(Foo, /*Priority=*/0)); + EXPECT_FALSE(Rel.IsInstanceMember); + Rel.merge(CodeCompletionResult(Bar, /*Priority=*/0)); + EXPECT_TRUE(Rel.IsInstanceMember); + Rel.IsInstanceMember = false; + Rel.merge(CodeCompletionResult(Tpl, /*Priority=*/0)); + EXPECT_TRUE(Rel.IsInstanceMember); +} + +TEST(QualityTests, ConstructorDestructor) { + auto Header = TestTU::withHeaderCode(R"cpp( + class Foo { + public: + Foo(int); + ~Foo(); + }; + )cpp"); + auto Symbols = Header.headerSymbols(); + auto AST = Header.build(); + + const NamedDecl *CtorDecl = &findDecl(AST, [](const NamedDecl &ND) { + return (ND.getQualifiedNameAsString() == "Foo::Foo") && + isa<CXXConstructorDecl>(&ND); + }); + const NamedDecl *DtorDecl = &findDecl(AST, [](const NamedDecl &ND) { + return (ND.getQualifiedNameAsString() == "Foo::~Foo") && + isa<CXXDestructorDecl>(&ND); + }); + + SymbolQualitySignals CtorQ; + CtorQ.merge(CodeCompletionResult(CtorDecl, /*Priority=*/0)); + EXPECT_EQ(CtorQ.Category, SymbolQualitySignals::Constructor); + + CtorQ.Category = SymbolQualitySignals::Unknown; + const Symbol &CtorSym = findSymbol(Symbols, "Foo::Foo"); + CtorQ.merge(CtorSym); + EXPECT_EQ(CtorQ.Category, SymbolQualitySignals::Constructor); + + SymbolQualitySignals DtorQ; + DtorQ.merge(CodeCompletionResult(DtorDecl, /*Priority=*/0)); + EXPECT_EQ(DtorQ.Category, SymbolQualitySignals::Destructor); +} + +TEST(QualityTests, Operator) { + auto Header = TestTU::withHeaderCode(R"cpp( + class Foo { + public: + bool operator<(const Foo& f1); + }; + )cpp"); + auto AST = Header.build(); + + const NamedDecl *Operator = &findDecl(AST, [](const NamedDecl &ND) { + if (const auto *OD = dyn_cast<FunctionDecl>(&ND)) + if (OD->isOverloadedOperator()) + return true; + return false; + }); + SymbolQualitySignals Q; + Q.merge(CodeCompletionResult(Operator, /*Priority=*/0)); + EXPECT_EQ(Q.Category, SymbolQualitySignals::Operator); +} + +TEST(QualityTests, ItemWithFixItsRankedDown) { + CodeCompleteOptions Opts; + Opts.IncludeFixIts = true; + + auto Header = TestTU::withHeaderCode(R"cpp( + int x; + )cpp"); + auto AST = Header.build(); + + SymbolRelevanceSignals RelevanceWithFixIt; + RelevanceWithFixIt.merge(CodeCompletionResult(&findDecl(AST, "x"), 0, nullptr, + false, true, {FixItHint{}})); + EXPECT_TRUE(RelevanceWithFixIt.NeedsFixIts); + + SymbolRelevanceSignals RelevanceWithoutFixIt; + RelevanceWithoutFixIt.merge( + CodeCompletionResult(&findDecl(AST, "x"), 0, nullptr, false, true, {})); + EXPECT_FALSE(RelevanceWithoutFixIt.NeedsFixIts); + + EXPECT_LT(RelevanceWithFixIt.evaluate(), RelevanceWithoutFixIt.evaluate()); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/RIFFTests.cpp b/clangd/unittests/RIFFTests.cpp new file mode 100644 index 00000000..4cd54f40 --- /dev/null +++ b/clangd/unittests/RIFFTests.cpp @@ -0,0 +1,37 @@ +//===-- RIFFTests.cpp - Binary container unit tests -----------------------===// +// +// 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 "RIFF.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using ::testing::ElementsAre; + +TEST(RIFFTest, File) { + riff::File File{riff::fourCC("test"), + { + {riff::fourCC("even"), "abcd"}, + {riff::fourCC("oddd"), "abcde"}, + }}; + llvm::StringRef Serialized = llvm::StringRef("RIFF\x1e\0\0\0test" + "even\x04\0\0\0abcd" + "oddd\x05\0\0\0abcde\0", + 38); + + EXPECT_EQ(llvm::to_string(File), Serialized); + auto Parsed = riff::readFile(Serialized); + ASSERT_TRUE(bool(Parsed)) << Parsed.takeError(); + EXPECT_EQ(*Parsed, File); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SelectionTests.cpp b/clangd/unittests/SelectionTests.cpp new file mode 100644 index 00000000..ac9facca --- /dev/null +++ b/clangd/unittests/SelectionTests.cpp @@ -0,0 +1,259 @@ +//===-- SelectionTests.cpp - ----------------------------------------------===// +// +// 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 "Selection.h" +#include "SourceCode.h" +#include "TestTU.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using ::testing::UnorderedElementsAreArray; + +SelectionTree makeSelectionTree(const StringRef MarkedCode, ParsedAST &AST) { + Annotations Test(MarkedCode); + switch (Test.points().size()) { + case 1: // Point selection. + return SelectionTree(AST.getASTContext(), + cantFail(positionToOffset(Test.code(), Test.point()))); + case 2: // Range selection. + return SelectionTree( + AST.getASTContext(), + cantFail(positionToOffset(Test.code(), Test.points()[0])), + cantFail(positionToOffset(Test.code(), Test.points()[1]))); + default: + ADD_FAILURE() << "Expected 1-2 points for selection.\n" << MarkedCode; + return SelectionTree(AST.getASTContext(), 0u, 0u); + } +} + +Range nodeRange(const SelectionTree::Node *N, ParsedAST &AST) { + if (!N) + return Range{}; + SourceManager &SM = AST.getASTContext().getSourceManager(); + StringRef Buffer = SM.getBufferData(SM.getMainFileID()); + SourceRange SR = N->ASTNode.getSourceRange(); + SR.setBegin(SM.getFileLoc(SR.getBegin())); + SR.setEnd(SM.getFileLoc(SR.getEnd())); + CharSourceRange R = + Lexer::getAsCharRange(SR, SM, AST.getASTContext().getLangOpts()); + return Range{offsetToPosition(Buffer, SM.getFileOffset(R.getBegin())), + offsetToPosition(Buffer, SM.getFileOffset(R.getEnd()))}; +} + +std::string nodeKind(const SelectionTree::Node *N) { + if (!N) + return "<null>"; + return N->ASTNode.getNodeKind().asStringRef().str(); +} + +std::vector<const SelectionTree::Node *> allNodes(const SelectionTree &T) { + std::vector<const SelectionTree::Node *> Result = {T.root()}; + for (unsigned I = 0; I < Result.size(); ++I) { + const SelectionTree::Node *N = Result[I]; + Result.insert(Result.end(), N->Children.begin(), N->Children.end()); + } + return Result; +} + +// Returns true if Common is a descendent of Root. +// Verifies nothing is selected above Common. +bool verifyCommonAncestor(const SelectionTree::Node *Root, + const SelectionTree::Node *Common, + StringRef MarkedCode) { + if (Root == Common) + return true; + if (Root->Selected) + ADD_FAILURE() << "Selected nodes outside common ancestor\n" << MarkedCode; + bool Seen = false; + for (const SelectionTree::Node *Child : Root->Children) + if (verifyCommonAncestor(Child, Common, MarkedCode)) { + if (Seen) + ADD_FAILURE() << "Saw common ancestor twice\n" << MarkedCode; + Seen = true; + } + return Seen; +} + +TEST(SelectionTest, CommonAncestor) { + struct Case { + // Selection is between ^marks^. + // common ancestor marked with a [[range]]. + const char *Code; + const char *CommonAncestorKind; + }; + Case Cases[] = { + { + R"cpp( + struct AAA { struct BBB { static int ccc(); };}; + int x = AAA::[[B^B^B]]::ccc(); + )cpp", + "TypeLoc", + }, + { + R"cpp( + struct AAA { struct BBB { static int ccc(); };}; + int x = AAA::[[B^BB^]]::ccc(); + )cpp", + "TypeLoc", + }, + { + R"cpp( + struct AAA { struct BBB { static int ccc(); };}; + int x = [[AAA::BBB::c^c^c]](); + )cpp", + "DeclRefExpr", + }, + { + R"cpp( + struct AAA { struct BBB { static int ccc(); };}; + int x = [[AAA::BBB::cc^c(^)]]; + )cpp", + "CallExpr", + }, + + { + R"cpp( + void foo() { [[if (1^11) { return; } else {^ }]] } + )cpp", + "IfStmt", + }, + { + R"cpp( + void foo(); + #define CALL_FUNCTION(X) X() + void bar() { CALL_FUNCTION([[f^o^o]]); } + )cpp", + "DeclRefExpr", + }, + { + R"cpp( + void foo(); + #define CALL_FUNCTION(X) X() + void bar() { CALL_FUNC^TION([[fo^o]]); } + )cpp", + "DeclRefExpr", + }, + { + R"cpp( + void foo(); + #define CALL_FUNCTION(X) X() + void bar() [[{ C^ALL_FUNC^TION(foo); }]] + )cpp", + "CompoundStmt", + }, + { + R"cpp( + void foo(); + #define CALL_FUNCTION(X) X^()^ + void bar() { CALL_FUNCTION(foo); } + )cpp", + nullptr, + }, + + // Point selections. + {"void foo() { [[^foo]](); }", "DeclRefExpr"}, + {"void foo() { [[f^oo]](); }", "DeclRefExpr"}, + {"void foo() { [[fo^o]](); }", "DeclRefExpr"}, + {"void foo() { [[foo^()]]; }", "CallExpr"}, + {"void foo() { [[foo^]] (); }", "DeclRefExpr"}, + {"int bar; void foo() [[{ foo (); }]]^", "CompoundStmt"}, + {"[[^void]] foo();", "TypeLoc"}, + {"^", nullptr}, + {"void foo() { [[foo^^]] (); }", "DeclRefExpr"}, + + // FIXME: Ideally we'd get a declstmt or the VarDecl itself here. + // This doesn't happen now; the RAV doesn't traverse a node containing ;. + {"int x = 42;^", nullptr}, + {"int x = 42^;", nullptr}, + + // Node types that have caused problems in the past. + {"template <typename T> void foo() { [[^T]] t; }", "TypeLoc"}, + + // No crash + { + R"cpp( + template <class T> struct Foo {}; + template <[[template<class> class /*cursor here*/^U]]> + struct Foo<U<int>*> {}; + )cpp", + "TemplateTemplateParmDecl" + }, + }; + for (const Case &C : Cases) { + Annotations Test(C.Code); + auto AST = TestTU::withCode(Test.code()).build(); + auto T = makeSelectionTree(C.Code, AST); + + if (Test.ranges().empty()) { + // If no [[range]] is marked in the example, there should be no selection. + EXPECT_FALSE(T.commonAncestor()) << C.Code << "\n" << T; + EXPECT_FALSE(T.root()) << C.Code << "\n" << T; + } else { + // If there is an expected selection, both common ancestor and root + // should exist with the appropriate node types in them. + EXPECT_EQ(C.CommonAncestorKind, nodeKind(T.commonAncestor())) + << C.Code << "\n" + << T; + EXPECT_EQ("TranslationUnitDecl", nodeKind(T.root())) << C.Code; + // Convert the reported common ancestor to a range and verify it. + EXPECT_EQ(nodeRange(T.commonAncestor(), AST), Test.range()) + << C.Code << "\n" + << T; + + // Check that common ancestor is reachable on exactly one path from root, + // and no nodes outside it are selected. + EXPECT_TRUE(verifyCommonAncestor(T.root(), T.commonAncestor(), C.Code)) + << C.Code; + } + } +} + +TEST(SelectionTest, Selected) { + // Selection with ^marks^. + // Partially selected nodes marked with a [[range]]. + // Completely selected nodes marked with a $C[[range]]. + const char *Cases[] = { + R"cpp( int abc, xyz = [[^ab^c]]; )cpp", + R"cpp( int abc, xyz = [[a^bc^]]; )cpp", + R"cpp( int abc, xyz = $C[[^abc^]]; )cpp", + R"cpp( + void foo() { + [[if ([[1^11]]) $C[[{ + $C[[return]]; + }]] else [[{^ + }]]]] + } + )cpp", + R"cpp( + template <class T> + struct unique_ptr {}; + void foo(^$C[[unique_ptr<unique_ptr<$C[[int]]>>]]^ a) {} + )cpp", + }; + for (const char *C : Cases) { + Annotations Test(C); + auto AST = TestTU::withCode(Test.code()).build(); + auto T = makeSelectionTree(C, AST); + + std::vector<Range> Complete, Partial; + for (const SelectionTree::Node *N : allNodes(T)) + if (N->Selected == SelectionTree::Complete) + Complete.push_back(nodeRange(N, AST)); + else if (N->Selected == SelectionTree::Partial) + Partial.push_back(nodeRange(N, AST)); + EXPECT_THAT(Complete, UnorderedElementsAreArray(Test.ranges("C"))) << C; + EXPECT_THAT(Partial, UnorderedElementsAreArray(Test.ranges())) << C; + } +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SerializationTests.cpp b/clangd/unittests/SerializationTests.cpp new file mode 100644 index 00000000..792da770 --- /dev/null +++ b/clangd/unittests/SerializationTests.cpp @@ -0,0 +1,220 @@ +//===-- SerializationTests.cpp - Binary and YAML serialization unit tests -===// +// +// 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 "index/Index.h" +#include "index/Serialization.h" +#include "llvm/Support/SHA1.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::_; +using ::testing::AllOf; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + +namespace clang { +namespace clangd { +namespace { + +const char *YAML = R"( +--- +!Symbol +ID: 057557CEBF6E6B2D +Name: 'Foo1' +Scope: 'clang::' +SymInfo: + Kind: Function + Lang: Cpp +CanonicalDeclaration: + FileURI: file:///path/foo.h + Start: + Line: 1 + Column: 0 + End: + Line: 1 + Column: 1 +Origin: 128 +Flags: 129 +Documentation: 'Foo doc' +ReturnType: 'int' +IncludeHeaders: + - Header: 'include1' + References: 7 + - Header: 'include2' + References: 3 +... +--- +!Symbol +ID: 057557CEBF6E6B2E +Name: 'Foo2' +Scope: 'clang::' +SymInfo: + Kind: Function + Lang: Cpp +CanonicalDeclaration: + FileURI: file:///path/bar.h + Start: + Line: 1 + Column: 0 + End: + Line: 1 + Column: 1 +Flags: 2 +Signature: '-sig' +CompletionSnippetSuffix: '-snippet' +... +!Refs +ID: 057557CEBF6E6B2D +References: + - Kind: 4 + Location: + FileURI: file:///path/foo.cc + Start: + Line: 5 + Column: 3 + End: + Line: 5 + Column: 8 +)"; + +MATCHER_P(ID, I, "") { return arg.ID == cantFail(SymbolID::fromStr(I)); } +MATCHER_P(QName, Name, "") { return (arg.Scope + arg.Name).str() == Name; } +MATCHER_P2(IncludeHeaderWithRef, IncludeHeader, References, "") { + return (arg.IncludeHeader == IncludeHeader) && (arg.References == References); +} + +TEST(SerializationTest, NoCrashOnEmptyYAML) { + EXPECT_TRUE(bool(readIndexFile(""))); +} + +TEST(SerializationTest, YAMLConversions) { + auto In = readIndexFile(YAML); + EXPECT_TRUE(bool(In)) << In.takeError(); + + auto ParsedYAML = readIndexFile(YAML); + ASSERT_TRUE(bool(ParsedYAML)) << ParsedYAML.takeError(); + ASSERT_TRUE(bool(ParsedYAML->Symbols)); + EXPECT_THAT( + *ParsedYAML->Symbols, + UnorderedElementsAre(ID("057557CEBF6E6B2D"), ID("057557CEBF6E6B2E"))); + + auto Sym1 = *ParsedYAML->Symbols->find( + cantFail(SymbolID::fromStr("057557CEBF6E6B2D"))); + auto Sym2 = *ParsedYAML->Symbols->find( + cantFail(SymbolID::fromStr("057557CEBF6E6B2E"))); + + EXPECT_THAT(Sym1, QName("clang::Foo1")); + EXPECT_EQ(Sym1.Signature, ""); + EXPECT_EQ(Sym1.Documentation, "Foo doc"); + EXPECT_EQ(Sym1.ReturnType, "int"); + EXPECT_EQ(StringRef(Sym1.CanonicalDeclaration.FileURI), "file:///path/foo.h"); + EXPECT_EQ(Sym1.Origin, static_cast<SymbolOrigin>(1 << 7)); + EXPECT_EQ(static_cast<uint8_t>(Sym1.Flags), 129); + EXPECT_TRUE(Sym1.Flags & Symbol::IndexedForCodeCompletion); + EXPECT_FALSE(Sym1.Flags & Symbol::Deprecated); + EXPECT_THAT(Sym1.IncludeHeaders, + UnorderedElementsAre(IncludeHeaderWithRef("include1", 7u), + IncludeHeaderWithRef("include2", 3u))); + + EXPECT_THAT(Sym2, QName("clang::Foo2")); + EXPECT_EQ(Sym2.Signature, "-sig"); + EXPECT_EQ(Sym2.ReturnType, ""); + EXPECT_EQ(llvm::StringRef(Sym2.CanonicalDeclaration.FileURI), + "file:///path/bar.h"); + EXPECT_FALSE(Sym2.Flags & Symbol::IndexedForCodeCompletion); + EXPECT_TRUE(Sym2.Flags & Symbol::Deprecated); + + ASSERT_TRUE(bool(ParsedYAML->Refs)); + EXPECT_THAT( + *ParsedYAML->Refs, + UnorderedElementsAre(Pair(cantFail(SymbolID::fromStr("057557CEBF6E6B2D")), + ::testing::SizeIs(1)))); + auto Ref1 = ParsedYAML->Refs->begin()->second.front(); + EXPECT_EQ(Ref1.Kind, RefKind::Reference); + EXPECT_EQ(StringRef(Ref1.Location.FileURI), "file:///path/foo.cc"); +} + +std::vector<std::string> YAMLFromSymbols(const SymbolSlab &Slab) { + std::vector<std::string> Result; + for (const auto &Sym : Slab) + Result.push_back(toYAML(Sym)); + return Result; +} +std::vector<std::string> YAMLFromRefs(const RefSlab &Slab) { + std::vector<std::string> Result; + for (const auto &Sym : Slab) + Result.push_back(toYAML(Sym)); + return Result; +} + +TEST(SerializationTest, BinaryConversions) { + auto In = readIndexFile(YAML); + EXPECT_TRUE(bool(In)) << In.takeError(); + + // Write to binary format, and parse again. + IndexFileOut Out(*In); + Out.Format = IndexFileFormat::RIFF; + std::string Serialized = llvm::to_string(Out); + + auto In2 = readIndexFile(Serialized); + ASSERT_TRUE(bool(In2)) << In.takeError(); + ASSERT_TRUE(In2->Symbols); + ASSERT_TRUE(In2->Refs); + + // Assert the YAML serializations match, for nice comparisons and diffs. + EXPECT_THAT(YAMLFromSymbols(*In2->Symbols), + UnorderedElementsAreArray(YAMLFromSymbols(*In->Symbols))); + EXPECT_THAT(YAMLFromRefs(*In2->Refs), + UnorderedElementsAreArray(YAMLFromRefs(*In->Refs))); +} + +TEST(SerializationTest, SrcsTest) { + auto In = readIndexFile(YAML); + EXPECT_TRUE(bool(In)) << In.takeError(); + + std::string TestContent("TestContent"); + IncludeGraphNode IGN; + IGN.Digest = + llvm::SHA1::hash({reinterpret_cast<const uint8_t *>(TestContent.data()), + TestContent.size()}); + IGN.DirectIncludes = {"inc1", "inc2"}; + IGN.URI = "URI"; + IGN.IsTU = true; + IncludeGraph Sources; + Sources[IGN.URI] = IGN; + // Write to binary format, and parse again. + IndexFileOut Out(*In); + Out.Format = IndexFileFormat::RIFF; + Out.Sources = &Sources; + { + std::string Serialized = llvm::to_string(Out); + + auto In = readIndexFile(Serialized); + ASSERT_TRUE(bool(In)) << In.takeError(); + ASSERT_TRUE(In->Symbols); + ASSERT_TRUE(In->Refs); + ASSERT_TRUE(In->Sources); + ASSERT_TRUE(In->Sources->count(IGN.URI)); + // Assert the YAML serializations match, for nice comparisons and diffs. + EXPECT_THAT(YAMLFromSymbols(*In->Symbols), + UnorderedElementsAreArray(YAMLFromSymbols(*In->Symbols))); + EXPECT_THAT(YAMLFromRefs(*In->Refs), + UnorderedElementsAreArray(YAMLFromRefs(*In->Refs))); + auto IGNDeserialized = In->Sources->lookup(IGN.URI); + EXPECT_EQ(IGNDeserialized.Digest, IGN.Digest); + EXPECT_EQ(IGNDeserialized.DirectIncludes, IGN.DirectIncludes); + EXPECT_EQ(IGNDeserialized.URI, IGN.URI); + EXPECT_EQ(IGNDeserialized.IsTU, IGN.IsTU); + } +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SourceCodeTests.cpp b/clangd/unittests/SourceCodeTests.cpp new file mode 100644 index 00000000..9ca6fa1a --- /dev/null +++ b/clangd/unittests/SourceCodeTests.cpp @@ -0,0 +1,409 @@ +//===-- SourceCodeTests.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 "Context.h" +#include "Protocol.h" +#include "SourceCode.h" +#include "clang/Format/Format.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_os_ostream.h" +#include "llvm/Testing/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using llvm::Failed; +using llvm::HasValue; +using ::testing::UnorderedElementsAreArray; + +MATCHER_P2(Pos, Line, Col, "") { + return arg.line == int(Line) && arg.character == int(Col); +} + +/// A helper to make tests easier to read. +Position position(int line, int character) { + Position Pos; + Pos.line = line; + Pos.character = character; + return Pos; +} + +Range range(const std::pair<int, int> p1, const std::pair<int, int> p2) { + Range range; + range.start = position(p1.first, p1.second); + range.end = position(p2.first, p2.second); + return range; +} + +TEST(SourceCodeTests, lspLength) { + EXPECT_EQ(lspLength(""), 0UL); + EXPECT_EQ(lspLength("ascii"), 5UL); + // BMP + EXPECT_EQ(lspLength("↓"), 1UL); + EXPECT_EQ(lspLength("¥"), 1UL); + // astral + EXPECT_EQ(lspLength("😂"), 2UL); + + WithContextValue UTF8(kCurrentOffsetEncoding, OffsetEncoding::UTF8); + EXPECT_EQ(lspLength(""), 0UL); + EXPECT_EQ(lspLength("ascii"), 5UL); + // BMP + EXPECT_EQ(lspLength("↓"), 3UL); + EXPECT_EQ(lspLength("¥"), 2UL); + // astral + EXPECT_EQ(lspLength("😂"), 4UL); + + WithContextValue UTF32(kCurrentOffsetEncoding, OffsetEncoding::UTF32); + EXPECT_EQ(lspLength(""), 0UL); + EXPECT_EQ(lspLength("ascii"), 5UL); + // BMP + EXPECT_EQ(lspLength("↓"), 1UL); + EXPECT_EQ(lspLength("¥"), 1UL); + // astral + EXPECT_EQ(lspLength("😂"), 1UL); +} + +// The = → 🡆 below are ASCII (1 byte), BMP (3 bytes), and astral (4 bytes). +const char File[] = R"(0:0 = 0 +1:0 → 8 +2:0 🡆 18)"; +struct Line { + unsigned Number; + unsigned Offset; + unsigned Length; +}; +Line FileLines[] = {Line{0, 0, 7}, Line{1, 8, 9}, Line{2, 18, 11}}; + +TEST(SourceCodeTests, PositionToOffset) { + // line out of bounds + EXPECT_THAT_EXPECTED(positionToOffset(File, position(-1, 2)), llvm::Failed()); + // first line + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, -1)), + llvm::Failed()); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 0)), + llvm::HasValue(0)); // first character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 3)), + llvm::HasValue(3)); // middle character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 6)), + llvm::HasValue(6)); // last character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 7)), + llvm::HasValue(7)); // the newline itself + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 7), false), + llvm::HasValue(7)); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 8)), + llvm::HasValue(7)); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 8), false), + llvm::Failed()); // out of range + // middle line + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, -1)), + llvm::Failed()); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 0)), + llvm::HasValue(8)); // first character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 3)), + llvm::HasValue(11)); // middle character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 3), false), + llvm::HasValue(11)); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 6)), + llvm::HasValue(16)); // last character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 7)), + llvm::HasValue(17)); // the newline itself + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 8)), + llvm::HasValue(17)); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 8), false), + llvm::Failed()); // out of range + // last line + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, -1)), + llvm::Failed()); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 0)), + llvm::HasValue(18)); // first character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 3)), + llvm::HasValue(21)); // middle character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 5), false), + llvm::Failed()); // middle of surrogate pair + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 5)), + llvm::HasValue(26)); // middle of surrogate pair + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 6), false), + llvm::HasValue(26)); // end of surrogate pair + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 8)), + llvm::HasValue(28)); // last character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 9)), + llvm::HasValue(29)); // EOF + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 10), false), + llvm::Failed()); // out of range + // line out of bounds + EXPECT_THAT_EXPECTED(positionToOffset(File, position(3, 0)), llvm::Failed()); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(3, 1)), llvm::Failed()); + + // Codepoints are similar, except near astral characters. + WithContextValue UTF32(kCurrentOffsetEncoding, OffsetEncoding::UTF32); + // line out of bounds + EXPECT_THAT_EXPECTED(positionToOffset(File, position(-1, 2)), llvm::Failed()); + // first line + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, -1)), + llvm::Failed()); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 0)), + llvm::HasValue(0)); // first character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 3)), + llvm::HasValue(3)); // middle character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 6)), + llvm::HasValue(6)); // last character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 7)), + llvm::HasValue(7)); // the newline itself + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 7), false), + llvm::HasValue(7)); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 8)), + llvm::HasValue(7)); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(0, 8), false), + llvm::Failed()); // out of range + // middle line + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, -1)), + llvm::Failed()); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 0)), + llvm::HasValue(8)); // first character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 3)), + llvm::HasValue(11)); // middle character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 3), false), + llvm::HasValue(11)); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 6)), + llvm::HasValue(16)); // last character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 7)), + llvm::HasValue(17)); // the newline itself + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 8)), + llvm::HasValue(17)); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(1, 8), false), + llvm::Failed()); // out of range + // last line + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, -1)), + llvm::Failed()); // out of range + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 0)), + llvm::HasValue(18)); // first character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 4)), + llvm::HasValue(22)); // Before astral character. + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 5), false), + llvm::HasValue(26)); // after astral character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 7)), + llvm::HasValue(28)); // last character + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 8)), + llvm::HasValue(29)); // EOF + EXPECT_THAT_EXPECTED(positionToOffset(File, position(2, 9), false), + llvm::Failed()); // out of range + // line out of bounds + EXPECT_THAT_EXPECTED(positionToOffset(File, position(3, 0)), llvm::Failed()); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(3, 1)), llvm::Failed()); + + // Test UTF-8, where transformations are trivial. + WithContextValue UTF8(kCurrentOffsetEncoding, OffsetEncoding::UTF8); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(-1, 2)), llvm::Failed()); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(3, 0)), llvm::Failed()); + for (Line L : FileLines) { + EXPECT_THAT_EXPECTED(positionToOffset(File, position(L.Number, -1)), + llvm::Failed()); // out of range + for (unsigned I = 0; I <= L.Length; ++I) + EXPECT_THAT_EXPECTED(positionToOffset(File, position(L.Number, I)), + llvm::HasValue(L.Offset + I)); + EXPECT_THAT_EXPECTED(positionToOffset(File, position(L.Number, L.Length+1)), + llvm::HasValue(L.Offset + L.Length)); + EXPECT_THAT_EXPECTED( + positionToOffset(File, position(L.Number, L.Length + 1), false), + llvm::Failed()); // out of range + } +} + +TEST(SourceCodeTests, OffsetToPosition) { + EXPECT_THAT(offsetToPosition(File, 0), Pos(0, 0)) << "start of file"; + EXPECT_THAT(offsetToPosition(File, 3), Pos(0, 3)) << "in first line"; + EXPECT_THAT(offsetToPosition(File, 6), Pos(0, 6)) << "end of first line"; + EXPECT_THAT(offsetToPosition(File, 7), Pos(0, 7)) << "first newline"; + EXPECT_THAT(offsetToPosition(File, 8), Pos(1, 0)) << "start of second line"; + EXPECT_THAT(offsetToPosition(File, 12), Pos(1, 4)) << "before BMP char"; + EXPECT_THAT(offsetToPosition(File, 13), Pos(1, 5)) << "in BMP char"; + EXPECT_THAT(offsetToPosition(File, 15), Pos(1, 5)) << "after BMP char"; + EXPECT_THAT(offsetToPosition(File, 16), Pos(1, 6)) << "end of second line"; + EXPECT_THAT(offsetToPosition(File, 17), Pos(1, 7)) << "second newline"; + EXPECT_THAT(offsetToPosition(File, 18), Pos(2, 0)) << "start of last line"; + EXPECT_THAT(offsetToPosition(File, 21), Pos(2, 3)) << "in last line"; + EXPECT_THAT(offsetToPosition(File, 22), Pos(2, 4)) << "before astral char"; + EXPECT_THAT(offsetToPosition(File, 24), Pos(2, 6)) << "in astral char"; + EXPECT_THAT(offsetToPosition(File, 26), Pos(2, 6)) << "after astral char"; + EXPECT_THAT(offsetToPosition(File, 28), Pos(2, 8)) << "end of last line"; + EXPECT_THAT(offsetToPosition(File, 29), Pos(2, 9)) << "EOF"; + EXPECT_THAT(offsetToPosition(File, 30), Pos(2, 9)) << "out of bounds"; + + // Codepoints are similar, except near astral characters. + WithContextValue UTF32(kCurrentOffsetEncoding, OffsetEncoding::UTF32); + EXPECT_THAT(offsetToPosition(File, 0), Pos(0, 0)) << "start of file"; + EXPECT_THAT(offsetToPosition(File, 3), Pos(0, 3)) << "in first line"; + EXPECT_THAT(offsetToPosition(File, 6), Pos(0, 6)) << "end of first line"; + EXPECT_THAT(offsetToPosition(File, 7), Pos(0, 7)) << "first newline"; + EXPECT_THAT(offsetToPosition(File, 8), Pos(1, 0)) << "start of second line"; + EXPECT_THAT(offsetToPosition(File, 12), Pos(1, 4)) << "before BMP char"; + EXPECT_THAT(offsetToPosition(File, 13), Pos(1, 5)) << "in BMP char"; + EXPECT_THAT(offsetToPosition(File, 15), Pos(1, 5)) << "after BMP char"; + EXPECT_THAT(offsetToPosition(File, 16), Pos(1, 6)) << "end of second line"; + EXPECT_THAT(offsetToPosition(File, 17), Pos(1, 7)) << "second newline"; + EXPECT_THAT(offsetToPosition(File, 18), Pos(2, 0)) << "start of last line"; + EXPECT_THAT(offsetToPosition(File, 21), Pos(2, 3)) << "in last line"; + EXPECT_THAT(offsetToPosition(File, 22), Pos(2, 4)) << "before astral char"; + EXPECT_THAT(offsetToPosition(File, 24), Pos(2, 5)) << "in astral char"; + EXPECT_THAT(offsetToPosition(File, 26), Pos(2, 5)) << "after astral char"; + EXPECT_THAT(offsetToPosition(File, 28), Pos(2, 7)) << "end of last line"; + EXPECT_THAT(offsetToPosition(File, 29), Pos(2, 8)) << "EOF"; + EXPECT_THAT(offsetToPosition(File, 30), Pos(2, 8)) << "out of bounds"; + + WithContextValue UTF8(kCurrentOffsetEncoding, OffsetEncoding::UTF8); + for (Line L : FileLines) { + for (unsigned I = 0; I <= L.Length; ++I) + EXPECT_THAT(offsetToPosition(File, L.Offset + I), Pos(L.Number, I)); + } + EXPECT_THAT(offsetToPosition(File, 30), Pos(2, 11)) << "out of bounds"; +} + +TEST(SourceCodeTests, IsRangeConsecutive) { + EXPECT_TRUE(isRangeConsecutive(range({2, 2}, {2, 3}), range({2, 3}, {2, 4}))); + EXPECT_FALSE( + isRangeConsecutive(range({0, 2}, {0, 3}), range({2, 3}, {2, 4}))); + EXPECT_FALSE( + isRangeConsecutive(range({2, 2}, {2, 3}), range({2, 4}, {2, 5}))); +} + +TEST(SourceCodeTests, SourceLocationInMainFile) { + Annotations Source(R"cpp( + ^in^t ^foo + ^bar + ^baz ^() {} {} {} {} { }^ +)cpp"); + + SourceManagerForFile Owner("foo.cpp", Source.code()); + SourceManager &SM = Owner.get(); + + SourceLocation StartOfFile = SM.getLocForStartOfFile(SM.getMainFileID()); + EXPECT_THAT_EXPECTED(sourceLocationInMainFile(SM, position(0, 0)), + HasValue(StartOfFile)); + // End of file. + EXPECT_THAT_EXPECTED( + sourceLocationInMainFile(SM, position(4, 0)), + HasValue(StartOfFile.getLocWithOffset(Source.code().size()))); + // Column number is too large. + EXPECT_THAT_EXPECTED(sourceLocationInMainFile(SM, position(0, 1)), Failed()); + EXPECT_THAT_EXPECTED(sourceLocationInMainFile(SM, position(0, 100)), + Failed()); + EXPECT_THAT_EXPECTED(sourceLocationInMainFile(SM, position(4, 1)), Failed()); + // Line number is too large. + EXPECT_THAT_EXPECTED(sourceLocationInMainFile(SM, position(5, 0)), Failed()); + // Check all positions mentioned in the test return valid results. + for (auto P : Source.points()) { + size_t Offset = llvm::cantFail(positionToOffset(Source.code(), P)); + EXPECT_THAT_EXPECTED(sourceLocationInMainFile(SM, P), + HasValue(StartOfFile.getLocWithOffset(Offset))); + } +} + +TEST(SourceCodeTests, CollectIdentifiers) { + auto Style = format::getLLVMStyle(); + auto IDs = collectIdentifiers(R"cpp( + #include "a.h" + void foo() { int xyz; int abc = xyz; return foo(); } + )cpp", + Style); + EXPECT_EQ(IDs.size(), 7u); + EXPECT_EQ(IDs["include"], 1u); + EXPECT_EQ(IDs["void"], 1u); + EXPECT_EQ(IDs["int"], 2u); + EXPECT_EQ(IDs["xyz"], 2u); + EXPECT_EQ(IDs["abc"], 1u); + EXPECT_EQ(IDs["return"], 1u); + EXPECT_EQ(IDs["foo"], 2u); +} + +TEST(SourceCodeTests, CollectWords) { + auto Words = collectWords(R"cpp( + #define FIZZ_BUZZ + // this is a comment + std::string getSomeText() { return "magic word"; } + )cpp"); + std::set<std::string> ActualWords(Words.keys().begin(), Words.keys().end()); + std::set<std::string> ExpectedWords = {"define", "fizz", "buzz", "this", + "comment", "string", "some", "text", + "return", "magic", "word"}; + EXPECT_EQ(ActualWords, ExpectedWords); +} + +TEST(SourceCodeTests, VisibleNamespaces) { + std::vector<std::pair<const char *, std::vector<std::string>>> Cases = { + { + R"cpp( + // Using directive resolved against enclosing namespaces. + using namespace foo; + namespace ns { + using namespace bar; + )cpp", + {"ns", "", "bar", "foo", "ns::bar"}, + }, + { + R"cpp( + // Don't include namespaces we've closed, ignore namespace aliases. + using namespace clang; + using std::swap; + namespace clang { + namespace clangd {} + namespace ll = ::llvm; + } + namespace clang { + )cpp", + {"clang", ""}, + }, + { + R"cpp( + // Using directives visible even if a namespace is reopened. + // Ignore anonymous namespaces. + namespace foo{ using namespace bar; } + namespace foo{ namespace { + )cpp", + {"foo", "", "bar", "foo::bar"}, + }, + { + R"cpp( + // Mismatched braces + namespace foo{} + }}} + namespace bar{ + )cpp", + {"bar", ""}, + }, + { + R"cpp( + // Namespaces with multiple chunks. + namespace a::b { + using namespace c::d; + namespace e::f { + )cpp", + { + "a::b::e::f", + "", + "a", + "a::b", + "a::b::c::d", + "a::b::e", + "a::c::d", + "c::d", + }, + }, + }; + for (const auto& Case : Cases) { + EXPECT_EQ(Case.second, + visibleNamespaces(Case.first, format::getLLVMStyle())) + << Case.first; + } +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SymbolCollectorTests.cpp b/clangd/unittests/SymbolCollectorTests.cpp new file mode 100644 index 00000000..d372b1d6 --- /dev/null +++ b/clangd/unittests/SymbolCollectorTests.cpp @@ -0,0 +1,1257 @@ +//===-- SymbolCollectorTests.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 "TestFS.h" +#include "TestTU.h" +#include "index/SymbolCollector.h" +#include "clang/Basic/FileManager.h" +#include "clang/Basic/FileSystemOptions.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Index/IndexingAction.h" +#include "clang/Tooling/Tooling.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/VirtualFileSystem.h" +#include "gmock/gmock-matchers.h" +#include "gmock/gmock-more-matchers.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include <memory> +#include <string> + +namespace clang { +namespace clangd { +namespace { + +using ::testing::_; +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::Each; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::Not; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + +// GMock helpers for matching Symbol. +MATCHER_P(Labeled, Label, "") { + return (arg.Name + arg.Signature).str() == Label; +} +MATCHER_P(ReturnType, D, "") { return arg.ReturnType == D; } +MATCHER_P(Doc, D, "") { return arg.Documentation == D; } +MATCHER_P(Snippet, S, "") { + return (arg.Name + arg.CompletionSnippetSuffix).str() == S; +} +MATCHER_P(QName, Name, "") { return (arg.Scope + arg.Name).str() == Name; } +MATCHER_P(TemplateArgs, TemplArgs, "") { + return arg.TemplateSpecializationArgs == TemplArgs; +} +MATCHER_P(DeclURI, P, "") { + return StringRef(arg.CanonicalDeclaration.FileURI) == P; +} +MATCHER_P(DefURI, P, "") { return StringRef(arg.Definition.FileURI) == P; } +MATCHER(IncludeHeader, "") { return !arg.IncludeHeaders.empty(); } +MATCHER_P(IncludeHeader, P, "") { + return (arg.IncludeHeaders.size() == 1) && + (arg.IncludeHeaders.begin()->IncludeHeader == P); +} +MATCHER_P2(IncludeHeaderWithRef, IncludeHeader, References, "") { + return (arg.IncludeHeader == IncludeHeader) && (arg.References == References); +} +MATCHER_P(DeclRange, Pos, "") { + return std::make_tuple(arg.CanonicalDeclaration.Start.line(), + arg.CanonicalDeclaration.Start.column(), + arg.CanonicalDeclaration.End.line(), + arg.CanonicalDeclaration.End.column()) == + std::make_tuple(Pos.start.line, Pos.start.character, Pos.end.line, + Pos.end.character); +} +MATCHER_P(DefRange, Pos, "") { + return std::make_tuple( + arg.Definition.Start.line(), arg.Definition.Start.column(), + arg.Definition.End.line(), arg.Definition.End.column()) == + std::make_tuple(Pos.start.line, Pos.start.character, Pos.end.line, + Pos.end.character); +} +MATCHER_P(RefCount, R, "") { return int(arg.References) == R; } +MATCHER_P(ForCodeCompletion, IsIndexedForCodeCompletion, "") { + return static_cast<bool>(arg.Flags & Symbol::IndexedForCodeCompletion) == + IsIndexedForCodeCompletion; +} +MATCHER(Deprecated, "") { return arg.Flags & Symbol::Deprecated; } +MATCHER(ImplementationDetail, "") { + return arg.Flags & Symbol::ImplementationDetail; +} +MATCHER(VisibleOutsideFile, "") { + return static_cast<bool>(arg.Flags & Symbol::VisibleOutsideFile); +} +MATCHER(RefRange, "") { + const Ref &Pos = ::testing::get<0>(arg); + const Range &Range = ::testing::get<1>(arg); + return std::make_tuple(Pos.Location.Start.line(), Pos.Location.Start.column(), + Pos.Location.End.line(), Pos.Location.End.column()) == + std::make_tuple(Range.start.line, Range.start.character, + Range.end.line, Range.end.character); +} +::testing::Matcher<const std::vector<Ref> &> +HaveRanges(const std::vector<Range> Ranges) { + return ::testing::UnorderedPointwise(RefRange(), Ranges); +} + +class ShouldCollectSymbolTest : public ::testing::Test { +public: + void build(llvm::StringRef HeaderCode, llvm::StringRef Code = "") { + File.HeaderFilename = HeaderName; + File.Filename = FileName; + File.HeaderCode = HeaderCode; + File.Code = Code; + AST = File.build(); + } + + // build() must have been called. + bool shouldCollect(llvm::StringRef Name, bool Qualified = true) { + assert(AST.hasValue()); + const NamedDecl &ND = + Qualified ? findDecl(*AST, Name) : findUnqualifiedDecl(*AST, Name); + ASTContext& Ctx = AST->getASTContext(); + const SourceManager& SM = Ctx.getSourceManager(); + bool MainFile = SM.isWrittenInMainFile(SM.getExpansionLoc(ND.getBeginLoc())); + return SymbolCollector::shouldCollectSymbol( + ND, Ctx, SymbolCollector::Options(), MainFile); + } + +protected: + std::string HeaderName = "f.h"; + std::string FileName = "f.cpp"; + TestTU File; + llvm::Optional<ParsedAST> AST; // Initialized after build. +}; + +TEST_F(ShouldCollectSymbolTest, ShouldCollectSymbol) { + build(R"( + namespace nx { + class X{}; + auto f() { int Local; } // auto ensures function body is parsed. + struct { int x; } var; + } + )", + R"( + class InMain {}; + namespace { class InAnonymous {}; } + static void g(); + )"); + auto AST = File.build(); + EXPECT_TRUE(shouldCollect("nx")); + EXPECT_TRUE(shouldCollect("nx::X")); + EXPECT_TRUE(shouldCollect("nx::f")); + EXPECT_TRUE(shouldCollect("InMain")); + EXPECT_TRUE(shouldCollect("InAnonymous", /*Qualified=*/false)); + EXPECT_TRUE(shouldCollect("g")); + + EXPECT_FALSE(shouldCollect("Local", /*Qualified=*/false)); +} + +TEST_F(ShouldCollectSymbolTest, NoPrivateProtoSymbol) { + HeaderName = "f.proto.h"; + build( + R"(// Generated by the protocol buffer compiler. DO NOT EDIT! + namespace nx { + class Top_Level {}; + class TopLevel {}; + enum Kind { + KIND_OK, + Kind_Not_Ok, + }; + })"); + EXPECT_TRUE(shouldCollect("nx::TopLevel")); + EXPECT_TRUE(shouldCollect("nx::Kind::KIND_OK")); + EXPECT_TRUE(shouldCollect("nx::Kind")); + + EXPECT_FALSE(shouldCollect("nx::Top_Level")); + EXPECT_FALSE(shouldCollect("nx::Kind::Kind_Not_Ok")); +} + +TEST_F(ShouldCollectSymbolTest, DoubleCheckProtoHeaderComment) { + HeaderName = "f.proto.h"; + build(R"( + namespace nx { + class Top_Level {}; + enum Kind { + Kind_Fine + }; + } + )"); + EXPECT_TRUE(shouldCollect("nx::Top_Level")); + EXPECT_TRUE(shouldCollect("nx::Kind_Fine")); +} + +class SymbolIndexActionFactory : public tooling::FrontendActionFactory { +public: + SymbolIndexActionFactory(SymbolCollector::Options COpts, + CommentHandler *PragmaHandler) + : COpts(std::move(COpts)), PragmaHandler(PragmaHandler) {} + + clang::FrontendAction *create() override { + class WrappedIndexAction : public WrapperFrontendAction { + public: + WrappedIndexAction(std::shared_ptr<SymbolCollector> C, + const index::IndexingOptions &Opts, + CommentHandler *PragmaHandler) + : WrapperFrontendAction( + index::createIndexingAction(C, Opts, nullptr)), + PragmaHandler(PragmaHandler) {} + + std::unique_ptr<ASTConsumer> + CreateASTConsumer(CompilerInstance &CI, llvm::StringRef InFile) override { + if (PragmaHandler) + CI.getPreprocessor().addCommentHandler(PragmaHandler); + return WrapperFrontendAction::CreateASTConsumer(CI, InFile); + } + + bool BeginInvocation(CompilerInstance &CI) override { + // Make the compiler parse all comments. + CI.getLangOpts().CommentOpts.ParseAllComments = true; + return WrapperFrontendAction::BeginInvocation(CI); + } + + private: + index::IndexingOptions IndexOpts; + CommentHandler *PragmaHandler; + }; + index::IndexingOptions IndexOpts; + IndexOpts.SystemSymbolFilter = + index::IndexingOptions::SystemSymbolFilterKind::All; + IndexOpts.IndexFunctionLocals = false; + Collector = std::make_shared<SymbolCollector>(COpts); + return new WrappedIndexAction(Collector, std::move(IndexOpts), + PragmaHandler); + } + + std::shared_ptr<SymbolCollector> Collector; + SymbolCollector::Options COpts; + CommentHandler *PragmaHandler; +}; + +class SymbolCollectorTest : public ::testing::Test { +public: + SymbolCollectorTest() + : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem), + TestHeaderName(testPath("symbol.h")), + TestFileName(testPath("symbol.cc")) { + TestHeaderURI = URI::create(TestHeaderName).toString(); + TestFileURI = URI::create(TestFileName).toString(); + } + + // Note that unlike TestTU, no automatic header guard is added. + // HeaderCode should start with #pragma once to be treated as modular. + bool runSymbolCollector(llvm::StringRef HeaderCode, llvm::StringRef MainCode, + const std::vector<std::string> &ExtraArgs = {}) { + llvm::IntrusiveRefCntPtr<FileManager> Files( + new FileManager(FileSystemOptions(), InMemoryFileSystem)); + + auto Factory = llvm::make_unique<SymbolIndexActionFactory>( + CollectorOpts, PragmaHandler.get()); + + std::vector<std::string> Args = {"symbol_collector", "-fsyntax-only", + "-xc++", "-include", TestHeaderName}; + Args.insert(Args.end(), ExtraArgs.begin(), ExtraArgs.end()); + // This allows to override the "-xc++" with something else, i.e. + // -xobjective-c++. + Args.push_back(TestFileName); + + tooling::ToolInvocation Invocation( + Args, Factory->create(), Files.get(), + std::make_shared<PCHContainerOperations>()); + + InMemoryFileSystem->addFile( + TestHeaderName, 0, llvm::MemoryBuffer::getMemBuffer(HeaderCode)); + InMemoryFileSystem->addFile(TestFileName, 0, + llvm::MemoryBuffer::getMemBuffer(MainCode)); + Invocation.run(); + Symbols = Factory->Collector->takeSymbols(); + Refs = Factory->Collector->takeRefs(); + return true; + } + +protected: + llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem; + std::string TestHeaderName; + std::string TestHeaderURI; + std::string TestFileName; + std::string TestFileURI; + SymbolSlab Symbols; + RefSlab Refs; + SymbolCollector::Options CollectorOpts; + std::unique_ptr<CommentHandler> PragmaHandler; +}; + +TEST_F(SymbolCollectorTest, CollectSymbols) { + const std::string Header = R"( + class Foo { + Foo() {} + Foo(int a) {} + void f(); + friend void f1(); + friend class Friend; + Foo& operator=(const Foo&); + ~Foo(); + class Nested { + void f(); + }; + }; + class Friend { + }; + + void f1(); + inline void f2() {} + static const int KInt = 2; + const char* kStr = "123"; + + namespace { + void ff() {} // ignore + } + + void f1() {} + + namespace foo { + // Type alias + typedef int int32; + using int32_t = int32; + + // Variable + int v1; + + // Namespace + namespace bar { + int v2; + } + // Namespace alias + namespace baz = bar; + + using bar::v2; + } // namespace foo + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAreArray( + {AllOf(QName("Foo"), ForCodeCompletion(true)), + AllOf(QName("Foo::Foo"), ForCodeCompletion(false)), + AllOf(QName("Foo::Foo"), ForCodeCompletion(false)), + AllOf(QName("Foo::f"), ForCodeCompletion(false)), + AllOf(QName("Foo::~Foo"), ForCodeCompletion(false)), + AllOf(QName("Foo::operator="), ForCodeCompletion(false)), + AllOf(QName("Foo::Nested"), ForCodeCompletion(false)), + AllOf(QName("Foo::Nested::f"), ForCodeCompletion(false)), + + AllOf(QName("Friend"), ForCodeCompletion(true)), + AllOf(QName("f1"), ForCodeCompletion(true)), + AllOf(QName("f2"), ForCodeCompletion(true)), + AllOf(QName("KInt"), ForCodeCompletion(true)), + AllOf(QName("kStr"), ForCodeCompletion(true)), + AllOf(QName("foo"), ForCodeCompletion(true)), + AllOf(QName("foo::bar"), ForCodeCompletion(true)), + AllOf(QName("foo::int32"), ForCodeCompletion(true)), + AllOf(QName("foo::int32_t"), ForCodeCompletion(true)), + AllOf(QName("foo::v1"), ForCodeCompletion(true)), + AllOf(QName("foo::bar::v2"), ForCodeCompletion(true)), + AllOf(QName("foo::v2"), ForCodeCompletion(true)), + AllOf(QName("foo::baz"), ForCodeCompletion(true))})); +} + +TEST_F(SymbolCollectorTest, FileLocal) { + const std::string Header = R"( + class Foo {}; + namespace { + class Ignored {}; + } + void bar(); + )"; + const std::string Main = R"( + class ForwardDecl; + void bar() {} + static void a(); + class B {}; + namespace { + void c(); + } + )"; + runSymbolCollector(Header, Main); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("Foo"), VisibleOutsideFile()), + AllOf(QName("bar"), VisibleOutsideFile()), + AllOf(QName("a"), Not(VisibleOutsideFile())), + AllOf(QName("B"), Not(VisibleOutsideFile())), + AllOf(QName("c"), Not(VisibleOutsideFile())), + // FIXME: ForwardDecl likely *is* visible outside. + AllOf(QName("ForwardDecl"), Not(VisibleOutsideFile())))); +} + +TEST_F(SymbolCollectorTest, Template) { + Annotations Header(R"( + // Primary template and explicit specialization are indexed, instantiation + // is not. + template <class T, class U> struct [[Tmpl]] {T $xdecl[[x]] = 0;}; + template <> struct $specdecl[[Tmpl]]<int, bool> {}; + template <class U> struct $partspecdecl[[Tmpl]]<bool, U> {}; + extern template struct Tmpl<float, bool>; + template struct Tmpl<double, bool>; + )"); + runSymbolCollector(Header.code(), /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("Tmpl"), DeclRange(Header.range()), + ForCodeCompletion(true)), + AllOf(QName("Tmpl"), DeclRange(Header.range("specdecl")), + ForCodeCompletion(false)), + AllOf(QName("Tmpl"), DeclRange(Header.range("partspecdecl")), + ForCodeCompletion(false)), + AllOf(QName("Tmpl::x"), DeclRange(Header.range("xdecl")), + ForCodeCompletion(false)))); +} + +TEST_F(SymbolCollectorTest, TemplateArgs) { + Annotations Header(R"( + template <class X> class $barclasstemp[[Bar]] {}; + template <class T, class U, template<typename> class Z, int Q> + struct [[Tmpl]] { T $xdecl[[x]] = 0; }; + + // template-template, non-type and type full spec + template <> struct $specdecl[[Tmpl]]<int, bool, Bar, 3> {}; + + // template-template, non-type and type partial spec + template <class U, int T> struct $partspecdecl[[Tmpl]]<bool, U, Bar, T> {}; + // instantiation + extern template struct Tmpl<float, bool, Bar, 8>; + // instantiation + template struct Tmpl<double, bool, Bar, 2>; + + template <typename ...> class $fooclasstemp[[Foo]] {}; + // parameter-packs full spec + template<> class $parampack[[Foo]]<Bar<int>, int, double> {}; + // parameter-packs partial spec + template<class T> class $parampackpartial[[Foo]]<T, T> {}; + + template <int ...> class $bazclasstemp[[Baz]] {}; + // non-type parameter-packs full spec + template<> class $parampacknontype[[Baz]]<3, 5, 8> {}; + // non-type parameter-packs partial spec + template<int T> class $parampacknontypepartial[[Baz]]<T, T> {}; + + template <template <class> class ...> class $fozclasstemp[[Foz]] {}; + // template-template parameter-packs full spec + template<> class $parampacktempltempl[[Foz]]<Bar, Bar> {}; + // template-template parameter-packs partial spec + template<template <class> class T> + class $parampacktempltemplpartial[[Foz]]<T, T> {}; + )"); + runSymbolCollector(Header.code(), /*Main=*/""); + EXPECT_THAT( + Symbols, + AllOf( + Contains(AllOf(QName("Tmpl"), TemplateArgs("<int, bool, Bar, 3>"), + DeclRange(Header.range("specdecl")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Tmpl"), TemplateArgs("<bool, U, Bar, T>"), + DeclRange(Header.range("partspecdecl")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Foo"), TemplateArgs("<Bar<int>, int, double>"), + DeclRange(Header.range("parampack")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Foo"), TemplateArgs("<T, T>"), + DeclRange(Header.range("parampackpartial")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Baz"), TemplateArgs("<3, 5, 8>"), + DeclRange(Header.range("parampacknontype")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Baz"), TemplateArgs("<T, T>"), + DeclRange(Header.range("parampacknontypepartial")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Foz"), TemplateArgs("<Bar, Bar>"), + DeclRange(Header.range("parampacktempltempl")), + ForCodeCompletion(false))), + Contains(AllOf(QName("Foz"), TemplateArgs("<T, T>"), + DeclRange(Header.range("parampacktempltemplpartial")), + ForCodeCompletion(false))))); +} + +TEST_F(SymbolCollectorTest, ObjCSymbols) { + const std::string Header = R"( + @interface Person + - (void)someMethodName:(void*)name1 lastName:(void*)lName; + @end + + @implementation Person + - (void)someMethodName:(void*)name1 lastName:(void*)lName{ + int foo; + ^(int param){ int bar; }; + } + @end + + @interface Person (MyCategory) + - (void)someMethodName2:(void*)name2; + @end + + @implementation Person (MyCategory) + - (void)someMethodName2:(void*)name2 { + int foo2; + } + @end + + @protocol MyProtocol + - (void)someMethodName3:(void*)name3; + @end + )"; + TestFileName = testPath("test.m"); + runSymbolCollector(Header, /*Main=*/"", {"-fblocks", "-xobjective-c++"}); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + QName("Person"), QName("Person::someMethodName:lastName:"), + QName("MyCategory"), QName("Person::someMethodName2:"), + QName("MyProtocol"), QName("MyProtocol::someMethodName3:"))); +} + +TEST_F(SymbolCollectorTest, ObjCPropertyImpl) { + const std::string Header = R"( + @interface Container + @property(nonatomic) int magic; + @end + + @implementation Container + @end + )"; + TestFileName = testPath("test.m"); + runSymbolCollector(Header, /*Main=*/"", {"-xobjective-c++"}); + EXPECT_THAT(Symbols, Contains(QName("Container"))); + EXPECT_THAT(Symbols, Contains(QName("Container::magic"))); + // FIXME: Results also contain Container::_magic on some platforms. + // Figure out why it's platform-dependent. +} + +TEST_F(SymbolCollectorTest, Locations) { + Annotations Header(R"cpp( + // Declared in header, defined in main. + extern int $xdecl[[X]]; + class $clsdecl[[Cls]]; + void $printdecl[[print]](); + + // Declared in header, defined nowhere. + extern int $zdecl[[Z]]; + + void $foodecl[[fo\ +o]](); + )cpp"); + Annotations Main(R"cpp( + int $xdef[[X]] = 42; + class $clsdef[[Cls]] {}; + void $printdef[[print]]() {} + + // Declared/defined in main only. + int $ydecl[[Y]]; + )cpp"); + runSymbolCollector(Header.code(), Main.code()); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("X"), DeclRange(Header.range("xdecl")), + DefRange(Main.range("xdef"))), + AllOf(QName("Cls"), DeclRange(Header.range("clsdecl")), + DefRange(Main.range("clsdef"))), + AllOf(QName("print"), DeclRange(Header.range("printdecl")), + DefRange(Main.range("printdef"))), + AllOf(QName("Z"), DeclRange(Header.range("zdecl"))), + AllOf(QName("foo"), DeclRange(Header.range("foodecl"))), + AllOf(QName("Y"), DeclRange(Main.range("ydecl"))))); +} + +TEST_F(SymbolCollectorTest, Refs) { + Annotations Header(R"( + class $foo[[Foo]] { + public: + $foo[[Foo]]() {} + $foo[[Foo]](int); + }; + class $bar[[Bar]]; + void $func[[func]](); + + namespace $ns[[NS]] {} // namespace ref is ignored + )"); + Annotations Main(R"( + class $bar[[Bar]] {}; + + void $func[[func]](); + + void fff() { + $foo[[Foo]] foo; + $bar[[Bar]] bar; + $func[[func]](); + int abc = 0; + $foo[[Foo]] foo2 = abc; + } + )"); + Annotations SymbolsOnlyInMainCode(R"( + int a; + void b() {} + static const int c = 0; + class d {}; + )"); + CollectorOpts.RefFilter = RefKind::All; + runSymbolCollector(Header.code(), + (Main.code() + SymbolsOnlyInMainCode.code()).str()); + auto HeaderSymbols = TestTU::withHeaderCode(Header.code()).headerSymbols(); + + EXPECT_THAT(Refs, Contains(Pair(findSymbol(Symbols, "Foo").ID, + HaveRanges(Main.ranges("foo"))))); + EXPECT_THAT(Refs, Contains(Pair(findSymbol(Symbols, "Bar").ID, + HaveRanges(Main.ranges("bar"))))); + EXPECT_THAT(Refs, Contains(Pair(findSymbol(Symbols, "func").ID, + HaveRanges(Main.ranges("func"))))); + EXPECT_THAT(Refs, Not(Contains(Pair(findSymbol(Symbols, "NS").ID, _)))); + // Symbols *only* in the main file (a, b, c) had no refs collected. + auto MainSymbols = + TestTU::withHeaderCode(SymbolsOnlyInMainCode.code()).headerSymbols(); + EXPECT_THAT(Refs, Not(Contains(Pair(findSymbol(MainSymbols, "a").ID, _)))); + EXPECT_THAT(Refs, Not(Contains(Pair(findSymbol(MainSymbols, "b").ID, _)))); + EXPECT_THAT(Refs, Not(Contains(Pair(findSymbol(MainSymbols, "c").ID, _)))); +} + +TEST_F(SymbolCollectorTest, RefsInHeaders) { + CollectorOpts.RefFilter = RefKind::All; + CollectorOpts.RefsInHeaders = true; + Annotations Header(R"( + class [[Foo]] {}; + )"); + runSymbolCollector(Header.code(), ""); + EXPECT_THAT(Refs, Contains(Pair(findSymbol(Symbols, "Foo").ID, + HaveRanges(Header.ranges())))); +} + +TEST_F(SymbolCollectorTest, References) { + const std::string Header = R"( + class W; + class X {}; + class Y; + class Z {}; // not used anywhere + Y* y = nullptr; // used in header doesn't count + #define GLOBAL_Z(name) Z name; + )"; + const std::string Main = R"( + W* w = nullptr; + W* w2 = nullptr; // only one usage counts + X x(); + class V; + class Y{}; // definition doesn't count as a reference + V* v = nullptr; + GLOBAL_Z(z); // Not a reference to Z, we don't spell the type. + )"; + CollectorOpts.CountReferences = true; + runSymbolCollector(Header, Main); + EXPECT_THAT( + Symbols, + UnorderedElementsAreArray( + {AllOf(QName("W"), RefCount(1)), AllOf(QName("X"), RefCount(1)), + AllOf(QName("Y"), RefCount(0)), AllOf(QName("Z"), RefCount(0)), + AllOf(QName("y"), RefCount(0)), AllOf(QName("z"), RefCount(0)), + AllOf(QName("x"), RefCount(0)), AllOf(QName("w"), RefCount(0)), + AllOf(QName("w2"), RefCount(0)), AllOf(QName("V"), RefCount(1)), + AllOf(QName("v"), RefCount(0))})); +} + +TEST_F(SymbolCollectorTest, SymbolRelativeNoFallback) { + runSymbolCollector("class Foo {};", /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("Foo"), DeclURI(TestHeaderURI)))); +} + +TEST_F(SymbolCollectorTest, SymbolRelativeWithFallback) { + TestHeaderName = "x.h"; + TestFileName = "x.cpp"; + TestHeaderURI = URI::create(testPath(TestHeaderName)).toString(); + CollectorOpts.FallbackDir = testRoot(); + runSymbolCollector("class Foo {};", /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("Foo"), DeclURI(TestHeaderURI)))); +} + +TEST_F(SymbolCollectorTest, UnittestURIScheme) { + // Use test URI scheme from URITests.cpp + TestHeaderName = testPath("x.h"); + TestFileName = testPath("x.cpp"); + runSymbolCollector("class Foo {};", /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("Foo"), DeclURI("unittest:///x.h")))); +} + +TEST_F(SymbolCollectorTest, IncludeEnums) { + const std::string Header = R"( + enum { + Red + }; + enum Color { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + } + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("Red"), ForCodeCompletion(true)), + AllOf(QName("Color"), ForCodeCompletion(true)), + AllOf(QName("Green"), ForCodeCompletion(true)), + AllOf(QName("Color2"), ForCodeCompletion(true)), + AllOf(QName("Color2::Yellow"), ForCodeCompletion(false)), + AllOf(QName("ns"), ForCodeCompletion(true)), + AllOf(QName("ns::Black"), ForCodeCompletion(true)))); +} + +TEST_F(SymbolCollectorTest, NamelessSymbols) { + const std::string Header = R"( + struct { + int a; + } Foo; + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre(QName("Foo"), + QName("(anonymous struct)::a"))); +} + +TEST_F(SymbolCollectorTest, SymbolFormedFromRegisteredSchemeFromMacro) { + + Annotations Header(R"( + #define FF(name) \ + class name##_Test {}; + + $expansion[[FF]](abc); + + #define FF2() \ + class $spelling[[Test]] {}; + + FF2(); + )"); + + runSymbolCollector(Header.code(), /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("abc_Test"), DeclRange(Header.range("expansion")), + DeclURI(TestHeaderURI)), + AllOf(QName("Test"), DeclRange(Header.range("spelling")), + DeclURI(TestHeaderURI)))); +} + +TEST_F(SymbolCollectorTest, SymbolFormedByCLI) { + Annotations Header(R"( + #ifdef NAME + class $expansion[[NAME]] {}; + #endif + )"); + runSymbolCollector(Header.code(), /*Main=*/"", /*ExtraArgs=*/{"-DNAME=name"}); + EXPECT_THAT(Symbols, UnorderedElementsAre(AllOf( + QName("name"), DeclRange(Header.range("expansion")), + DeclURI(TestHeaderURI)))); +} + +TEST_F(SymbolCollectorTest, SymbolsInMainFile) { + const std::string Main = R"( + class Foo {}; + void f1(); + inline void f2() {} + + namespace { + void ff() {} + } + namespace foo { + namespace { + class Bar {}; + } + } + void main_f() {} + void f1() {} + )"; + runSymbolCollector(/*Header=*/"", Main); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("Foo"), QName("f1"), QName("f2"), + QName("ff"), QName("foo"), QName("foo::Bar"), + QName("main_f"))); +} + +TEST_F(SymbolCollectorTest, Documentation) { + const std::string Header = R"( + // Doc Foo + class Foo { + // Doc f + int f(); + }; + )"; + CollectorOpts.StoreAllDocumentation = false; + runSymbolCollector(Header, /* Main */ ""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("Foo"), Doc("Doc Foo"), ForCodeCompletion(true)), + AllOf(QName("Foo::f"), Doc(""), ReturnType(""), + ForCodeCompletion(false)))); + + CollectorOpts.StoreAllDocumentation = true; + runSymbolCollector(Header, /* Main */ ""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("Foo"), Doc("Doc Foo"), ForCodeCompletion(true)), + AllOf(QName("Foo::f"), Doc("Doc f"), ReturnType(""), + ForCodeCompletion(false)))); +} + +TEST_F(SymbolCollectorTest, ClassMembers) { + const std::string Header = R"( + class Foo { + void f() {} + void g(); + static void sf() {} + static void ssf(); + static int x; + }; + )"; + const std::string Main = R"( + void Foo::g() {} + void Foo::ssf() {} + )"; + runSymbolCollector(Header, Main); + EXPECT_THAT( + Symbols, + UnorderedElementsAre( + QName("Foo"), + AllOf(QName("Foo::f"), ReturnType(""), ForCodeCompletion(false)), + AllOf(QName("Foo::g"), ReturnType(""), ForCodeCompletion(false)), + AllOf(QName("Foo::sf"), ReturnType(""), ForCodeCompletion(false)), + AllOf(QName("Foo::ssf"), ReturnType(""), ForCodeCompletion(false)), + AllOf(QName("Foo::x"), ReturnType(""), ForCodeCompletion(false)))); +} + +TEST_F(SymbolCollectorTest, Scopes) { + const std::string Header = R"( + namespace na { + class Foo {}; + namespace nb { + class Bar {}; + } + } + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("na"), QName("na::nb"), + QName("na::Foo"), QName("na::nb::Bar"))); +} + +TEST_F(SymbolCollectorTest, ExternC) { + const std::string Header = R"( + extern "C" { class Foo {}; } + namespace na { + extern "C" { class Bar {}; } + } + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre(QName("na"), QName("Foo"), + QName("na::Bar"))); +} + +TEST_F(SymbolCollectorTest, SkipInlineNamespace) { + const std::string Header = R"( + namespace na { + inline namespace nb { + class Foo {}; + } + } + namespace na { + // This is still inlined. + namespace nb { + class Bar {}; + } + } + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("na"), QName("na::nb"), + QName("na::Foo"), QName("na::Bar"))); +} + +TEST_F(SymbolCollectorTest, SymbolWithDocumentation) { + const std::string Header = R"( + namespace nx { + /// Foo comment. + int ff(int x, double y) { return 0; } + } + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT( + Symbols, + UnorderedElementsAre( + QName("nx"), AllOf(QName("nx::ff"), Labeled("ff(int x, double y)"), + ReturnType("int"), Doc("Foo comment.")))); +} + +TEST_F(SymbolCollectorTest, Snippet) { + const std::string Header = R"( + namespace nx { + void f() {} + int ff(int x, double y) { return 0; } + } + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + QName("nx"), + AllOf(QName("nx::f"), Labeled("f()"), Snippet("f()")), + AllOf(QName("nx::ff"), Labeled("ff(int x, double y)"), + Snippet("ff(${1:int x}, ${2:double y})")))); +} + +TEST_F(SymbolCollectorTest, IncludeHeaderSameAsFileURI) { + CollectorOpts.CollectIncludePath = true; + runSymbolCollector("#pragma once\nclass Foo {};", /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("Foo"), DeclURI(TestHeaderURI)))); + EXPECT_THAT(Symbols.begin()->IncludeHeaders, + UnorderedElementsAre(IncludeHeaderWithRef(TestHeaderURI, 1u))); +} + +TEST_F(SymbolCollectorTest, CanonicalSTLHeader) { + CollectorOpts.CollectIncludePath = true; + CanonicalIncludes Includes; + addSystemHeadersMapping(&Includes); + CollectorOpts.Includes = &Includes; + runSymbolCollector("namespace std { class string {}; }", /*Main=*/""); + EXPECT_THAT(Symbols, + Contains(AllOf(QName("std::string"), DeclURI(TestHeaderURI), + IncludeHeader("<string>")))); +} + +TEST_F(SymbolCollectorTest, IWYUPragma) { + CollectorOpts.CollectIncludePath = true; + CanonicalIncludes Includes; + PragmaHandler = collectIWYUHeaderMaps(&Includes); + CollectorOpts.Includes = &Includes; + const std::string Header = R"( + // IWYU pragma: private, include the/good/header.h + class Foo {}; + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("Foo"), DeclURI(TestHeaderURI), + IncludeHeader("\"the/good/header.h\"")))); +} + +TEST_F(SymbolCollectorTest, IWYUPragmaWithDoubleQuotes) { + CollectorOpts.CollectIncludePath = true; + CanonicalIncludes Includes; + PragmaHandler = collectIWYUHeaderMaps(&Includes); + CollectorOpts.Includes = &Includes; + const std::string Header = R"( + // IWYU pragma: private, include "the/good/header.h" + class Foo {}; + )"; + runSymbolCollector(Header, /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("Foo"), DeclURI(TestHeaderURI), + IncludeHeader("\"the/good/header.h\"")))); +} + +TEST_F(SymbolCollectorTest, SkipIncFileWhenCanonicalizeHeaders) { + CollectorOpts.CollectIncludePath = true; + CanonicalIncludes Includes; + Includes.addMapping(TestHeaderName, "<canonical>"); + CollectorOpts.Includes = &Includes; + auto IncFile = testPath("test.inc"); + auto IncURI = URI::create(IncFile).toString(); + InMemoryFileSystem->addFile(IncFile, 0, + llvm::MemoryBuffer::getMemBuffer("class X {};")); + runSymbolCollector("#include \"test.inc\"\nclass Y {};", /*Main=*/"", + /*ExtraArgs=*/{"-I", testRoot()}); + EXPECT_THAT(Symbols, + UnorderedElementsAre(AllOf(QName("X"), DeclURI(IncURI), + IncludeHeader("<canonical>")), + AllOf(QName("Y"), DeclURI(TestHeaderURI), + IncludeHeader("<canonical>")))); +} + +TEST_F(SymbolCollectorTest, MainFileIsHeaderWhenSkipIncFile) { + CollectorOpts.CollectIncludePath = true; + // To make this case as hard as possible, we won't tell clang main is a + // header. No extension, no -x c++-header. + TestFileName = testPath("no_ext_main"); + TestFileURI = URI::create(TestFileName).toString(); + auto IncFile = testPath("test.inc"); + auto IncURI = URI::create(IncFile).toString(); + InMemoryFileSystem->addFile(IncFile, 0, + llvm::MemoryBuffer::getMemBuffer("class X {};")); + runSymbolCollector("", R"cpp( + // Can't use #pragma once in a main file clang doesn't think is a header. + #ifndef MAIN_H_ + #define MAIN_H_ + #include "test.inc" + #endif + )cpp", + /*ExtraArgs=*/{"-I", testRoot()}); + EXPECT_THAT(Symbols, UnorderedElementsAre(AllOf(QName("X"), DeclURI(IncURI), + IncludeHeader(TestFileURI)))); +} + +TEST_F(SymbolCollectorTest, IncFileInNonHeader) { + CollectorOpts.CollectIncludePath = true; + TestFileName = testPath("main.cc"); + TestFileURI = URI::create(TestFileName).toString(); + auto IncFile = testPath("test.inc"); + auto IncURI = URI::create(IncFile).toString(); + InMemoryFileSystem->addFile(IncFile, 0, + llvm::MemoryBuffer::getMemBuffer("class X {};")); + runSymbolCollector("", R"cpp( + #include "test.inc" + )cpp", + /*ExtraArgs=*/{"-I", testRoot()}); + EXPECT_THAT(Symbols, UnorderedElementsAre(AllOf(QName("X"), DeclURI(IncURI), + Not(IncludeHeader())))); +} + +// Features that depend on header-guards are fragile. Header guards are only +// recognized when the file ends, so we have to defer checking for them. +TEST_F(SymbolCollectorTest, HeaderGuardDetected) { + CollectorOpts.CollectIncludePath = true; + CollectorOpts.CollectMacro = true; + runSymbolCollector(R"cpp( + #ifndef HEADER_GUARD_ + #define HEADER_GUARD_ + + // Symbols are seen before the header guard is complete. + #define MACRO + int decl(); + + #endif // Header guard is recognized here. + )cpp", + ""); + EXPECT_THAT(Symbols, Not(Contains(QName("HEADER_GUARD_")))); + EXPECT_THAT(Symbols, Each(IncludeHeader())); +} + +TEST_F(SymbolCollectorTest, NonModularHeader) { + auto TU = TestTU::withHeaderCode("int x();"); + EXPECT_THAT(TU.headerSymbols(), ElementsAre(IncludeHeader())); + + // Files missing include guards aren't eligible for insertion. + TU.ImplicitHeaderGuard = false; + EXPECT_THAT(TU.headerSymbols(), ElementsAre(Not(IncludeHeader()))); + + // We recognize some patterns of trying to prevent insertion. + TU = TestTU::withHeaderCode(R"cpp( +#ifndef SECRET +#error "This file isn't safe to include directly" +#endif + int x(); + )cpp"); + TU.ExtraArgs.push_back("-DSECRET"); // *we're* able to include it. + EXPECT_THAT(TU.headerSymbols(), ElementsAre(Not(IncludeHeader()))); +} + +TEST_F(SymbolCollectorTest, AvoidUsingFwdDeclsAsCanonicalDecls) { + CollectorOpts.CollectIncludePath = true; + Annotations Header(R"( + #pragma once + // Forward declarations of TagDecls. + class C; + struct S; + union U; + + // Canonical declarations. + class $cdecl[[C]] {}; + struct $sdecl[[S]] {}; + union $udecl[[U]] {int $xdecl[[x]]; bool $ydecl[[y]];}; + )"); + runSymbolCollector(Header.code(), /*Main=*/""); + EXPECT_THAT( + Symbols, + UnorderedElementsAre( + AllOf(QName("C"), DeclURI(TestHeaderURI), + DeclRange(Header.range("cdecl")), IncludeHeader(TestHeaderURI), + DefURI(TestHeaderURI), DefRange(Header.range("cdecl"))), + AllOf(QName("S"), DeclURI(TestHeaderURI), + DeclRange(Header.range("sdecl")), IncludeHeader(TestHeaderURI), + DefURI(TestHeaderURI), DefRange(Header.range("sdecl"))), + AllOf(QName("U"), DeclURI(TestHeaderURI), + DeclRange(Header.range("udecl")), IncludeHeader(TestHeaderURI), + DefURI(TestHeaderURI), DefRange(Header.range("udecl"))), + AllOf(QName("U::x"), DeclURI(TestHeaderURI), + DeclRange(Header.range("xdecl")), DefURI(TestHeaderURI), + DefRange(Header.range("xdecl"))), + AllOf(QName("U::y"), DeclURI(TestHeaderURI), + DeclRange(Header.range("ydecl")), DefURI(TestHeaderURI), + DefRange(Header.range("ydecl"))))); +} + +TEST_F(SymbolCollectorTest, ClassForwardDeclarationIsCanonical) { + CollectorOpts.CollectIncludePath = true; + runSymbolCollector(/*Header=*/"#pragma once\nclass X;", + /*Main=*/"class X {};"); + EXPECT_THAT(Symbols, UnorderedElementsAre(AllOf( + QName("X"), DeclURI(TestHeaderURI), + IncludeHeader(TestHeaderURI), DefURI(TestFileURI)))); +} + +TEST_F(SymbolCollectorTest, UTF16Character) { + // ö is 2-bytes. + Annotations Header(/*Header=*/"class [[pörk]] {};"); + runSymbolCollector(Header.code(), /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("pörk"), DeclRange(Header.range())))); +} + +TEST_F(SymbolCollectorTest, DoNotIndexSymbolsInFriendDecl) { + Annotations Header(R"( + namespace nx { + class $z[[Z]] {}; + class X { + friend class Y; + friend class Z; + friend void foo(); + friend void $bar[[bar]]() {} + }; + class $y[[Y]] {}; + void $foo[[foo]](); + } + )"); + runSymbolCollector(Header.code(), /*Main=*/""); + + EXPECT_THAT(Symbols, + UnorderedElementsAre( + QName("nx"), QName("nx::X"), + AllOf(QName("nx::Y"), DeclRange(Header.range("y"))), + AllOf(QName("nx::Z"), DeclRange(Header.range("z"))), + AllOf(QName("nx::foo"), DeclRange(Header.range("foo"))), + AllOf(QName("nx::bar"), DeclRange(Header.range("bar"))))); +} + +TEST_F(SymbolCollectorTest, ReferencesInFriendDecl) { + const std::string Header = R"( + class X; + class Y; + )"; + const std::string Main = R"( + class C { + friend ::X; + friend class Y; + }; + )"; + CollectorOpts.CountReferences = true; + runSymbolCollector(Header, Main); + EXPECT_THAT(Symbols, UnorderedElementsAre(AllOf(QName("X"), RefCount(1)), + AllOf(QName("Y"), RefCount(1)), + AllOf(QName("C"), RefCount(0)))); +} + +TEST_F(SymbolCollectorTest, Origin) { + CollectorOpts.Origin = SymbolOrigin::Static; + runSymbolCollector("class Foo {};", /*Main=*/""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + Field(&Symbol::Origin, SymbolOrigin::Static))); +} + +TEST_F(SymbolCollectorTest, CollectMacros) { + CollectorOpts.CollectIncludePath = true; + Annotations Header(R"( + #pragma once + #define X 1 + #define $mac[[MAC]](x) int x + #define $used[[USED]](y) float y; + + MAC(p); + )"); + + Annotations Main(R"( + #define $main[[MAIN]] 1 + USED(t); + )"); + CollectorOpts.CountReferences = true; + CollectorOpts.CollectMacro = true; + runSymbolCollector(Header.code(), Main.code()); + EXPECT_THAT( + Symbols, + UnorderedElementsAre( + QName("p"), QName("t"), + AllOf(QName("X"), DeclURI(TestHeaderURI), + IncludeHeader(TestHeaderURI)), + AllOf(Labeled("MAC(x)"), RefCount(0), + + DeclRange(Header.range("mac")), VisibleOutsideFile()), + AllOf(Labeled("USED(y)"), RefCount(1), + DeclRange(Header.range("used")), VisibleOutsideFile()), + AllOf(Labeled("MAIN"), RefCount(0), DeclRange(Main.range("main")), + Not(VisibleOutsideFile())))); +} + +TEST_F(SymbolCollectorTest, DeprecatedSymbols) { + const std::string Header = R"( + void TestClangc() __attribute__((deprecated("", ""))); + void TestClangd(); + )"; + runSymbolCollector(Header, /**/ ""); + EXPECT_THAT(Symbols, UnorderedElementsAre( + AllOf(QName("TestClangc"), Deprecated()), + AllOf(QName("TestClangd"), Not(Deprecated())))); +} + +TEST_F(SymbolCollectorTest, ImplementationDetail) { + const std::string Header = R"( + #define DECL_NAME(x, y) x##_##y##_Decl + #define DECL(x, y) class DECL_NAME(x, y) {}; + DECL(X, Y); // X_Y_Decl + + class Public {}; + )"; + runSymbolCollector(Header, /**/ ""); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + AllOf(QName("X_Y_Decl"), ImplementationDetail()), + AllOf(QName("Public"), Not(ImplementationDetail())))); +} + +TEST_F(SymbolCollectorTest, UsingDecl) { + const char *Header = R"( + void foo(); + namespace std { + using ::foo; + })"; + runSymbolCollector(Header, /**/ ""); + EXPECT_THAT(Symbols, Contains(QName("std::foo"))); +} + +TEST_F(SymbolCollectorTest, CBuiltins) { + // In C, printf in stdio.h is a redecl of an implicit builtin. + const char *Header = R"( + extern int printf(const char*, ...); + )"; + runSymbolCollector(Header, /**/ "", {"-xc"}); + EXPECT_THAT(Symbols, Contains(QName("printf"))); +} + +TEST_F(SymbolCollectorTest, InvalidSourceLoc) { + const char *Header = R"( + void operator delete(void*) + __attribute__((__externally_visible__));)"; + runSymbolCollector(Header, /**/ ""); + EXPECT_THAT(Symbols, Contains(QName("operator delete"))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SymbolInfoTests.cpp b/clangd/unittests/SymbolInfoTests.cpp new file mode 100644 index 00000000..d8d40432 --- /dev/null +++ b/clangd/unittests/SymbolInfoTests.cpp @@ -0,0 +1,339 @@ +//===-- SymbolInfoTests.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 "ClangdUnit.h" +#include "Compiler.h" +#include "Matchers.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestTU.h" +#include "XRefs.h" +#include "index/FileIndex.h" +#include "index/SymbolCollector.h" +#include "clang/Index/IndexingAction.h" +#include "llvm/Support/Path.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +using ::testing::ElementsAreArray; + +auto CreateExpectedSymbolDetails = [](const std::string &name, + const std::string &container, + const std::string &USR) { + return SymbolDetails{name, container, USR, SymbolID(USR)}; +}; + +TEST(SymbolInfoTests, All) { + std::pair<const char *, std::vector<SymbolDetails>> + TestInputExpectedOutput[] = { + { + R"cpp( // Simple function reference - declaration + void foo(); + int bar() { + fo^o(); + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@F@foo#")}}, + { + R"cpp( // Simple function reference - definition + void foo() {} + int bar() { + fo^o(); + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@F@foo#")}}, + { + R"cpp( // Function in namespace reference + namespace bar { + void foo(); + int baz() { + fo^o(); + } + } + )cpp", + {CreateExpectedSymbolDetails("foo", "bar::", "c:@N@bar@F@foo#")}}, + { + R"cpp( // Function in different namespace reference + namespace bar { + void foo(); + } + namespace barbar { + int baz() { + bar::fo^o(); + } + } + )cpp", + {CreateExpectedSymbolDetails("foo", "bar::", "c:@N@bar@F@foo#")}}, + { + R"cpp( // Function in global namespace reference + void foo(); + namespace Nbar { + namespace Nbaz { + int baz() { + ::fo^o(); + } + } + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@F@foo#")}}, + { + R"cpp( // Function in anonymous namespace reference + namespace { + void foo(); + } + namespace barbar { + int baz() { + fo^o(); + } + } + )cpp", + {CreateExpectedSymbolDetails("foo", "(anonymous)", + "c:TestTU.cpp@aN@F@foo#")}}, + { + R"cpp( // Function reference - ADL + namespace bar { + struct BarType {}; + void foo(const BarType&); + } + namespace barbar { + int baz() { + bar::BarType b; + fo^o(b); + } + } + )cpp", + {CreateExpectedSymbolDetails( + "foo", "bar::", "c:@N@bar@F@foo#&1$@N@bar@S@BarType#")}}, + { + R"cpp( // Global value reference + int value; + void foo(int) { } + void bar() { + foo(val^ue); + } + )cpp", + {CreateExpectedSymbolDetails("value", "", "c:@value")}}, + { + R"cpp( // Local value reference + void foo() { int aaa; int bbb = aa^a; } + )cpp", + {CreateExpectedSymbolDetails("aaa", "foo", + "c:TestTU.cpp@49@F@foo#@aaa")}}, + { + R"cpp( // Function param + void bar(int aaa) { + int bbb = a^aa; + } + )cpp", + {CreateExpectedSymbolDetails("aaa", "bar", + "c:TestTU.cpp@38@F@bar#I#@aaa")}}, + { + R"cpp( // Lambda capture + int ii; + auto lam = [ii]() { + return i^i; + }; + )cpp", + {CreateExpectedSymbolDetails("ii", "", "c:@ii")}}, + { + R"cpp( // Macro reference + #define MACRO 5\nint i = MAC^RO; + )cpp", + {CreateExpectedSymbolDetails("MACRO", "", + "c:TestTU.cpp@38@macro@MACRO")}}, + { + R"cpp( // Macro reference + #define MACRO 5\nint i = MACRO^; + )cpp", + {CreateExpectedSymbolDetails("MACRO", "", + "c:TestTU.cpp@38@macro@MACRO")}}, + { + R"cpp( // Multiple symbols returned - using overloaded function name + void foo() {} + void foo(bool) {} + void foo(int) {} + namespace bar { + using ::fo^o; + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@F@foo#"), + CreateExpectedSymbolDetails("foo", "", "c:@F@foo#b#"), + CreateExpectedSymbolDetails("foo", "", "c:@F@foo#I#"), + CreateExpectedSymbolDetails("foo", "bar::", "c:@N@bar@UD@foo")}}, + { + R"cpp( // Multiple symbols returned - implicit conversion + struct foo {}; + struct bar { + bar(const foo&) {} + }; + void func_baz1(bar) {} + void func_baz2() { + foo ff; + func_baz1(f^f); + } + )cpp", + {CreateExpectedSymbolDetails( + "ff", "func_baz2", "c:TestTU.cpp@218@F@func_baz2#@ff")}}, + { + R"cpp( // Type reference - declaration + struct foo; + void bar(fo^o*); + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@S@foo")}}, + { + R"cpp( // Type reference - definition + struct foo {}; + void bar(fo^o*); + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@S@foo")}}, + { + R"cpp( // Type Reference - template argumen + struct foo {}; + template<class T> struct bar {}; + void baz() { + bar<fo^o> b; + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@S@foo")}}, + { + R"cpp( // Template parameter reference - type param + template<class TT> struct bar { + T^T t; + }; + )cpp", + {CreateExpectedSymbolDetails("TT", "bar::", "c:TestTU.cpp@65")}}, + { + R"cpp( // Template parameter reference - type param + template<int NN> struct bar { + int a = N^N; + }; + )cpp", + {CreateExpectedSymbolDetails("NN", "bar::", "c:TestTU.cpp@65")}}, + { + R"cpp( // Class member reference - objec + struct foo { + int aa; + }; + void bar() { + foo f; + f.a^a; + } + )cpp", + {CreateExpectedSymbolDetails("aa", "foo::", "c:@S@foo@FI@aa")}}, + { + R"cpp( // Class member reference - pointer + struct foo { + int aa; + }; + void bar() { + &foo::a^a; + } + )cpp", + {CreateExpectedSymbolDetails("aa", "foo::", "c:@S@foo@FI@aa")}}, + { + R"cpp( // Class method reference - objec + struct foo { + void aa() {} + }; + void bar() { + foo f; + f.a^a(); + } + )cpp", + {CreateExpectedSymbolDetails("aa", "foo::", "c:@S@foo@F@aa#")}}, + { + R"cpp( // Class method reference - pointer + struct foo { + void aa() {} + }; + void bar() { + &foo::a^a; + } + )cpp", + {CreateExpectedSymbolDetails("aa", "foo::", "c:@S@foo@F@aa#")}}, + { + R"cpp( // Typedef + typedef int foo; + void bar() { + fo^o a; + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:TestTU.cpp@T@foo")}}, + { + R"cpp( // Type alias + using foo = int; + void bar() { + fo^o a; + } + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@foo")}}, + { + R"cpp( // Namespace reference + namespace foo {} + using namespace fo^o; + )cpp", + {CreateExpectedSymbolDetails("foo", "", "c:@N@foo")}}, + { + R"cpp( // Enum value reference + enum foo { bar, baz }; + void f() { + foo fff = ba^r; + } + )cpp", + {CreateExpectedSymbolDetails("bar", "foo", "c:@E@foo@bar")}}, + { + R"cpp( // Enum class value reference + enum class foo { bar, baz }; + void f() { + foo fff = foo::ba^r; + } + )cpp", + {CreateExpectedSymbolDetails("bar", "foo::", "c:@E@foo@bar")}}, + { + R"cpp( // Parameters in declarations + void foo(int ba^r); + )cpp", + {CreateExpectedSymbolDetails("bar", "foo", + "c:TestTU.cpp@50@F@foo#I#@bar")}}, + { + R"cpp( // Type inferrence with auto keyword + struct foo {}; + foo getfoo() { return foo{}; } + void f() { + au^to a = getfoo(); + } + )cpp", + {/* not implemented */}}, + { + R"cpp( // decltype + struct foo {}; + void f() { + foo f; + declt^ype(f); + } + )cpp", + {/* not implemented */}}, + }; + + for (const auto &T : TestInputExpectedOutput) { + Annotations TestInput(T.first); + auto AST = TestTU::withCode(TestInput.code()).build(); + + EXPECT_THAT(getSymbolInfo(AST, TestInput.point()), + ElementsAreArray(T.second)) + << T.first; + } +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SyncAPI.cpp b/clangd/unittests/SyncAPI.cpp new file mode 100644 index 00000000..102cecb5 --- /dev/null +++ b/clangd/unittests/SyncAPI.cpp @@ -0,0 +1,151 @@ +//===--- SyncAPI.cpp - Sync version of ClangdServer's API --------*- 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 "SyncAPI.h" +#include "index/Index.h" + +namespace clang { +namespace clangd { + +void runAddDocument(ClangdServer &Server, PathRef File, + llvm::StringRef Contents, WantDiagnostics WantDiags) { + Server.addDocument(File, Contents, WantDiags); + if (!Server.blockUntilIdleForTest()) + llvm_unreachable("not idle after addDocument"); +} + +namespace { +/// A helper that waits for async callbacks to fire and exposes their result in +/// the output variable. Intended to be used in the following way: +/// T Result; +/// someAsyncFunc(Param1, Param2, /*Callback=*/capture(Result)); +template <typename T> struct CaptureProxy { + CaptureProxy(llvm::Optional<T> &Target) : Target(&Target) { + assert(!Target.hasValue()); + } + + CaptureProxy(const CaptureProxy &) = delete; + CaptureProxy &operator=(const CaptureProxy &) = delete; + // We need move ctor to return a value from the 'capture' helper. + CaptureProxy(CaptureProxy &&Other) : Target(Other.Target) { + Other.Target = nullptr; + } + CaptureProxy &operator=(CaptureProxy &&) = delete; + + operator llvm::unique_function<void(T)>() && { + assert(!Future.valid() && "conversion to callback called multiple times"); + Future = Promise.get_future(); + return Bind( + [](std::promise<std::shared_ptr<T>> Promise, T Value) { + Promise.set_value(std::make_shared<T>(std::move(Value))); + }, + std::move(Promise)); + } + + ~CaptureProxy() { + if (!Target) + return; + assert(Future.valid() && "conversion to callback was not called"); + assert(!Target->hasValue()); + Target->emplace(std::move(*Future.get())); + } + +private: + llvm::Optional<T> *Target; + // Using shared_ptr to workaround compilation errors with MSVC. + // MSVC only allows default-construcitble and copyable objects as future<> + // arguments. + std::promise<std::shared_ptr<T>> Promise; + std::future<std::shared_ptr<T>> Future; +}; + +template <typename T> CaptureProxy<T> capture(llvm::Optional<T> &Target) { + return CaptureProxy<T>(Target); +} +} // namespace + +llvm::Expected<CodeCompleteResult> +runCodeComplete(ClangdServer &Server, PathRef File, Position Pos, + clangd::CodeCompleteOptions Opts) { + llvm::Optional<llvm::Expected<CodeCompleteResult>> Result; + Server.codeComplete(File, Pos, Opts, capture(Result)); + return std::move(*Result); +} + +llvm::Expected<SignatureHelp> runSignatureHelp(ClangdServer &Server, + PathRef File, Position Pos) { + llvm::Optional<llvm::Expected<SignatureHelp>> Result; + Server.signatureHelp(File, Pos, capture(Result)); + return std::move(*Result); +} + +llvm::Expected<std::vector<LocatedSymbol>> +runLocateSymbolAt(ClangdServer &Server, PathRef File, Position Pos) { + llvm::Optional<llvm::Expected<std::vector<LocatedSymbol>>> Result; + Server.locateSymbolAt(File, Pos, capture(Result)); + return std::move(*Result); +} + +llvm::Expected<std::vector<DocumentHighlight>> +runFindDocumentHighlights(ClangdServer &Server, PathRef File, Position Pos) { + llvm::Optional<llvm::Expected<std::vector<DocumentHighlight>>> Result; + Server.findDocumentHighlights(File, Pos, capture(Result)); + return std::move(*Result); +} + +llvm::Expected<std::vector<TextEdit>> runRename(ClangdServer &Server, + PathRef File, Position Pos, + llvm::StringRef NewName) { + llvm::Optional<llvm::Expected<std::vector<TextEdit>>> Result; + Server.rename(File, Pos, NewName, capture(Result)); + return std::move(*Result); +} + +std::string runDumpAST(ClangdServer &Server, PathRef File) { + llvm::Optional<std::string> Result; + Server.dumpAST(File, capture(Result)); + return std::move(*Result); +} + +llvm::Expected<std::vector<SymbolInformation>> +runWorkspaceSymbols(ClangdServer &Server, llvm::StringRef Query, int Limit) { + llvm::Optional<llvm::Expected<std::vector<SymbolInformation>>> Result; + Server.workspaceSymbols(Query, Limit, capture(Result)); + return std::move(*Result); +} + +llvm::Expected<std::vector<DocumentSymbol>> +runDocumentSymbols(ClangdServer &Server, PathRef File) { + llvm::Optional<llvm::Expected<std::vector<DocumentSymbol>>> Result; + Server.documentSymbols(File, capture(Result)); + return std::move(*Result); +} + +SymbolSlab runFuzzyFind(const SymbolIndex &Index, llvm::StringRef Query) { + FuzzyFindRequest Req; + Req.Query = Query; + Req.AnyScope = true; + return runFuzzyFind(Index, Req); +} + +SymbolSlab runFuzzyFind(const SymbolIndex &Index, const FuzzyFindRequest &Req) { + SymbolSlab::Builder Builder; + Index.fuzzyFind(Req, [&](const Symbol &Sym) { Builder.insert(Sym); }); + return std::move(Builder).build(); +} + +RefSlab getRefs(const SymbolIndex &Index, SymbolID ID) { + RefsRequest Req; + Req.IDs = {ID}; + RefSlab::Builder Slab; + Index.refs(Req, [&](const Ref &S) { Slab.insert(ID, S); }); + return std::move(Slab).build(); +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/SyncAPI.h b/clangd/unittests/SyncAPI.h new file mode 100644 index 00000000..c1416524 --- /dev/null +++ b/clangd/unittests/SyncAPI.h @@ -0,0 +1,59 @@ +//===--- SyncAPI.h - Sync version of ClangdServer's API ----------*- 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 +// +//===----------------------------------------------------------------------===// +// +// This file contains synchronous versions of ClangdServer's async API. We +// deliberately don't expose the sync API outside tests to encourage using the +// async versions in clangd code. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_SYNCAPI_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_SYNCAPI_H + +#include "ClangdServer.h" +#include "index/Index.h" + +namespace clang { +namespace clangd { + +// Calls addDocument and then blockUntilIdleForTest. +void runAddDocument(ClangdServer &Server, PathRef File, StringRef Contents, + WantDiagnostics WantDiags = WantDiagnostics::Auto); + +llvm::Expected<CodeCompleteResult> +runCodeComplete(ClangdServer &Server, PathRef File, Position Pos, + clangd::CodeCompleteOptions Opts); + +llvm::Expected<SignatureHelp> runSignatureHelp(ClangdServer &Server, + PathRef File, Position Pos); + +llvm::Expected<std::vector<LocatedSymbol>> +runLocateSymbolAt(ClangdServer &Server, PathRef File, Position Pos); + +llvm::Expected<std::vector<DocumentHighlight>> +runFindDocumentHighlights(ClangdServer &Server, PathRef File, Position Pos); + +llvm::Expected<std::vector<TextEdit>> +runRename(ClangdServer &Server, PathRef File, Position Pos, StringRef NewName); + +std::string runDumpAST(ClangdServer &Server, PathRef File); + +llvm::Expected<std::vector<SymbolInformation>> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit); + +Expected<std::vector<DocumentSymbol>> runDocumentSymbols(ClangdServer &Server, + PathRef File); + +SymbolSlab runFuzzyFind(const SymbolIndex &Index, StringRef Query); +SymbolSlab runFuzzyFind(const SymbolIndex &Index, const FuzzyFindRequest &Req); +RefSlab getRefs(const SymbolIndex &Index, SymbolID ID); + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_SYNCAPI_H diff --git a/clangd/unittests/TUSchedulerTests.cpp b/clangd/unittests/TUSchedulerTests.cpp new file mode 100644 index 00000000..a7d032cc --- /dev/null +++ b/clangd/unittests/TUSchedulerTests.cpp @@ -0,0 +1,710 @@ +//===-- TUSchedulerTests.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 "Context.h" +#include "Matchers.h" +#include "TUScheduler.h" +#include "TestFS.h" +#include "llvm/ADT/ScopeExit.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <algorithm> +#include <utility> + +namespace clang { +namespace clangd { +namespace { + +using ::testing::AnyOf; +using ::testing::Each; +using ::testing::ElementsAre; +using ::testing::Pointee; +using ::testing::UnorderedElementsAre; + +MATCHER_P2(TUState, State, ActionName, "") { + return arg.Action.S == State && arg.Action.Name == ActionName; +} + +class TUSchedulerTests : public ::testing::Test { +protected: + ParseInputs getInputs(PathRef File, std::string Contents) { + ParseInputs Inputs; + Inputs.CompileCommand = *CDB.getCompileCommand(File); + Inputs.FS = buildTestFS(Files, Timestamps); + Inputs.Contents = std::move(Contents); + Inputs.Opts = ParseOptions(); + return Inputs; + } + + void updateWithCallback(TUScheduler &S, PathRef File, + llvm::StringRef Contents, WantDiagnostics WD, + llvm::unique_function<void()> CB) { + WithContextValue Ctx(llvm::make_scope_exit(std::move(CB))); + S.update(File, getInputs(File, Contents), WD); + } + + static Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>> + DiagsCallbackKey; + + /// A diagnostics callback that should be passed to TUScheduler when it's used + /// in updateWithDiags. + static std::unique_ptr<ParsingCallbacks> captureDiags() { + class CaptureDiags : public ParsingCallbacks { + void onDiagnostics(PathRef File, std::vector<Diag> Diags) override { + auto D = Context::current().get(DiagsCallbackKey); + if (!D) + return; + const_cast<llvm::unique_function<void(PathRef, std::vector<Diag>)> &> ( + *D)(File, Diags); + } + }; + return llvm::make_unique<CaptureDiags>(); + } + + /// Schedule an update and call \p CB with the diagnostics it produces, if + /// any. The TUScheduler should be created with captureDiags as a + /// DiagsCallback for this to work. + void updateWithDiags(TUScheduler &S, PathRef File, ParseInputs Inputs, + WantDiagnostics WD, + llvm::unique_function<void(std::vector<Diag>)> CB) { + Path OrigFile = File.str(); + WithContextValue Ctx( + DiagsCallbackKey, + Bind( + [OrigFile](decltype(CB) CB, PathRef File, std::vector<Diag> Diags) { + assert(File == OrigFile); + CB(std::move(Diags)); + }, + std::move(CB))); + S.update(File, std::move(Inputs), WD); + } + + void updateWithDiags(TUScheduler &S, PathRef File, llvm::StringRef Contents, + WantDiagnostics WD, + llvm::unique_function<void(std::vector<Diag>)> CB) { + return updateWithDiags(S, File, getInputs(File, Contents), WD, + std::move(CB)); + } + + llvm::StringMap<std::string> Files; + llvm::StringMap<time_t> Timestamps; + MockCompilationDatabase CDB; +}; + +Key<llvm::unique_function<void(PathRef File, std::vector<Diag>)>> + TUSchedulerTests::DiagsCallbackKey; + +TEST_F(TUSchedulerTests, MissingFiles) { + TUScheduler S(CDB, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, /*ASTCallbacks=*/nullptr, + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + + auto Added = testPath("added.cpp"); + Files[Added] = ""; + + auto Missing = testPath("missing.cpp"); + Files[Missing] = ""; + + S.update(Added, getInputs(Added, ""), WantDiagnostics::No); + + // Assert each operation for missing file is an error (even if it's available + // in VFS). + S.runWithAST("", Missing, + [&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); }); + S.runWithPreamble( + "", Missing, TUScheduler::Stale, + [&](Expected<InputsAndPreamble> Preamble) { EXPECT_ERROR(Preamble); }); + // remove() shouldn't crash on missing files. + S.remove(Missing); + + // Assert there aren't any errors for added file. + S.runWithAST("", Added, + [&](Expected<InputsAndAST> AST) { EXPECT_TRUE(bool(AST)); }); + S.runWithPreamble("", Added, TUScheduler::Stale, + [&](Expected<InputsAndPreamble> Preamble) { + EXPECT_TRUE(bool(Preamble)); + }); + S.remove(Added); + + // Assert that all operations fail after removing the file. + S.runWithAST("", Added, + [&](Expected<InputsAndAST> AST) { EXPECT_ERROR(AST); }); + S.runWithPreamble("", Added, TUScheduler::Stale, + [&](Expected<InputsAndPreamble> Preamble) { + ASSERT_FALSE(bool(Preamble)); + llvm::consumeError(Preamble.takeError()); + }); + // remove() shouldn't crash on missing files. + S.remove(Added); +} + +TEST_F(TUSchedulerTests, WantDiagnostics) { + std::atomic<int> CallbackCount(0); + { + // To avoid a racy test, don't allow tasks to actualy run on the worker + // thread until we've scheduled them all. + Notification Ready; + TUScheduler S( + CDB, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, captureDiags(), + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + auto Path = testPath("foo.cpp"); + updateWithDiags(S, Path, "", WantDiagnostics::Yes, + [&](std::vector<Diag>) { Ready.wait(); }); + updateWithDiags(S, Path, "request diags", WantDiagnostics::Yes, + [&](std::vector<Diag>) { ++CallbackCount; }); + updateWithDiags(S, Path, "auto (clobbered)", WantDiagnostics::Auto, + [&](std::vector<Diag>) { + ADD_FAILURE() + << "auto should have been cancelled by auto"; + }); + updateWithDiags(S, Path, "request no diags", WantDiagnostics::No, + [&](std::vector<Diag>) { + ADD_FAILURE() << "no diags should not be called back"; + }); + updateWithDiags(S, Path, "auto (produces)", WantDiagnostics::Auto, + [&](std::vector<Diag>) { ++CallbackCount; }); + Ready.notify(); + + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + } + EXPECT_EQ(2, CallbackCount); +} + +TEST_F(TUSchedulerTests, Debounce) { + std::atomic<int> CallbackCount(0); + { + TUScheduler S(CDB, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, captureDiags(), + /*UpdateDebounce=*/std::chrono::seconds(1), + ASTRetentionPolicy()); + // FIXME: we could probably use timeouts lower than 1 second here. + auto Path = testPath("foo.cpp"); + updateWithDiags(S, Path, "auto (debounced)", WantDiagnostics::Auto, + [&](std::vector<Diag>) { + ADD_FAILURE() + << "auto should have been debounced and canceled"; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + updateWithDiags(S, Path, "auto (timed out)", WantDiagnostics::Auto, + [&](std::vector<Diag>) { ++CallbackCount; }); + std::this_thread::sleep_for(std::chrono::seconds(2)); + updateWithDiags(S, Path, "auto (shut down)", WantDiagnostics::Auto, + [&](std::vector<Diag>) { ++CallbackCount; }); + + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + } + EXPECT_EQ(2, CallbackCount); +} + +static std::vector<std::string> includes(const PreambleData *Preamble) { + std::vector<std::string> Result; + if (Preamble) + for (const auto &Inclusion : Preamble->Includes.MainFileIncludes) + Result.push_back(Inclusion.Written); + return Result; +} + +TEST_F(TUSchedulerTests, PreambleConsistency) { + std::atomic<int> CallbackCount(0); + { + Notification InconsistentReadDone; // Must live longest. + TUScheduler S( + CDB, getDefaultAsyncThreadsCount(), /*StorePreamblesInMemory=*/true, + /*ASTCallbacks=*/nullptr, + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + auto Path = testPath("foo.cpp"); + // Schedule two updates (A, B) and two preamble reads (stale, consistent). + // The stale read should see A, and the consistent read should see B. + // (We recognize the preambles by their included files). + updateWithCallback(S, Path, "#include <A>", WantDiagnostics::Yes, [&]() { + // This callback runs in between the two preamble updates. + + // This blocks update B, preventing it from winning the race + // against the stale read. + // If the first read was instead consistent, this would deadlock. + InconsistentReadDone.wait(); + // This delays update B, preventing it from winning a race + // against the consistent read. The consistent read sees B + // only because it waits for it. + // If the second read was stale, it would usually see A. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + }); + S.update(Path, getInputs(Path, "#include <B>"), WantDiagnostics::Yes); + + S.runWithPreamble("StaleRead", Path, TUScheduler::Stale, + [&](Expected<InputsAndPreamble> Pre) { + ASSERT_TRUE(bool(Pre)); + assert(bool(Pre)); + EXPECT_THAT(includes(Pre->Preamble), + ElementsAre("<A>")); + InconsistentReadDone.notify(); + ++CallbackCount; + }); + S.runWithPreamble("ConsistentRead", Path, TUScheduler::Consistent, + [&](Expected<InputsAndPreamble> Pre) { + ASSERT_TRUE(bool(Pre)); + EXPECT_THAT(includes(Pre->Preamble), + ElementsAre("<B>")); + ++CallbackCount; + }); + } + EXPECT_EQ(2, CallbackCount); +} + +TEST_F(TUSchedulerTests, Cancellation) { + // We have the following update/read sequence + // U0 + // U1(WantDiags=Yes) <-- cancelled + // R1 <-- cancelled + // U2(WantDiags=Yes) <-- cancelled + // R2A <-- cancelled + // R2B + // U3(WantDiags=Yes) + // R3 <-- cancelled + std::vector<std::string> DiagsSeen, ReadsSeen, ReadsCanceled; + { + Notification Proceed; // Ensure we schedule everything. + TUScheduler S( + CDB, getDefaultAsyncThreadsCount(), /*StorePreamblesInMemory=*/true, + /*ASTCallbacks=*/captureDiags(), + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + auto Path = testPath("foo.cpp"); + // Helper to schedule a named update and return a function to cancel it. + auto Update = [&](std::string ID) -> Canceler { + auto T = cancelableTask(); + WithContext C(std::move(T.first)); + updateWithDiags( + S, Path, "//" + ID, WantDiagnostics::Yes, + [&, ID](std::vector<Diag> Diags) { DiagsSeen.push_back(ID); }); + return std::move(T.second); + }; + // Helper to schedule a named read and return a function to cancel it. + auto Read = [&](std::string ID) -> Canceler { + auto T = cancelableTask(); + WithContext C(std::move(T.first)); + S.runWithAST(ID, Path, [&, ID](llvm::Expected<InputsAndAST> E) { + if (auto Err = E.takeError()) { + if (Err.isA<CancelledError>()) { + ReadsCanceled.push_back(ID); + consumeError(std::move(Err)); + } else { + ADD_FAILURE() << "Non-cancelled error for " << ID << ": " + << llvm::toString(std::move(Err)); + } + } else { + ReadsSeen.push_back(ID); + } + }); + return std::move(T.second); + }; + + updateWithCallback(S, Path, "", WantDiagnostics::Yes, + [&]() { Proceed.wait(); }); + // The second parens indicate cancellation, where present. + Update("U1")(); + Read("R1")(); + Update("U2")(); + Read("R2A")(); + Read("R2B"); + Update("U3"); + Read("R3")(); + Proceed.notify(); + + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + } + EXPECT_THAT(DiagsSeen, ElementsAre("U2", "U3")) + << "U1 and all dependent reads were cancelled. " + "U2 has a dependent read R2A. " + "U3 was not cancelled."; + EXPECT_THAT(ReadsSeen, ElementsAre("R2B")) + << "All reads other than R2B were cancelled"; + EXPECT_THAT(ReadsCanceled, ElementsAre("R1", "R2A", "R3")) + << "All reads other than R2B were cancelled"; +} + +TEST_F(TUSchedulerTests, ManyUpdates) { + const int FilesCount = 3; + const int UpdatesPerFile = 10; + + std::mutex Mut; + int TotalASTReads = 0; + int TotalPreambleReads = 0; + int TotalUpdates = 0; + + // Run TUScheduler and collect some stats. + { + TUScheduler S(CDB, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, captureDiags(), + /*UpdateDebounce=*/std::chrono::milliseconds(50), + ASTRetentionPolicy()); + + std::vector<std::string> Files; + for (int I = 0; I < FilesCount; ++I) { + std::string Name = "foo" + std::to_string(I) + ".cpp"; + Files.push_back(testPath(Name)); + this->Files[Files.back()] = ""; + } + + StringRef Contents1 = R"cpp(int a;)cpp"; + StringRef Contents2 = R"cpp(int main() { return 1; })cpp"; + StringRef Contents3 = R"cpp(int a; int b; int sum() { return a + b; })cpp"; + + StringRef AllContents[] = {Contents1, Contents2, Contents3}; + const int AllContentsSize = 3; + + // Scheduler may run tasks asynchronously, but should propagate the context. + // We stash a nonce in the context, and verify it in the task. + static Key<int> NonceKey; + int Nonce = 0; + + for (int FileI = 0; FileI < FilesCount; ++FileI) { + for (int UpdateI = 0; UpdateI < UpdatesPerFile; ++UpdateI) { + auto Contents = AllContents[(FileI + UpdateI) % AllContentsSize]; + + auto File = Files[FileI]; + auto Inputs = getInputs(File, Contents.str()); + { + WithContextValue WithNonce(NonceKey, ++Nonce); + updateWithDiags( + S, File, Inputs, WantDiagnostics::Auto, + [File, Nonce, &Mut, &TotalUpdates](std::vector<Diag>) { + EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce)); + + std::lock_guard<std::mutex> Lock(Mut); + ++TotalUpdates; + EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext()); + }); + } + { + WithContextValue WithNonce(NonceKey, ++Nonce); + S.runWithAST( + "CheckAST", File, + [File, Inputs, Nonce, &Mut, + &TotalASTReads](Expected<InputsAndAST> AST) { + EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce)); + + ASSERT_TRUE((bool)AST); + EXPECT_EQ(AST->Inputs.FS, Inputs.FS); + EXPECT_EQ(AST->Inputs.Contents, Inputs.Contents); + + std::lock_guard<std::mutex> Lock(Mut); + ++TotalASTReads; + EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext()); + }); + } + + { + WithContextValue WithNonce(NonceKey, ++Nonce); + S.runWithPreamble( + "CheckPreamble", File, TUScheduler::Stale, + [File, Inputs, Nonce, &Mut, + &TotalPreambleReads](Expected<InputsAndPreamble> Preamble) { + EXPECT_THAT(Context::current().get(NonceKey), Pointee(Nonce)); + + ASSERT_TRUE((bool)Preamble); + EXPECT_EQ(Preamble->Contents, Inputs.Contents); + + std::lock_guard<std::mutex> Lock(Mut); + ++TotalPreambleReads; + EXPECT_EQ(File, *TUScheduler::getFileBeingProcessedInContext()); + }); + } + } + } + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + } // TUScheduler destructor waits for all operations to finish. + + std::lock_guard<std::mutex> Lock(Mut); + EXPECT_EQ(TotalUpdates, FilesCount * UpdatesPerFile); + EXPECT_EQ(TotalASTReads, FilesCount * UpdatesPerFile); + EXPECT_EQ(TotalPreambleReads, FilesCount * UpdatesPerFile); +} + +TEST_F(TUSchedulerTests, EvictedAST) { + std::atomic<int> BuiltASTCounter(0); + ASTRetentionPolicy Policy; + Policy.MaxRetainedASTs = 2; + TUScheduler S(CDB, + /*AsyncThreadsCount=*/1, /*StorePreambleInMemory=*/true, + /*ASTCallbacks=*/nullptr, + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + Policy); + + llvm::StringLiteral SourceContents = R"cpp( + int* a; + double* b = a; + )cpp"; + llvm::StringLiteral OtherSourceContents = R"cpp( + int* a; + double* b = a + 0; + )cpp"; + + auto Foo = testPath("foo.cpp"); + auto Bar = testPath("bar.cpp"); + auto Baz = testPath("baz.cpp"); + + // Build one file in advance. We will not access it later, so it will be the + // one that the cache will evict. + updateWithCallback(S, Foo, SourceContents, WantDiagnostics::Yes, + [&BuiltASTCounter]() { ++BuiltASTCounter; }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + ASSERT_EQ(BuiltASTCounter.load(), 1); + + // Build two more files. Since we can retain only 2 ASTs, these should be the + // ones we see in the cache later. + updateWithCallback(S, Bar, SourceContents, WantDiagnostics::Yes, + [&BuiltASTCounter]() { ++BuiltASTCounter; }); + updateWithCallback(S, Baz, SourceContents, WantDiagnostics::Yes, + [&BuiltASTCounter]() { ++BuiltASTCounter; }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + ASSERT_EQ(BuiltASTCounter.load(), 3); + + // Check only the last two ASTs are retained. + ASSERT_THAT(S.getFilesWithCachedAST(), UnorderedElementsAre(Bar, Baz)); + + // Access the old file again. + updateWithCallback(S, Foo, OtherSourceContents, WantDiagnostics::Yes, + [&BuiltASTCounter]() { ++BuiltASTCounter; }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + ASSERT_EQ(BuiltASTCounter.load(), 4); + + // Check the AST for foo.cpp is retained now and one of the others got + // evicted. + EXPECT_THAT(S.getFilesWithCachedAST(), + UnorderedElementsAre(Foo, AnyOf(Bar, Baz))); +} + +TEST_F(TUSchedulerTests, EmptyPreamble) { + TUScheduler S(CDB, + /*AsyncThreadsCount=*/4, /*StorePreambleInMemory=*/true, + /*ASTCallbacks=*/nullptr, + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + + auto Foo = testPath("foo.cpp"); + auto Header = testPath("foo.h"); + + Files[Header] = "void foo()"; + Timestamps[Header] = time_t(0); + auto WithPreamble = R"cpp( + #include "foo.h" + int main() {} + )cpp"; + auto WithEmptyPreamble = R"cpp(int main() {})cpp"; + S.update(Foo, getInputs(Foo, WithPreamble), WantDiagnostics::Auto); + S.runWithPreamble( + "getNonEmptyPreamble", Foo, TUScheduler::Stale, + [&](Expected<InputsAndPreamble> Preamble) { + // We expect to get a non-empty preamble. + EXPECT_GT( + cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size, + 0u); + }); + // Wait for the preamble is being built. + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + + // Update the file which results in an empty preamble. + S.update(Foo, getInputs(Foo, WithEmptyPreamble), WantDiagnostics::Auto); + // Wait for the preamble is being built. + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + S.runWithPreamble( + "getEmptyPreamble", Foo, TUScheduler::Stale, + [&](Expected<InputsAndPreamble> Preamble) { + // We expect to get an empty preamble. + EXPECT_EQ( + cantFail(std::move(Preamble)).Preamble->Preamble.getBounds().Size, + 0u); + }); +} + +TEST_F(TUSchedulerTests, RunWaitsForPreamble) { + // Testing strategy: we update the file and schedule a few preamble reads at + // the same time. All reads should get the same non-null preamble. + TUScheduler S(CDB, + /*AsyncThreadsCount=*/4, /*StorePreambleInMemory=*/true, + /*ASTCallbacks=*/nullptr, + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + auto Foo = testPath("foo.cpp"); + auto NonEmptyPreamble = R"cpp( + #define FOO 1 + #define BAR 2 + + int main() {} + )cpp"; + constexpr int ReadsToSchedule = 10; + std::mutex PreamblesMut; + std::vector<const void *> Preambles(ReadsToSchedule, nullptr); + S.update(Foo, getInputs(Foo, NonEmptyPreamble), WantDiagnostics::Auto); + for (int I = 0; I < ReadsToSchedule; ++I) { + S.runWithPreamble( + "test", Foo, TUScheduler::Stale, + [I, &PreamblesMut, &Preambles](Expected<InputsAndPreamble> IP) { + std::lock_guard<std::mutex> Lock(PreamblesMut); + Preambles[I] = cantFail(std::move(IP)).Preamble; + }); + } + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + // Check all actions got the same non-null preamble. + std::lock_guard<std::mutex> Lock(PreamblesMut); + ASSERT_NE(Preambles[0], nullptr); + ASSERT_THAT(Preambles, Each(Preambles[0])); +} + +TEST_F(TUSchedulerTests, NoopOnEmptyChanges) { + TUScheduler S(CDB, + /*AsyncThreadsCount=*/getDefaultAsyncThreadsCount(), + /*StorePreambleInMemory=*/true, captureDiags(), + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + + auto Source = testPath("foo.cpp"); + auto Header = testPath("foo.h"); + + Files[Header] = "int a;"; + Timestamps[Header] = time_t(0); + + auto SourceContents = R"cpp( + #include "foo.h" + int b = a; + )cpp"; + + // Return value indicates if the updated callback was received. + auto DoUpdate = [&](std::string Contents) -> bool { + std::atomic<bool> Updated(false); + Updated = false; + updateWithDiags(S, Source, Contents, WantDiagnostics::Yes, + [&Updated](std::vector<Diag>) { Updated = true; }); + bool UpdateFinished = S.blockUntilIdle(timeoutSeconds(10)); + if (!UpdateFinished) + ADD_FAILURE() << "Updated has not finished in one second. Threading bug?"; + return Updated; + }; + + // Test that subsequent updates with the same inputs do not cause rebuilds. + ASSERT_TRUE(DoUpdate(SourceContents)); + ASSERT_FALSE(DoUpdate(SourceContents)); + + // Update to a header should cause a rebuild, though. + Timestamps[Header] = time_t(1); + ASSERT_TRUE(DoUpdate(SourceContents)); + ASSERT_FALSE(DoUpdate(SourceContents)); + + // Update to the contents should cause a rebuild. + auto OtherSourceContents = R"cpp( + #include "foo.h" + int c = d; + )cpp"; + ASSERT_TRUE(DoUpdate(OtherSourceContents)); + ASSERT_FALSE(DoUpdate(OtherSourceContents)); + + // Update to the compile commands should also cause a rebuild. + CDB.ExtraClangFlags.push_back("-DSOMETHING"); + ASSERT_TRUE(DoUpdate(OtherSourceContents)); + ASSERT_FALSE(DoUpdate(OtherSourceContents)); +} + +TEST_F(TUSchedulerTests, NoChangeDiags) { + TUScheduler S(CDB, + /*AsyncThreadsCount=*/getDefaultAsyncThreadsCount(), + /*StorePreambleInMemory=*/true, captureDiags(), + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + + auto FooCpp = testPath("foo.cpp"); + auto Contents = "int a; int b;"; + + updateWithDiags( + S, FooCpp, Contents, WantDiagnostics::No, + [](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; }); + S.runWithAST("touchAST", FooCpp, [](Expected<InputsAndAST> IA) { + // Make sure the AST was actually built. + cantFail(std::move(IA)); + }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + + // Even though the inputs didn't change and AST can be reused, we need to + // report the diagnostics, as they were not reported previously. + std::atomic<bool> SeenDiags(false); + updateWithDiags(S, FooCpp, Contents, WantDiagnostics::Auto, + [&](std::vector<Diag>) { SeenDiags = true; }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + ASSERT_TRUE(SeenDiags); + + // Subsequent request does not get any diagnostics callback because the same + // diags have previously been reported and the inputs didn't change. + updateWithDiags( + S, FooCpp, Contents, WantDiagnostics::Auto, + [&](std::vector<Diag>) { ADD_FAILURE() << "Should not be called."; }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); +} + +TEST_F(TUSchedulerTests, Run) { + TUScheduler S(CDB, /*AsyncThreadsCount=*/getDefaultAsyncThreadsCount(), + /*StorePreambleInMemory=*/true, /*ASTCallbacks=*/nullptr, + /*UpdateDebounce=*/std::chrono::steady_clock::duration::zero(), + ASTRetentionPolicy()); + std::atomic<int> Counter(0); + S.run("add 1", [&] { ++Counter; }); + S.run("add 2", [&] { Counter += 2; }); + ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10))); + EXPECT_EQ(Counter.load(), 3); +} + +TEST_F(TUSchedulerTests, TUStatus) { + class CaptureTUStatus : public DiagnosticsConsumer { + public: + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override {} + + void onFileUpdated(PathRef File, const TUStatus &Status) override { + std::lock_guard<std::mutex> Lock(Mutex); + AllStatus.push_back(Status); + } + + std::vector<TUStatus> AllStatus; + + private: + std::mutex Mutex; + } CaptureTUStatus; + MockFSProvider FS; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, CaptureTUStatus, ClangdServer::optsForTest()); + Annotations Code("int m^ain () {}"); + + // We schedule the following tasks in the queue: + // [Update] [GoToDefinition] + Server.addDocument(testPath("foo.cpp"), Code.code(), WantDiagnostics::Yes); + Server.locateSymbolAt(testPath("foo.cpp"), Code.point(), + [](Expected<std::vector<LocatedSymbol>> Result) { + ASSERT_TRUE((bool)Result); + }); + + ASSERT_TRUE(Server.blockUntilIdleForTest()); + + EXPECT_THAT(CaptureTUStatus.AllStatus, + ElementsAre( + // Statuses of "Update" action. + TUState(TUAction::RunningAction, "Update"), + TUState(TUAction::BuildingPreamble, "Update"), + TUState(TUAction::BuildingFile, "Update"), + + // Statuses of "Definitions" action + TUState(TUAction::RunningAction, "Definitions"), + TUState(TUAction::Idle, /*No action*/ ""))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TestFS.cpp b/clangd/unittests/TestFS.cpp new file mode 100644 index 00000000..c5b2613f --- /dev/null +++ b/clangd/unittests/TestFS.cpp @@ -0,0 +1,129 @@ +//===-- TestFS.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 "TestFS.h" +#include "URI.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Path.h" + +namespace clang { +namespace clangd { + +llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> +buildTestFS(llvm::StringMap<std::string> const &Files, + llvm::StringMap<time_t> const &Timestamps) { + llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> MemFS( + new llvm::vfs::InMemoryFileSystem); + MemFS->setCurrentWorkingDirectory(testRoot()); + for (auto &FileAndContents : Files) { + llvm::StringRef File = FileAndContents.first(); + MemFS->addFile( + File, Timestamps.lookup(File), + llvm::MemoryBuffer::getMemBufferCopy(FileAndContents.second, File)); + } + return MemFS; +} + +MockCompilationDatabase::MockCompilationDatabase(llvm::StringRef Directory, + llvm::StringRef RelPathPrefix) + : ExtraClangFlags({"-ffreestanding"}), Directory(Directory), + RelPathPrefix(RelPathPrefix) { + // -ffreestanding avoids implicit stdc-predef.h. +} + +llvm::Optional<tooling::CompileCommand> +MockCompilationDatabase::getCompileCommand(PathRef File, + ProjectInfo *Project) const { + if (ExtraClangFlags.empty()) + return None; + + auto FileName = llvm::sys::path::filename(File); + + // Build the compile command. + auto CommandLine = ExtraClangFlags; + CommandLine.insert(CommandLine.begin(), "clang"); + if (RelPathPrefix.empty()) { + // Use the absolute path in the compile command. + CommandLine.push_back(File); + } else { + // Build a relative path using RelPathPrefix. + llvm::SmallString<32> RelativeFilePath(RelPathPrefix); + llvm::sys::path::append(RelativeFilePath, FileName); + CommandLine.push_back(RelativeFilePath.str()); + } + + if (Project) + Project->SourceRoot = Directory; + return {tooling::CompileCommand(Directory != llvm::StringRef() + ? Directory + : llvm::sys::path::parent_path(File), + FileName, std::move(CommandLine), "")}; +} + +const char *testRoot() { +#ifdef _WIN32 + return "C:\\clangd-test"; +#else + return "/clangd-test"; +#endif +} + +std::string testPath(PathRef File) { + assert(llvm::sys::path::is_relative(File) && "FileName should be relative"); + + llvm::SmallString<32> NativeFile = File; + llvm::sys::path::native(NativeFile); + llvm::SmallString<32> Path; + llvm::sys::path::append(Path, testRoot(), NativeFile); + return Path.str(); +} + +/// unittest: is a scheme that refers to files relative to testRoot(). +/// URI body is a path relative to testRoot() e.g. unittest:///x.h for +/// /clangd-test/x.h. +class TestScheme : public URIScheme { +public: + static const char *Scheme; + + llvm::Expected<std::string> + getAbsolutePath(llvm::StringRef /*Authority*/, llvm::StringRef Body, + llvm::StringRef HintPath) const override { + if (!HintPath.startswith(testRoot())) + return llvm::make_error<llvm::StringError>( + "Hint path doesn't start with test root: " + HintPath, + llvm::inconvertibleErrorCode()); + if (!Body.consume_front("/")) + return llvm::make_error<llvm::StringError>( + "Body of an unittest: URI must start with '/'", + llvm::inconvertibleErrorCode()); + llvm::SmallString<16> Path(Body.begin(), Body.end()); + llvm::sys::path::native(Path); + return testPath(Path); + } + + llvm::Expected<URI> + uriFromAbsolutePath(llvm::StringRef AbsolutePath) const override { + llvm::StringRef Body = AbsolutePath; + if (!Body.consume_front(testRoot())) + return llvm::make_error<llvm::StringError>( + AbsolutePath + "does not start with " + testRoot(), + llvm::inconvertibleErrorCode()); + + return URI(Scheme, /*Authority=*/"", + llvm::sys::path::convert_to_slash(Body)); + } +}; + +const char *TestScheme::Scheme = "unittest"; + +static URISchemeRegistry::Add<TestScheme> X(TestScheme::Scheme, "Test schema"); + +volatile int UnittestSchemeAnchorSource = 0; + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TestFS.h b/clangd/unittests/TestFS.h new file mode 100644 index 00000000..eabdddf7 --- /dev/null +++ b/clangd/unittests/TestFS.h @@ -0,0 +1,73 @@ +//===-- TestFS.h ------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// +// +// Allows setting up fake filesystem environments for tests. +// +//===----------------------------------------------------------------------===// +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTFS_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTFS_H +#include "ClangdServer.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/VirtualFileSystem.h" + +namespace clang { +namespace clangd { + +// Builds a VFS that provides access to the provided files, plus temporary +// directories. +llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> +buildTestFS(llvm::StringMap<std::string> const &Files, + llvm::StringMap<time_t> const &Timestamps = {}); + +// A VFS provider that returns TestFSes containing a provided set of files. +class MockFSProvider : public FileSystemProvider { +public: + IntrusiveRefCntPtr<llvm::vfs::FileSystem> getFileSystem() const override { + return buildTestFS(Files); + } + + // If relative paths are used, they are resolved with testPath(). + llvm::StringMap<std::string> Files; +}; + +// A Compilation database that returns a fixed set of compile flags. +class MockCompilationDatabase : public GlobalCompilationDatabase { +public: + /// If \p Directory is not empty, use that as the Directory field of the + /// CompileCommand, and as project SourceRoot. + /// + /// If \p RelPathPrefix is not empty, use that as a prefix in front of the + /// source file name, instead of using an absolute path. + MockCompilationDatabase(StringRef Directory = StringRef(), + StringRef RelPathPrefix = StringRef()); + + llvm::Optional<tooling::CompileCommand> + getCompileCommand(PathRef File, ProjectInfo * = nullptr) const override; + + std::vector<std::string> ExtraClangFlags; + +private: + StringRef Directory; + StringRef RelPathPrefix; +}; + +// Returns an absolute (fake) test directory for this OS. +const char *testRoot(); + +// Returns a suitable absolute path for this OS. +std::string testPath(PathRef File); + +// unittest: is a scheme that refers to files relative to testRoot() +// This anchor is used to force the linker to link in the generated object file +// and thus register unittest: URI scheme plugin. +extern volatile int UnittestSchemeAnchorSource; + +} // namespace clangd +} // namespace clang +#endif diff --git a/clangd/unittests/TestIndex.cpp b/clangd/unittests/TestIndex.cpp new file mode 100644 index 00000000..11ac4239 --- /dev/null +++ b/clangd/unittests/TestIndex.cpp @@ -0,0 +1,118 @@ +//===-- TestIndex.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 "TestIndex.h" +#include "clang/Index/IndexSymbol.h" +#include "llvm/Support/Regex.h" + +namespace clang { +namespace clangd { + +Symbol symbol(llvm::StringRef QName) { + Symbol Sym; + Sym.ID = SymbolID(QName.str()); + size_t Pos = QName.rfind("::"); + if (Pos == llvm::StringRef::npos) { + Sym.Name = QName; + Sym.Scope = ""; + } else { + Sym.Name = QName.substr(Pos + 2); + Sym.Scope = QName.substr(0, Pos + 2); + } + return Sym; +} + +static std::string replace(llvm::StringRef Haystack, llvm::StringRef Needle, + llvm::StringRef Repl) { + llvm::SmallVector<llvm::StringRef, 8> Parts; + Haystack.split(Parts, Needle); + return llvm::join(Parts, Repl); +} + +// Helpers to produce fake index symbols for memIndex() or completions(). +// USRFormat is a regex replacement string for the unqualified part of the USR. +Symbol sym(llvm::StringRef QName, index::SymbolKind Kind, + llvm::StringRef USRFormat) { + Symbol Sym; + std::string USR = "c:"; // We synthesize a few simple cases of USRs by hand! + size_t Pos = QName.rfind("::"); + if (Pos == llvm::StringRef::npos) { + Sym.Name = QName; + Sym.Scope = ""; + } else { + Sym.Name = QName.substr(Pos + 2); + Sym.Scope = QName.substr(0, Pos + 2); + USR += "@N@" + replace(QName.substr(0, Pos), "::", "@N@"); // ns:: -> @N@ns + } + USR += llvm::Regex("^.*$").sub(USRFormat, Sym.Name); // e.g. func -> @F@func# + Sym.ID = SymbolID(USR); + Sym.SymInfo.Kind = Kind; + Sym.Flags |= Symbol::IndexedForCodeCompletion; + Sym.Origin = SymbolOrigin::Static; + return Sym; +} + +Symbol func(llvm::StringRef Name) { // Assumes the function has no args. + return sym(Name, index::SymbolKind::Function, "@F@\\0#"); // no args +} + +Symbol cls(llvm::StringRef Name) { + return sym(Name, index::SymbolKind::Class, "@S@\\0"); +} + +Symbol var(llvm::StringRef Name) { + return sym(Name, index::SymbolKind::Variable, "@\\0"); +} + +Symbol ns(llvm::StringRef Name) { + return sym(Name, index::SymbolKind::Namespace, "@N@\\0"); +} + +SymbolSlab generateSymbols(std::vector<std::string> QualifiedNames) { + SymbolSlab::Builder Slab; + for (llvm::StringRef QName : QualifiedNames) + Slab.insert(symbol(QName)); + return std::move(Slab).build(); +} + +SymbolSlab generateNumSymbols(int Begin, int End) { + std::vector<std::string> Names; + for (int i = Begin; i <= End; i++) + Names.push_back(std::to_string(i)); + return generateSymbols(Names); +} + +std::string getQualifiedName(const Symbol &Sym) { + return (Sym.Scope + Sym.Name + Sym.TemplateSpecializationArgs).str(); +} + +std::vector<std::string> match(const SymbolIndex &I, + const FuzzyFindRequest &Req, bool *Incomplete) { + std::vector<std::string> Matches; + bool IsIncomplete = I.fuzzyFind(Req, [&](const Symbol &Sym) { + Matches.push_back(clang::clangd::getQualifiedName(Sym)); + }); + if (Incomplete) + *Incomplete = IsIncomplete; + return Matches; +} + +// Returns qualified names of symbols with any of IDs in the index. +std::vector<std::string> lookup(const SymbolIndex &I, + llvm::ArrayRef<SymbolID> IDs) { + LookupRequest Req; + Req.IDs.insert(IDs.begin(), IDs.end()); + std::vector<std::string> Results; + I.lookup(Req, [&](const Symbol &Sym) { + Results.push_back(getQualifiedName(Sym)); + }); + return Results; +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TestIndex.h b/clangd/unittests/TestIndex.h new file mode 100644 index 00000000..01de089e --- /dev/null +++ b/clangd/unittests/TestIndex.h @@ -0,0 +1,57 @@ +//===-- IndexHelpers.h ------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_INDEXTESTCOMMON_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_INDEXTESTCOMMON_H + +#include "index/Index.h" +#include "index/Merge.h" + +namespace clang { +namespace clangd { + +// Creates Symbol instance and sets SymbolID to given QualifiedName. +Symbol symbol(llvm::StringRef QName); + +// Helpers to produce fake index symbols with proper SymbolID. +// USRFormat is a regex replacement string for the unqualified part of the USR. +Symbol sym(llvm::StringRef QName, index::SymbolKind Kind, + llvm::StringRef USRFormat); +// Creats a function symbol assuming no function arg. +Symbol func(llvm::StringRef Name); +// Creates a class symbol. +Symbol cls(llvm::StringRef Name); +// Creates a variable symbol. +Symbol var(llvm::StringRef Name); +// Creates a namespace symbol. +Symbol ns(llvm::StringRef Name); + +// Create a slab of symbols with the given qualified names as IDs and names. +SymbolSlab generateSymbols(std::vector<std::string> QualifiedNames); + +// Create a slab of symbols with IDs and names [Begin, End]. +SymbolSlab generateNumSymbols(int Begin, int End); + +// Returns fully-qualified name out of given symbol. +std::string getQualifiedName(const Symbol &Sym); + +// Performs fuzzy matching-based symbol lookup given a query and an index. +// Incomplete is set true if more items than requested can be retrieved, false +// otherwise. +std::vector<std::string> match(const SymbolIndex &I, + const FuzzyFindRequest &Req, + bool *Incomplete = nullptr); + +// Returns qualified names of symbols with any of IDs in the index. +std::vector<std::string> lookup(const SymbolIndex &I, + llvm::ArrayRef<SymbolID> IDs); + +} // namespace clangd +} // namespace clang + +#endif diff --git a/clangd/unittests/TestScheme.h b/clangd/unittests/TestScheme.h new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/clangd/unittests/TestScheme.h diff --git a/clangd/unittests/TestTU.cpp b/clangd/unittests/TestTU.cpp new file mode 100644 index 00000000..8f48eab5 --- /dev/null +++ b/clangd/unittests/TestTU.cpp @@ -0,0 +1,157 @@ +//===--- TestTU.cpp - Scratch source files for testing --------------------===// +// +// 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 "TestTU.h" +#include "TestFS.h" +#include "index/FileIndex.h" +#include "index/MemIndex.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/Utils.h" + +namespace clang { +namespace clangd { + +ParsedAST TestTU::build() const { + std::string FullFilename = testPath(Filename), + FullHeaderName = testPath(HeaderFilename), + ImportThunk = testPath("import_thunk.h"); + // We want to implicitly include HeaderFilename without messing up offsets. + // -include achieves this, but sometimes we want #import (to simulate a header + // guard without messing up offsets). In this case, use an intermediate file. + std::string ThunkContents = "#import \"" + FullHeaderName + "\"\n"; + + llvm::StringMap<std::string> Files(AdditionalFiles); + Files[FullFilename] = Code; + Files[FullHeaderName] = HeaderCode; + Files[ImportThunk] = ThunkContents; + + std::vector<const char *> Cmd = {"clang", FullFilename.c_str()}; + // FIXME: this shouldn't need to be conditional, but it breaks a + // GoToDefinition test for some reason (getMacroArgExpandedLocation fails). + if (!HeaderCode.empty()) { + Cmd.push_back("-include"); + Cmd.push_back(ImplicitHeaderGuard ? ImportThunk.c_str() + : FullHeaderName.c_str()); + } + Cmd.insert(Cmd.end(), ExtraArgs.begin(), ExtraArgs.end()); + ParseInputs Inputs; + Inputs.CompileCommand.Filename = FullFilename; + Inputs.CompileCommand.CommandLine = {Cmd.begin(), Cmd.end()}; + Inputs.CompileCommand.Directory = testRoot(); + Inputs.Contents = Code; + Inputs.FS = buildTestFS(Files); + Inputs.Opts = ParseOptions(); + Inputs.Opts.ClangTidyOpts.Checks = ClangTidyChecks; + Inputs.Index = ExternalIndex; + if (Inputs.Index) + Inputs.Opts.SuggestMissingIncludes = true; + auto CI = buildCompilerInvocation(Inputs); + assert(CI && "Failed to build compilation invocation."); + auto Preamble = + buildPreamble(FullFilename, *CI, + /*OldPreamble=*/nullptr, + /*OldCompileCommand=*/Inputs.CompileCommand, Inputs, + /*StoreInMemory=*/true, /*PreambleCallback=*/nullptr); + auto AST = buildAST(FullFilename, createInvocationFromCommandLine(Cmd), + Inputs, Preamble); + if (!AST.hasValue()) { + ADD_FAILURE() << "Failed to build code:\n" << Code; + llvm_unreachable("Failed to build TestTU!"); + } + return std::move(*AST); +} + +SymbolSlab TestTU::headerSymbols() const { + auto AST = build(); + return indexHeaderSymbols(AST.getASTContext(), AST.getPreprocessorPtr(), + AST.getCanonicalIncludes()); +} + +std::unique_ptr<SymbolIndex> TestTU::index() const { + auto AST = build(); + auto Idx = llvm::make_unique<FileIndex>(/*UseDex=*/true); + Idx->updatePreamble(Filename, AST.getASTContext(), AST.getPreprocessorPtr(), + AST.getCanonicalIncludes()); + Idx->updateMain(Filename, AST); + return std::move(Idx); +} + +const Symbol &findSymbol(const SymbolSlab &Slab, llvm::StringRef QName) { + const Symbol *Result = nullptr; + for (const Symbol &S : Slab) { + if (QName != (S.Scope + S.Name).str()) + continue; + if (Result) { + ADD_FAILURE() << "Multiple symbols named " << QName << ":\n" + << *Result << "\n---\n" + << S; + assert(false && "QName is not unique"); + } + Result = &S; + } + if (!Result) { + ADD_FAILURE() << "No symbol named " << QName << " in " + << ::testing::PrintToString(Slab); + assert(false && "No symbol with QName"); + } + return *Result; +} + +const NamedDecl &findDecl(ParsedAST &AST, llvm::StringRef QName) { + llvm::SmallVector<llvm::StringRef, 4> Components; + QName.split(Components, "::"); + + auto &Ctx = AST.getASTContext(); + auto LookupDecl = [&Ctx](const DeclContext &Scope, + llvm::StringRef Name) -> const NamedDecl & { + auto LookupRes = Scope.lookup(DeclarationName(&Ctx.Idents.get(Name))); + assert(!LookupRes.empty() && "Lookup failed"); + assert(LookupRes.size() == 1 && "Lookup returned multiple results"); + return *LookupRes.front(); + }; + + const DeclContext *Scope = Ctx.getTranslationUnitDecl(); + for (auto NameIt = Components.begin(), End = Components.end() - 1; + NameIt != End; ++NameIt) { + Scope = &cast<DeclContext>(LookupDecl(*Scope, *NameIt)); + } + return LookupDecl(*Scope, Components.back()); +} + +const NamedDecl &findDecl(ParsedAST &AST, + std::function<bool(const NamedDecl &)> Filter) { + struct Visitor : RecursiveASTVisitor<Visitor> { + decltype(Filter) F; + llvm::SmallVector<const NamedDecl *, 1> Decls; + bool VisitNamedDecl(const NamedDecl *ND) { + if (F(*ND)) + Decls.push_back(ND); + return true; + } + } Visitor; + Visitor.F = Filter; + Visitor.TraverseDecl(AST.getASTContext().getTranslationUnitDecl()); + if (Visitor.Decls.size() != 1) { + ADD_FAILURE() << Visitor.Decls.size() << " symbols matched."; + assert(Visitor.Decls.size() == 1); + } + return *Visitor.Decls.front(); +} + +const NamedDecl &findUnqualifiedDecl(ParsedAST &AST, llvm::StringRef Name) { + return findDecl(AST, [Name](const NamedDecl &ND) { + if (auto *ID = ND.getIdentifier()) + if (ID->getName() == Name) + return true; + return false; + }); +} + +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TestTU.h b/clangd/unittests/TestTU.h new file mode 100644 index 00000000..6ac4c86a --- /dev/null +++ b/clangd/unittests/TestTU.h @@ -0,0 +1,84 @@ +//===--- TestTU.h - Scratch source files for testing -------------*- 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 +// +//===----------------------------------------------------------------------===// +// +// Many tests for indexing, code completion etc are most naturally expressed +// using code examples. +// TestTU lets test define these examples in a common way without dealing with +// the mechanics of VFS and compiler interactions, and then easily grab the +// AST, particular symbols, etc. +// +//===---------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTTU_H +#define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTTU_H + +#include "ClangdUnit.h" +#include "Path.h" +#include "index/Index.h" +#include "llvm/ADT/StringMap.h" +#include "gtest/gtest.h" +#include <string> +#include <utility> +#include <vector> + +namespace clang { +namespace clangd { + +struct TestTU { + static TestTU withCode(llvm::StringRef Code) { + TestTU TU; + TU.Code = Code; + return TU; + } + + static TestTU withHeaderCode(llvm::StringRef HeaderCode) { + TestTU TU; + TU.HeaderCode = HeaderCode; + return TU; + } + + // The code to be compiled. + std::string Code; + std::string Filename = "TestTU.cpp"; + + // Define contents of a header which will be implicitly included by Code. + std::string HeaderCode; + std::string HeaderFilename = "TestTU.h"; + + // Name and contents of each file. + llvm::StringMap<std::string> AdditionalFiles; + + // Extra arguments for the compiler invocation. + std::vector<const char *> ExtraArgs; + + llvm::Optional<std::string> ClangTidyChecks; + // Index to use when building AST. + const SymbolIndex *ExternalIndex = nullptr; + + // Simulate a header guard of the header (using an #import directive). + bool ImplicitHeaderGuard = true; + + ParsedAST build() const; + SymbolSlab headerSymbols() const; + std::unique_ptr<SymbolIndex> index() const; +}; + +// Look up an index symbol by qualified name, which must be unique. +const Symbol &findSymbol(const SymbolSlab &, llvm::StringRef QName); +// Look up an AST symbol by qualified name, which must be unique and top-level. +const NamedDecl &findDecl(ParsedAST &AST, llvm::StringRef QName); +// Look up an AST symbol that satisfies \p Filter. +const NamedDecl &findDecl(ParsedAST &AST, + std::function<bool(const NamedDecl &)> Filter); +// Look up an AST symbol by unqualified name, which must be unique. +const NamedDecl &findUnqualifiedDecl(ParsedAST &AST, llvm::StringRef Name); + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTTU_H diff --git a/clangd/unittests/ThreadingTests.cpp b/clangd/unittests/ThreadingTests.cpp new file mode 100644 index 00000000..18b9146e --- /dev/null +++ b/clangd/unittests/ThreadingTests.cpp @@ -0,0 +1,64 @@ +//===-- ThreadingTests.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 "Threading.h" +#include "gtest/gtest.h" +#include <mutex> + +namespace clang { +namespace clangd { +class ThreadingTest : public ::testing::Test {}; + +TEST_F(ThreadingTest, TaskRunner) { + const int TasksCnt = 100; + // This should be const, but MSVC does not allow to use const vars in lambdas + // without capture. On the other hand, clang gives a warning that capture of + // const var is not required. + // Making it non-const makes both compilers happy. + int IncrementsPerTask = 1000; + + std::mutex Mutex; + int Counter(0); /* GUARDED_BY(Mutex) */ + { + AsyncTaskRunner Tasks; + auto scheduleIncrements = [&]() { + for (int TaskI = 0; TaskI < TasksCnt; ++TaskI) { + Tasks.runAsync("task", [&Counter, &Mutex, IncrementsPerTask]() { + for (int Increment = 0; Increment < IncrementsPerTask; ++Increment) { + std::lock_guard<std::mutex> Lock(Mutex); + ++Counter; + } + }); + } + }; + + { + // Make sure runAsync is not running tasks synchronously on the same + // thread by locking the Mutex used for increments. + std::lock_guard<std::mutex> Lock(Mutex); + scheduleIncrements(); + } + + Tasks.wait(); + { + std::lock_guard<std::mutex> Lock(Mutex); + ASSERT_EQ(Counter, TasksCnt * IncrementsPerTask); + } + + { + std::lock_guard<std::mutex> Lock(Mutex); + Counter = 0; + scheduleIncrements(); + } + } + // Check that destructor has waited for tasks to finish. + std::lock_guard<std::mutex> Lock(Mutex); + ASSERT_EQ(Counter, TasksCnt * IncrementsPerTask); +} +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TraceTests.cpp b/clangd/unittests/TraceTests.cpp new file mode 100644 index 00000000..1871e6ac --- /dev/null +++ b/clangd/unittests/TraceTests.cpp @@ -0,0 +1,127 @@ +//===-- TraceTests.cpp - Tracing unit tests ---------------------*- 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 "Trace.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/SourceMgr.h" +#include "llvm/Support/Threading.h" +#include "llvm/Support/YAMLParser.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +MATCHER_P(StringNode, Val, "") { + if (arg->getType() != llvm::yaml::Node::NK_Scalar) { + *result_listener << "is a " << arg->getVerbatimTag(); + return false; + } + llvm::SmallString<32> S; + return Val == static_cast<llvm::yaml::ScalarNode *>(arg)->getValue(S); +} + +// Checks that N is a Mapping (JS object) with the expected scalar properties. +// The object must have all the Expected properties, but may have others. +bool VerifyObject(llvm::yaml::Node &N, + std::map<std::string, std::string> Expected) { + auto *M = llvm::dyn_cast<llvm::yaml::MappingNode>(&N); + if (!M) { + ADD_FAILURE() << "Not an object"; + return false; + } + bool Match = true; + llvm::SmallString<32> Tmp; + for (auto &Prop : *M) { + auto *K = llvm::dyn_cast_or_null<llvm::yaml::ScalarNode>(Prop.getKey()); + if (!K) + continue; + std::string KS = K->getValue(Tmp).str(); + auto I = Expected.find(KS); + if (I == Expected.end()) + continue; // Ignore properties with no assertion. + + auto *V = llvm::dyn_cast_or_null<llvm::yaml::ScalarNode>(Prop.getValue()); + if (!V) { + ADD_FAILURE() << KS << " is not a string"; + Match = false; + } + std::string VS = V->getValue(Tmp).str(); + if (VS != I->second) { + ADD_FAILURE() << KS << " expected " << I->second << " but actual " << VS; + Match = false; + } + Expected.erase(I); + } + for (const auto &P : Expected) { + ADD_FAILURE() << P.first << " missing, expected " << P.second; + Match = false; + } + return Match; +} + +TEST(TraceTest, SmokeTest) { + // Capture some events. + std::string JSON; + { + llvm::raw_string_ostream OS(JSON); + auto JSONTracer = trace::createJSONTracer(OS); + trace::Session Session(*JSONTracer); + { + trace::Span Tracer("A"); + trace::log("B"); + } + } + + // Get the root JSON object using the YAML parser. + llvm::SourceMgr SM; + llvm::yaml::Stream Stream(JSON, SM); + auto Doc = Stream.begin(); + ASSERT_NE(Doc, Stream.end()); + auto *Root = llvm::dyn_cast_or_null<llvm::yaml::MappingNode>(Doc->getRoot()); + ASSERT_NE(Root, nullptr) << "Root should be an object"; + + // Check whether we expect thread name events on this platform. + llvm::SmallString<32> ThreadName; + get_thread_name(ThreadName); + bool ThreadsHaveNames = !ThreadName.empty(); + + // We expect in order: + // displayTimeUnit: "ns" + // traceEvents: [process name, thread name, start span, log, end span] + // (The order doesn't matter, but the YAML parser is awkward to use otherwise) + auto Prop = Root->begin(); + ASSERT_NE(Prop, Root->end()) << "Expected displayTimeUnit property"; + ASSERT_THAT(Prop->getKey(), StringNode("displayTimeUnit")); + EXPECT_THAT(Prop->getValue(), StringNode("ns")); + ASSERT_NE(++Prop, Root->end()) << "Expected traceEvents property"; + EXPECT_THAT(Prop->getKey(), StringNode("traceEvents")); + auto *Events = + llvm::dyn_cast_or_null<llvm::yaml::SequenceNode>(Prop->getValue()); + ASSERT_NE(Events, nullptr) << "traceEvents should be an array"; + auto Event = Events->begin(); + ASSERT_NE(Event, Events->end()) << "Expected process name"; + EXPECT_TRUE(VerifyObject(*Event, {{"ph", "M"}, {"name", "process_name"}})); + if (ThreadsHaveNames) { + ASSERT_NE(++Event, Events->end()) << "Expected thread name"; + EXPECT_TRUE(VerifyObject(*Event, {{"ph", "M"}, {"name", "thread_name"}})); + } + ASSERT_NE(++Event, Events->end()) << "Expected log message"; + EXPECT_TRUE(VerifyObject(*Event, {{"ph", "i"}, {"name", "Log"}})); + ASSERT_NE(++Event, Events->end()) << "Expected span end"; + EXPECT_TRUE(VerifyObject(*Event, {{"ph", "X"}, {"name", "A"}})); + ASSERT_EQ(++Event, Events->end()); + ASSERT_EQ(++Prop, Root->end()); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TweakTests.cpp b/clangd/unittests/TweakTests.cpp new file mode 100644 index 00000000..baa60292 --- /dev/null +++ b/clangd/unittests/TweakTests.cpp @@ -0,0 +1,190 @@ +//===-- TweakTests.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 "SourceCode.h" +#include "TestTU.h" +#include "refactor/Tweak.h" +#include "clang/AST/Expr.h" +#include "clang/Rewrite/Core/Rewriter.h" +#include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Testing/Support/Error.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <cassert> + +using llvm::Failed; +using llvm::HasValue; +using llvm::Succeeded; + +namespace clang { +namespace clangd { +namespace { + +std::string markRange(llvm::StringRef Code, Range R) { + size_t Begin = llvm::cantFail(positionToOffset(Code, R.start)); + size_t End = llvm::cantFail(positionToOffset(Code, R.end)); + assert(Begin <= End); + if (Begin == End) // Mark a single point. + return (Code.substr(0, Begin) + "^" + Code.substr(Begin)).str(); + // Mark a range. + return (Code.substr(0, Begin) + "[[" + Code.substr(Begin, End - Begin) + + "]]" + Code.substr(End)) + .str(); +} + +void checkAvailable(StringRef ID, llvm::StringRef Input, bool Available) { + Annotations Code(Input); + ASSERT_TRUE(0 < Code.points().size() || 0 < Code.ranges().size()) + << "no points of interest specified"; + TestTU TU; + TU.Filename = "foo.cpp"; + TU.Code = Code.code(); + + ParsedAST AST = TU.build(); + + auto CheckOver = [&](Range Selection) { + unsigned Begin = cantFail(positionToOffset(Code.code(), Selection.start)); + unsigned End = cantFail(positionToOffset(Code.code(), Selection.end)); + auto T = prepareTweak(ID, Tweak::Selection(AST, Begin, End)); + if (Available) + EXPECT_THAT_EXPECTED(T, Succeeded()) + << "code is " << markRange(Code.code(), Selection); + else + EXPECT_THAT_EXPECTED(T, Failed()) + << "code is " << markRange(Code.code(), Selection); + }; + for (auto P : Code.points()) + CheckOver(Range{P, P}); + for (auto R : Code.ranges()) + CheckOver(R); +} + +/// Checks action is available at every point and range marked in \p Input. +void checkAvailable(StringRef ID, llvm::StringRef Input) { + return checkAvailable(ID, Input, /*Available=*/true); +} + +/// Same as checkAvailable, but checks the action is not available. +void checkNotAvailable(StringRef ID, llvm::StringRef Input) { + return checkAvailable(ID, Input, /*Available=*/false); +} +llvm::Expected<std::string> apply(StringRef ID, llvm::StringRef Input) { + Annotations Code(Input); + Range SelectionRng; + if (Code.points().size() != 0) { + assert(Code.ranges().size() == 0 && + "both a cursor point and a selection range were specified"); + SelectionRng = Range{Code.point(), Code.point()}; + } else { + SelectionRng = Code.range(); + } + TestTU TU; + TU.Filename = "foo.cpp"; + TU.Code = Code.code(); + + ParsedAST AST = TU.build(); + unsigned Begin = cantFail(positionToOffset(Code.code(), SelectionRng.start)); + unsigned End = cantFail(positionToOffset(Code.code(), SelectionRng.end)); + Tweak::Selection S(AST, Begin, End); + + auto T = prepareTweak(ID, S); + if (!T) + return T.takeError(); + auto Replacements = (*T)->apply(S); + if (!Replacements) + return Replacements.takeError(); + return applyAllReplacements(Code.code(), *Replacements); +} + +void checkTransform(llvm::StringRef ID, llvm::StringRef Input, + llvm::StringRef Output) { + EXPECT_THAT_EXPECTED(apply(ID, Input), HasValue(Output)) + << "action id is" << ID; +} + +TEST(TweakTest, SwapIfBranches) { + llvm::StringLiteral ID = "SwapIfBranches"; + + checkAvailable(ID, R"cpp( + void test() { + ^i^f^^(^t^r^u^e^) { return 100; } ^e^l^s^e^ { continue; } + } + )cpp"); + + checkNotAvailable(ID, R"cpp( + void test() { + if (true) {^return ^100;^ } else { ^continue^;^ } + } + )cpp"); + + llvm::StringLiteral Input = R"cpp( + void test() { + ^if (true) { return 100; } else { continue; } + } + )cpp"; + llvm::StringLiteral Output = R"cpp( + void test() { + if (true) { continue; } else { return 100; } + } + )cpp"; + checkTransform(ID, Input, Output); + + Input = R"cpp( + void test() { + ^if () { return 100; } else { continue; } + } + )cpp"; + Output = R"cpp( + void test() { + if () { continue; } else { return 100; } + } + )cpp"; + checkTransform(ID, Input, Output); + + // Available in subexpressions of the condition. + checkAvailable(ID, R"cpp( + void test() { + if(2 + [[2]] + 2) { return 2 + 2 + 2; } else { continue; } + } + )cpp"); + // But not as part of the branches. + checkNotAvailable(ID, R"cpp( + void test() { + if(2 + 2 + 2) { return 2 + [[2]] + 2; } else { continue; } + } + )cpp"); + // Range covers the "else" token, so available. + checkAvailable(ID, R"cpp( + void test() { + if(2 + 2 + 2) { return 2 + [[2 + 2; } else { continue;]] } + } + )cpp"); + // Not available in compound statements in condition. + checkNotAvailable(ID, R"cpp( + void test() { + if([]{return [[true]];}()) { return 2 + 2 + 2; } else { continue; } + } + )cpp"); + // Not available if both sides aren't braced. + checkNotAvailable(ID, R"cpp( + void test() { + ^if (1) return; else { return; } + } + )cpp"); + // Only one if statement is supported! + checkNotAvailable(ID, R"cpp( + [[if(1){}else{}if(2){}else{}]] + )cpp"); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/TypeHierarchyTests.cpp b/clangd/unittests/TypeHierarchyTests.cpp new file mode 100644 index 00000000..9fcb94a9 --- /dev/null +++ b/clangd/unittests/TypeHierarchyTests.cpp @@ -0,0 +1,455 @@ +//===-- TypeHierarchyTests.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 "ClangdUnit.h" +#include "Compiler.h" +#include "Matchers.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestTU.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::Eq; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::Matcher; +using ::testing::Pointee; +using ::testing::UnorderedElementsAreArray; + +// GMock helpers for matching TypeHierarchyItem. +MATCHER_P(WithName, N, "") { return arg.name == N; } +MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; } +MATCHER_P(SelectionRangeIs, R, "") { return arg.selectionRange == R; } +template <class... ParentMatchers> +::testing::Matcher<TypeHierarchyItem> Parents(ParentMatchers... ParentsM) { + return Field(&TypeHierarchyItem::parents, HasValue(ElementsAre(ParentsM...))); +} + +TEST(FindRecordTypeAt, TypeOrVariable) { + Annotations Source(R"cpp( +struct Ch^ild2 { + int c; +}; + +int main() { + Ch^ild2 ch^ild2; + ch^ild2.c = 1; +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + for (Position Pt : Source.points()) { + const CXXRecordDecl *RD = findRecordTypeAt(AST, Pt); + EXPECT_EQ(&findDecl(AST, "Child2"), static_cast<const NamedDecl *>(RD)); + } +} + +TEST(FindRecordTypeAt, Method) { + Annotations Source(R"cpp( +struct Child2 { + void met^hod (); + void met^hod (int x); +}; + +int main() { + Child2 child2; + child2.met^hod(5); +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + for (Position Pt : Source.points()) { + const CXXRecordDecl *RD = findRecordTypeAt(AST, Pt); + EXPECT_EQ(&findDecl(AST, "Child2"), static_cast<const NamedDecl *>(RD)); + } +} + +TEST(FindRecordTypeAt, Field) { + Annotations Source(R"cpp( +struct Child2 { + int fi^eld; +}; + +int main() { + Child2 child2; + child2.fi^eld = 5; +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + for (Position Pt : Source.points()) { + const CXXRecordDecl *RD = findRecordTypeAt(AST, Pt); + // A field does not unambiguously specify a record type + // (possible associated reocrd types could be the field's type, + // or the type of the record that the field is a member of). + EXPECT_EQ(nullptr, RD); + } +} + +TEST(TypeParents, SimpleInheritance) { + Annotations Source(R"cpp( +struct Parent { + int a; +}; + +struct Child1 : Parent { + int b; +}; + +struct Child2 : Child1 { + int c; +}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Parent")); + const CXXRecordDecl *Child1 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Child1")); + const CXXRecordDecl *Child2 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Child2")); + + EXPECT_THAT(typeParents(Parent), ElementsAre()); + EXPECT_THAT(typeParents(Child1), ElementsAre(Parent)); + EXPECT_THAT(typeParents(Child2), ElementsAre(Child1)); +} + +TEST(TypeParents, MultipleInheritance) { + Annotations Source(R"cpp( +struct Parent1 { + int a; +}; + +struct Parent2 { + int b; +}; + +struct Parent3 : Parent2 { + int c; +}; + +struct Child : Parent1, Parent3 { + int d; +}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent1 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Parent1")); + const CXXRecordDecl *Parent2 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Parent2")); + const CXXRecordDecl *Parent3 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Parent3")); + const CXXRecordDecl *Child = dyn_cast<CXXRecordDecl>(&findDecl(AST, "Child")); + + EXPECT_THAT(typeParents(Parent1), ElementsAre()); + EXPECT_THAT(typeParents(Parent2), ElementsAre()); + EXPECT_THAT(typeParents(Parent3), ElementsAre(Parent2)); + EXPECT_THAT(typeParents(Child), ElementsAre(Parent1, Parent3)); +} + +TEST(TypeParents, ClassTemplate) { + Annotations Source(R"cpp( +struct Parent {}; + +template <typename T> +struct Child : Parent {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Parent")); + const CXXRecordDecl *Child = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Child"))->getTemplatedDecl(); + + EXPECT_THAT(typeParents(Child), ElementsAre(Parent)); +} + +MATCHER_P(ImplicitSpecOf, ClassTemplate, "") { + const ClassTemplateSpecializationDecl *CTS = + dyn_cast<ClassTemplateSpecializationDecl>(arg); + return CTS && + CTS->getSpecializedTemplate()->getTemplatedDecl() == ClassTemplate && + CTS->getSpecializationKind() == TSK_ImplicitInstantiation; +} + +// This is similar to findDecl(AST, QName), but supports using +// a template-id as a query. +const NamedDecl &findDeclWithTemplateArgs(ParsedAST &AST, + llvm::StringRef Query) { + return findDecl(AST, [&Query](const NamedDecl &ND) { + std::string QName; + llvm::raw_string_ostream OS(QName); + PrintingPolicy Policy(ND.getASTContext().getLangOpts()); + // Use getNameForDiagnostic() which includes the template + // arguments in the printed name. + ND.getNameForDiagnostic(OS, Policy, /*Qualified=*/true); + OS.flush(); + return QName == Query; + }); +} + +TEST(TypeParents, TemplateSpec1) { + Annotations Source(R"cpp( +template <typename T> +struct Parent {}; + +template <> +struct Parent<int> {}; + +struct Child1 : Parent<float> {}; + +struct Child2 : Parent<int> {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Parent"))->getTemplatedDecl(); + const CXXRecordDecl *ParentSpec = + dyn_cast<CXXRecordDecl>(&findDeclWithTemplateArgs(AST, "Parent<int>")); + const CXXRecordDecl *Child1 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Child1")); + const CXXRecordDecl *Child2 = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Child2")); + + EXPECT_THAT(typeParents(Child1), ElementsAre(ImplicitSpecOf(Parent))); + EXPECT_THAT(typeParents(Child2), ElementsAre(ParentSpec)); +} + +TEST(TypeParents, TemplateSpec2) { + Annotations Source(R"cpp( +struct Parent {}; + +template <typename T> +struct Child {}; + +template <> +struct Child<int> : Parent {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast<CXXRecordDecl>(&findDecl(AST, "Parent")); + const CXXRecordDecl *Child = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Child"))->getTemplatedDecl(); + const CXXRecordDecl *ChildSpec = + dyn_cast<CXXRecordDecl>(&findDeclWithTemplateArgs(AST, "Child<int>")); + + EXPECT_THAT(typeParents(Child), ElementsAre()); + EXPECT_THAT(typeParents(ChildSpec), ElementsAre(Parent)); +} + +TEST(TypeParents, DependentBase) { + Annotations Source(R"cpp( +template <typename T> +struct Parent {}; + +template <typename T> +struct Child1 : Parent<T> {}; + +template <typename T> +struct Child2 : Parent<T>::Type {}; + +template <typename T> +struct Child3 : T {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Parent"))->getTemplatedDecl(); + const CXXRecordDecl *Child1 = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Child1"))->getTemplatedDecl(); + const CXXRecordDecl *Child2 = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Child2"))->getTemplatedDecl(); + const CXXRecordDecl *Child3 = + dyn_cast<ClassTemplateDecl>(&findDecl(AST, "Child3"))->getTemplatedDecl(); + + // For "Parent<T>", use the primary template as a best-effort guess. + EXPECT_THAT(typeParents(Child1), ElementsAre(Parent)); + // For "Parent<T>::Type", there is nothing we can do. + EXPECT_THAT(typeParents(Child2), ElementsAre()); + // Likewise for "T". + EXPECT_THAT(typeParents(Child3), ElementsAre()); +} + +// Parts of getTypeHierarchy() are tested in more detail by the +// FindRecordTypeAt.* and TypeParents.* tests above. This test exercises the +// entire operation. +TEST(TypeHierarchy, Parents) { + Annotations Source(R"cpp( +struct $Parent1Def[[Parent1]] { + int a; +}; + +struct $Parent2Def[[Parent2]] { + int b; +}; + +struct $Parent3Def[[Parent3]] : Parent2 { + int c; +}; + +struct Ch^ild : Parent1, Parent3 { + int d; +}; + +int main() { + Ch^ild ch^ild; + + ch^ild.a = 1; +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + for (Position Pt : Source.points()) { + // Set ResolveLevels to 0 because it's only used for Children; + // for Parents, getTypeHierarchy() always returns all levels. + llvm::Optional<TypeHierarchyItem> Result = getTypeHierarchy( + AST, Pt, /*ResolveLevels=*/0, TypeHierarchyDirection::Parents); + ASSERT_TRUE(bool(Result)); + EXPECT_THAT( + *Result, + AllOf( + WithName("Child"), WithKind(SymbolKind::Struct), + Parents(AllOf(WithName("Parent1"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("Parent1Def")), + Parents()), + AllOf(WithName("Parent3"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("Parent3Def")), + Parents(AllOf( + WithName("Parent2"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("Parent2Def")), + Parents())))))); + } +} + +TEST(TypeHierarchy, RecursiveHierarchyUnbounded) { + Annotations Source(R"cpp( + template <int N> + struct $SDef[[S]] : S<N + 1> {}; + + S^<0> s; + )cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + // The compiler should produce a diagnostic for hitting the + // template instantiation depth. + ASSERT_TRUE(!AST.getDiagnostics().empty()); + + // Make sure getTypeHierarchy() doesn't get into an infinite recursion. + // FIXME(nridge): It would be preferable if the type hierarchy gave us type + // names (e.g. "S<0>" for the child and "S<1>" for the parent) rather than + // template names (e.g. "S"). + llvm::Optional<TypeHierarchyItem> Result = getTypeHierarchy( + AST, Source.points()[0], 0, TypeHierarchyDirection::Parents); + ASSERT_TRUE(bool(Result)); + EXPECT_THAT( + *Result, + AllOf(WithName("S"), WithKind(SymbolKind::Struct), + Parents(AllOf(WithName("S"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("SDef")), Parents())))); +} + +TEST(TypeHierarchy, RecursiveHierarchyBounded) { + Annotations Source(R"cpp( + template <int N> + struct $SDef[[S]] : S<N - 1> {}; + + template <> + struct S<0>{}; + + S$SRefConcrete^<2> s; + + template <int N> + struct Foo { + S$SRefDependent^<N> s; + };)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + // Make sure getTypeHierarchy() doesn't get into an infinite recursion + // for either a concrete starting point or a dependent starting point. + llvm::Optional<TypeHierarchyItem> Result = getTypeHierarchy( + AST, Source.point("SRefConcrete"), 0, TypeHierarchyDirection::Parents); + ASSERT_TRUE(bool(Result)); + EXPECT_THAT( + *Result, + AllOf(WithName("S"), WithKind(SymbolKind::Struct), + Parents(AllOf(WithName("S"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("SDef")), Parents())))); + Result = getTypeHierarchy(AST, Source.point("SRefDependent"), 0, + TypeHierarchyDirection::Parents); + ASSERT_TRUE(bool(Result)); + EXPECT_THAT( + *Result, + AllOf(WithName("S"), WithKind(SymbolKind::Struct), + Parents(AllOf(WithName("S"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("SDef")), Parents())))); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/URITests.cpp b/clangd/unittests/URITests.cpp new file mode 100644 index 00000000..52ca7b44 --- /dev/null +++ b/clangd/unittests/URITests.cpp @@ -0,0 +1,187 @@ +//===-- URITests.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 "Matchers.h" +#include "TestFS.h" +#include "URI.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +// Force the unittest URI scheme to be linked, +static int LLVM_ATTRIBUTE_UNUSED UnittestSchemeAnchorDest = + UnittestSchemeAnchorSource; + +namespace { + +using ::testing::AllOf; + +MATCHER_P(Scheme, S, "") { return arg.scheme() == S; } +MATCHER_P(Authority, A, "") { return arg.authority() == A; } +MATCHER_P(Body, B, "") { return arg.body() == B; } + +std::string createOrDie(llvm::StringRef AbsolutePath, + llvm::StringRef Scheme = "file") { + auto Uri = URI::create(AbsolutePath, Scheme); + if (!Uri) + llvm_unreachable(toString(Uri.takeError()).c_str()); + return Uri->toString(); +} + +URI parseOrDie(llvm::StringRef Uri) { + auto U = URI::parse(Uri); + if (!U) + llvm_unreachable(toString(U.takeError()).c_str()); + return *U; +} + +TEST(PercentEncodingTest, Encode) { + EXPECT_EQ(URI("x", /*Authority=*/"", "a/b/c").toString(), "x:a/b/c"); + EXPECT_EQ(URI("x", /*Authority=*/"", "a!b;c~").toString(), "x:a%21b%3Bc~"); + EXPECT_EQ(URI("x", /*Authority=*/"", "a123b").toString(), "x:a123b"); + EXPECT_EQ(URI("x", /*Authority=*/"", "a:b;c").toString(), "x:a:b%3Bc"); +} + +TEST(PercentEncodingTest, Decode) { + EXPECT_EQ(parseOrDie("x:a/b/c").body(), "a/b/c"); + + EXPECT_EQ(parseOrDie("s%2b://%3a/%3").scheme(), "s+"); + EXPECT_EQ(parseOrDie("s%2b://%3a/%3").authority(), ":"); + EXPECT_EQ(parseOrDie("s%2b://%3a/%3").body(), "/%3"); + + EXPECT_EQ(parseOrDie("x:a%21b%3ac~").body(), "a!b:c~"); + EXPECT_EQ(parseOrDie("x:a:b%3bc").body(), "a:b;c"); +} + +std::string resolveOrDie(const URI &U, llvm::StringRef HintPath = "") { + auto Path = URI::resolve(U, HintPath); + if (!Path) + llvm_unreachable(toString(Path.takeError()).c_str()); + return *Path; +} + +TEST(URITest, Create) { +#ifdef _WIN32 + EXPECT_THAT(createOrDie("c:\\x\\y\\z"), "file:///c:/x/y/z"); +#else + EXPECT_THAT(createOrDie("/x/y/z"), "file:///x/y/z"); + EXPECT_THAT(createOrDie("/(x)/y/\\ z"), "file:///%28x%29/y/%5C%20z"); +#endif +} + +TEST(URITest, FailedCreate) { + EXPECT_ERROR(URI::create("/x/y/z", "no")); + // Path has to be absolute. + EXPECT_ERROR(URI::create("x/y/z", "file")); +} + +TEST(URITest, Parse) { + EXPECT_THAT(parseOrDie("file://auth/x/y/z"), + AllOf(Scheme("file"), Authority("auth"), Body("/x/y/z"))); + + EXPECT_THAT(parseOrDie("file://au%3dth/%28x%29/y/%5c%20z"), + AllOf(Scheme("file"), Authority("au=th"), Body("/(x)/y/\\ z"))); + + EXPECT_THAT(parseOrDie("file:///%28x%29/y/%5c%20z"), + AllOf(Scheme("file"), Authority(""), Body("/(x)/y/\\ z"))); + EXPECT_THAT(parseOrDie("file:///x/y/z"), + AllOf(Scheme("file"), Authority(""), Body("/x/y/z"))); + EXPECT_THAT(parseOrDie("file:"), + AllOf(Scheme("file"), Authority(""), Body(""))); + EXPECT_THAT(parseOrDie("file:///x/y/z%2"), + AllOf(Scheme("file"), Authority(""), Body("/x/y/z%2"))); + EXPECT_THAT(parseOrDie("http://llvm.org"), + AllOf(Scheme("http"), Authority("llvm.org"), Body(""))); + EXPECT_THAT(parseOrDie("http://llvm.org/"), + AllOf(Scheme("http"), Authority("llvm.org"), Body("/"))); + EXPECT_THAT(parseOrDie("http://llvm.org/D"), + AllOf(Scheme("http"), Authority("llvm.org"), Body("/D"))); + EXPECT_THAT(parseOrDie("http:/"), + AllOf(Scheme("http"), Authority(""), Body("/"))); + EXPECT_THAT(parseOrDie("urn:isbn:0451450523"), + AllOf(Scheme("urn"), Authority(""), Body("isbn:0451450523"))); + EXPECT_THAT( + parseOrDie("file:///c:/windows/system32/"), + AllOf(Scheme("file"), Authority(""), Body("/c:/windows/system32/"))); +} + +TEST(URITest, ParseFailed) { + // Expect ':' in URI. + EXPECT_ERROR(URI::parse("file//x/y/z")); + // Empty. + EXPECT_ERROR(URI::parse("")); + EXPECT_ERROR(URI::parse(":/a/b/c")); + EXPECT_ERROR(URI::parse("\"/a/b/c\" IWYU pragma: abc")); +} + +TEST(URITest, Resolve) { +#ifdef _WIN32 + EXPECT_THAT(resolveOrDie(parseOrDie("file:///c%3a/x/y/z")), "c:\\x\\y\\z"); + EXPECT_THAT(resolveOrDie(parseOrDie("file:///c:/x/y/z")), "c:\\x\\y\\z"); +#else + EXPECT_EQ(resolveOrDie(parseOrDie("file:/a/b/c")), "/a/b/c"); + EXPECT_EQ(resolveOrDie(parseOrDie("file://auth/a/b/c")), "/a/b/c"); + EXPECT_THAT(resolveOrDie(parseOrDie("file://au%3dth/%28x%29/y/%20z")), + "/(x)/y/ z"); + EXPECT_THAT(resolveOrDie(parseOrDie("file:///c:/x/y/z")), "c:/x/y/z"); +#endif + EXPECT_EQ(resolveOrDie(parseOrDie("unittest:///a"), testPath("x")), + testPath("a")); +} + +std::string resolvePathOrDie(llvm::StringRef AbsPath, + llvm::StringRef HintPath = "") { + auto Path = URI::resolvePath(AbsPath, HintPath); + if (!Path) + llvm_unreachable(toString(Path.takeError()).c_str()); + return *Path; +} + +TEST(URITest, ResolvePath) { + StringRef FilePath = +#ifdef _WIN32 + "c:\\x\\y\\z"; +#else + "/a/b/c"; +#endif + EXPECT_EQ(resolvePathOrDie(FilePath), FilePath); + EXPECT_EQ(resolvePathOrDie(testPath("x"), testPath("hint")), testPath("x")); + // HintPath is not in testRoot(); resolution fails. + auto Resolve = URI::resolvePath(testPath("x"), FilePath); + EXPECT_FALSE(Resolve); + llvm::consumeError(Resolve.takeError()); +} + +TEST(URITest, Platform) { + auto Path = testPath("x"); + auto U = URI::create(Path, "file"); + EXPECT_TRUE(static_cast<bool>(U)); + EXPECT_THAT(resolveOrDie(*U), Path); +} + +TEST(URITest, ResolveFailed) { + auto FailedResolve = [](StringRef Uri) { + auto Path = URI::resolve(parseOrDie(Uri)); + if (!Path) { + consumeError(Path.takeError()); + return true; + } + return false; + }; + + // Invalid scheme. + EXPECT_TRUE(FailedResolve("no:/a/b/c")); + // File path needs to be absolute. + EXPECT_TRUE(FailedResolve("file:a/b/c")); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/XRefsTests.cpp b/clangd/unittests/XRefsTests.cpp new file mode 100644 index 00000000..77fa042c --- /dev/null +++ b/clangd/unittests/XRefsTests.cpp @@ -0,0 +1,1517 @@ +//===-- XRefsTests.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 "ClangdUnit.h" +#include "Compiler.h" +#include "Matchers.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "TestTU.h" +#include "XRefs.h" +#include "index/FileIndex.h" +#include "index/SymbolCollector.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::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Matcher; +using ::testing::UnorderedElementsAreArray; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override {} +}; + +MATCHER_P2(FileRange, File, Range, "") { + return Location{URIForFile::canonicalize(File, testRoot()), Range} == arg; +} + +// Extracts ranges from an annotated example, and constructs a matcher for a +// highlight set. Ranges should be named $read/$write as appropriate. +Matcher<const std::vector<DocumentHighlight> &> +HighlightsFrom(const Annotations &Test) { + std::vector<DocumentHighlight> Expected; + auto Add = [&](const Range &R, DocumentHighlightKind K) { + Expected.emplace_back(); + Expected.back().range = R; + Expected.back().kind = K; + }; + for (const auto &Range : Test.ranges()) + Add(Range, DocumentHighlightKind::Text); + for (const auto &Range : Test.ranges("read")) + Add(Range, DocumentHighlightKind::Read); + for (const auto &Range : Test.ranges("write")) + Add(Range, DocumentHighlightKind::Write); + return UnorderedElementsAreArray(Expected); +} + +TEST(HighlightsTest, All) { + const char *Tests[] = { + R"cpp(// Local variable + int main() { + int [[bonjour]]; + $write[[^bonjour]] = 2; + int test1 = $read[[bonjour]]; + } + )cpp", + + R"cpp(// Struct + namespace ns1 { + struct [[MyClass]] { + static void foo([[MyClass]]*) {} + }; + } // namespace ns1 + int main() { + ns1::[[My^Class]]* Params; + } + )cpp", + + R"cpp(// Function + int [[^foo]](int) {} + int main() { + [[foo]]([[foo]](42)); + auto *X = &[[foo]]; + } + )cpp", + + R"cpp(// Function parameter in decl + void foo(int [[^bar]]); + )cpp", + }; + for (const char *Test : Tests) { + Annotations T(Test); + auto AST = TestTU::withCode(T.code()).build(); + EXPECT_THAT(findDocumentHighlights(AST, T.point()), HighlightsFrom(T)) + << Test; + } +} + +MATCHER_P3(Sym, Name, Decl, DefOrNone, "") { + llvm::Optional<Range> Def = DefOrNone; + if (Name != arg.Name) { + *result_listener << "Name is " << arg.Name; + return false; + } + if (Decl != arg.PreferredDeclaration.range) { + *result_listener << "Declaration is " + << llvm::to_string(arg.PreferredDeclaration); + return false; + } + if (Def && !arg.Definition) { + *result_listener << "Has no definition"; + return false; + } + if (Def && arg.Definition->range != *Def) { + *result_listener << "Definition is " << llvm::to_string(arg.Definition); + return false; + } + return true; +} +::testing::Matcher<LocatedSymbol> Sym(std::string Name, Range Decl) { + return Sym(Name, Decl, llvm::None); +} +MATCHER_P(Sym, Name, "") { return arg.Name == Name; } + +MATCHER_P(RangeIs, R, "") { return arg.range == R; } + +TEST(LocateSymbol, WithIndex) { + Annotations SymbolHeader(R"cpp( + class $forward[[Forward]]; + class $foo[[Foo]] {}; + + void $f1[[f1]](); + + inline void $f2[[f2]]() {} + )cpp"); + Annotations SymbolCpp(R"cpp( + class $forward[[forward]] {}; + void $f1[[f1]]() {} + )cpp"); + + TestTU TU; + TU.Code = SymbolCpp.code(); + TU.HeaderCode = SymbolHeader.code(); + auto Index = TU.index(); + auto LocateWithIndex = [&Index](const Annotations &Main) { + auto AST = TestTU::withCode(Main.code()).build(); + return clangd::locateSymbolAt(AST, Main.point(), Index.get()); + }; + + Annotations Test(R"cpp(// only declaration in AST. + void [[f1]](); + int main() { + ^f1(); + } + )cpp"); + EXPECT_THAT(LocateWithIndex(Test), + ElementsAre(Sym("f1", Test.range(), SymbolCpp.range("f1")))); + + Test = Annotations(R"cpp(// definition in AST. + void [[f1]]() {} + int main() { + ^f1(); + } + )cpp"); + EXPECT_THAT(LocateWithIndex(Test), + ElementsAre(Sym("f1", SymbolHeader.range("f1"), Test.range()))); + + Test = Annotations(R"cpp(// forward declaration in AST. + class [[Foo]]; + F^oo* create(); + )cpp"); + EXPECT_THAT(LocateWithIndex(Test), + ElementsAre(Sym("Foo", Test.range(), SymbolHeader.range("foo")))); + + Test = Annotations(R"cpp(// defintion in AST. + class [[Forward]] {}; + F^orward create(); + )cpp"); + EXPECT_THAT( + LocateWithIndex(Test), + ElementsAre(Sym("Forward", SymbolHeader.range("forward"), Test.range()))); +} + +TEST(LocateSymbol, WithIndexPreferredLocation) { + Annotations SymbolHeader(R"cpp( + class $p[[Proto]] {}; + void $f[[func]]() {}; + )cpp"); + TestTU TU; + TU.HeaderCode = SymbolHeader.code(); + TU.HeaderFilename = "x.proto"; // Prefer locations in codegen files. + auto Index = TU.index(); + + Annotations Test(R"cpp(// only declaration in AST. + // Shift to make range different. + class Proto; + void func() {} + P$p^roto* create() { + fu$f^nc(); + return nullptr; + } + )cpp"); + + auto AST = TestTU::withCode(Test.code()).build(); + { + auto Locs = clangd::locateSymbolAt(AST, Test.point("p"), Index.get()); + auto CodeGenLoc = SymbolHeader.range("p"); + EXPECT_THAT(Locs, ElementsAre(Sym("Proto", CodeGenLoc, CodeGenLoc))); + } + { + auto Locs = clangd::locateSymbolAt(AST, Test.point("f"), Index.get()); + auto CodeGenLoc = SymbolHeader.range("f"); + EXPECT_THAT(Locs, ElementsAre(Sym("func", CodeGenLoc, CodeGenLoc))); + } +} + +TEST(LocateSymbol, All) { + // Ranges in tests: + // $decl is the declaration location (if absent, no symbol is located) + // $def is the definition location (if absent, symbol has no definition) + // unnamed range becomes both $decl and $def. + const char *Tests[] = { + R"cpp(// Local variable + int main() { + int [[bonjour]]; + ^bonjour = 2; + int test1 = bonjour; + } + )cpp", + + R"cpp(// Struct + namespace ns1 { + struct [[MyClass]] {}; + } // namespace ns1 + int main() { + ns1::My^Class* Params; + } + )cpp", + + R"cpp(// Function definition via pointer + int [[foo]](int) {} + int main() { + auto *X = &^foo; + } + )cpp", + + R"cpp(// Function declaration via call + int $decl[[foo]](int); + int main() { + return ^foo(42); + } + )cpp", + + R"cpp(// Field + struct Foo { int [[x]]; }; + int main() { + Foo bar; + bar.^x; + } + )cpp", + + R"cpp(// Field, member initializer + struct Foo { + int [[x]]; + Foo() : ^x(0) {} + }; + )cpp", + + R"cpp(// Field, GNU old-style field designator + struct Foo { int [[x]]; }; + int main() { + Foo bar = { ^x : 1 }; + } + )cpp", + + R"cpp(// Field, field designator + struct Foo { int [[x]]; }; + int main() { + Foo bar = { .^x = 2 }; + } + )cpp", + + R"cpp(// Method call + struct Foo { int $decl[[x]](); }; + int main() { + Foo bar; + bar.^x(); + } + )cpp", + + R"cpp(// Typedef + typedef int $decl[[Foo]]; + int main() { + ^Foo bar; + } + )cpp", + + R"cpp(// Template type parameter + template <typename [[T]]> + void foo() { ^T t; } + )cpp", + + R"cpp(// Template template type parameter + template <template<typename> class [[T]]> + void foo() { ^T<int> t; } + )cpp", + + R"cpp(// Namespace + namespace $decl[[ns]] { + struct Foo { static void bar(); } + } // namespace ns + int main() { ^ns::Foo::bar(); } + )cpp", + + R"cpp(// Macro + #define MACRO 0 + #define [[MACRO]] 1 + int main() { return ^MACRO; } + #define MACRO 2 + #undef macro + )cpp", + + R"cpp(// Macro + class TTT { public: int a; }; + #define [[FF]](S) if (int b = S.a) {} + void f() { + TTT t; + F^F(t); + } + )cpp", + + R"cpp(// Macro argument + int [[i]]; + #define ADDRESSOF(X) &X; + int *j = ADDRESSOF(^i); + )cpp", + + R"cpp(// Symbol concatenated inside macro (not supported) + int *pi; + #define POINTER(X) p # X; + int i = *POINTER(^i); + )cpp", + + R"cpp(// Forward class declaration + class Foo; + class [[Foo]] {}; + F^oo* foo(); + )cpp", + + R"cpp(// Function declaration + void foo(); + void g() { f^oo(); } + void [[foo]]() {} + )cpp", + + R"cpp( + #define FF(name) class name##_Test {}; + [[FF]](my); + void f() { my^_Test a; } + )cpp", + + R"cpp( + #define FF() class [[Test]] {}; + FF(); + void f() { T^est a; } + )cpp", + + R"cpp(// explicit template specialization + template <typename T> + struct Foo { void bar() {} }; + + template <> + struct [[Foo]]<int> { void bar() {} }; + + void foo() { + Foo<char> abc; + Fo^o<int> b; + } + )cpp", + + R"cpp(// implicit template specialization + template <typename T> + struct [[Foo]] { void bar() {} }; + template <> + struct Foo<int> { void bar() {} }; + void foo() { + Fo^o<char> abc; + Foo<int> b; + } + )cpp", + + R"cpp(// partial template specialization + template <typename T> + struct Foo { void bar() {} }; + template <typename T> + struct [[Foo]]<T*> { void bar() {} }; + ^Foo<int*> x; + )cpp", + + R"cpp(// function template specializations + template <class T> + void foo(T) {} + template <> + void [[foo]](int) {} + void bar() { + fo^o(10); + } + )cpp", + + R"cpp(// variable template decls + template <class T> + T var = T(); + + template <> + double [[var]]<int> = 10; + + double y = va^r<int>; + )cpp", + + R"cpp(// No implicit constructors + class X { + X(X&& x) = default; + }; + X [[makeX]]() {} + void foo() { + auto x = m^akeX(); + } + )cpp", + }; + for (const char *Test : Tests) { + Annotations T(Test); + llvm::Optional<Range> WantDecl; + llvm::Optional<Range> WantDef; + if (!T.ranges().empty()) + WantDecl = WantDef = T.range(); + if (!T.ranges("decl").empty()) + WantDecl = T.range("decl"); + if (!T.ranges("def").empty()) + WantDef = T.range("def"); + + auto AST = TestTU::withCode(T.code()).build(); + auto Results = locateSymbolAt(AST, T.point()); + + if (!WantDecl) { + EXPECT_THAT(Results, IsEmpty()) << Test; + } else { + ASSERT_THAT(Results, ::testing::SizeIs(1)) << Test; + EXPECT_EQ(Results[0].PreferredDeclaration.range, *WantDecl) << Test; + llvm::Optional<Range> GotDef; + if (Results[0].Definition) + GotDef = Results[0].Definition->range; + EXPECT_EQ(WantDef, GotDef) << Test; + } + } +} + +TEST(LocateSymbol, Ambiguous) { + auto T = Annotations(R"cpp( + struct Foo { + Foo(); + Foo(Foo&&); + Foo(const char*); + }; + + Foo f(); + + void g(Foo foo); + + void call() { + const char* str = "123"; + Foo a = $1^str; + Foo b = Foo($2^str); + Foo c = $3^f(); + $4^g($5^f()); + g($6^str); + Foo ab$7^c; + Foo ab$8^cd("asdf"); + Foo foox = Fo$9^o("asdf"); + } + )cpp"); + auto AST = TestTU::withCode(T.code()).build(); + // Ordered assertions are deliberate: we expect a predictable order. + EXPECT_THAT(locateSymbolAt(AST, T.point("1")), ElementsAre(Sym("str"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("2")), ElementsAre(Sym("str"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("3")), ElementsAre(Sym("f"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("4")), ElementsAre(Sym("g"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("5")), ElementsAre(Sym("f"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("6")), ElementsAre(Sym("str"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("7")), ElementsAre(Sym("abc"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("8")), + ElementsAre(Sym("Foo"), Sym("abcd"))); + EXPECT_THAT(locateSymbolAt(AST, T.point("9")), + // First one is class definition, second is the constructor. + ElementsAre(Sym("Foo"), Sym("Foo"))); +} + +TEST(LocateSymbol, RelPathsInCompileCommand) { + // The source is in "/clangd-test/src". + // We build in "/clangd-test/build". + + Annotations SourceAnnotations(R"cpp( +#include "header_in_preamble.h" +int [[foo]]; +#include "header_not_in_preamble.h" +int baz = f$p1^oo + bar_pre$p2^amble + bar_not_pre$p3^amble; +)cpp"); + + Annotations HeaderInPreambleAnnotations(R"cpp( +int [[bar_preamble]]; +)cpp"); + + Annotations HeaderNotInPreambleAnnotations(R"cpp( +int [[bar_not_preamble]]; +)cpp"); + + // Make the compilation paths appear as ../src/foo.cpp in the compile + // commands. + SmallString<32> RelPathPrefix(".."); + llvm::sys::path::append(RelPathPrefix, "src"); + std::string BuildDir = testPath("build"); + MockCompilationDatabase CDB(BuildDir, RelPathPrefix); + + IgnoreDiagnostics DiagConsumer; + MockFSProvider FS; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + // Fill the filesystem. + auto FooCpp = testPath("src/foo.cpp"); + FS.Files[FooCpp] = ""; + auto HeaderInPreambleH = testPath("src/header_in_preamble.h"); + FS.Files[HeaderInPreambleH] = HeaderInPreambleAnnotations.code(); + auto HeaderNotInPreambleH = testPath("src/header_not_in_preamble.h"); + FS.Files[HeaderNotInPreambleH] = HeaderNotInPreambleAnnotations.code(); + + runAddDocument(Server, FooCpp, SourceAnnotations.code()); + + // Go to a definition in main source file. + auto Locations = + runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("p1")); + EXPECT_TRUE(bool(Locations)) << "findDefinitions returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo", SourceAnnotations.range()))); + + // Go to a definition in header_in_preamble.h. + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("p2")); + EXPECT_TRUE(bool(Locations)) << "findDefinitions returned an error"; + EXPECT_THAT( + *Locations, + ElementsAre(Sym("bar_preamble", HeaderInPreambleAnnotations.range()))); + + // Go to a definition in header_not_in_preamble.h. + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("p3")); + EXPECT_TRUE(bool(Locations)) << "findDefinitions returned an error"; + EXPECT_THAT(*Locations, + ElementsAre(Sym("bar_not_preamble", + HeaderNotInPreambleAnnotations.range()))); +} + +TEST(Hover, All) { + struct OneTest { + StringRef Input; + StringRef ExpectedHover; + }; + + OneTest Tests[] = { + { + R"cpp(// No hover + ^int main() { + } + )cpp", + "", + }, + { + R"cpp(// Local variable + int main() { + int bonjour; + ^bonjour = 2; + int test1 = bonjour; + } + )cpp", + "Declared in function main\n\nint bonjour", + }, + { + R"cpp(// Local variable in method + struct s { + void method() { + int bonjour; + ^bonjour = 2; + } + }; + )cpp", + "Declared in function s::method\n\nint bonjour", + }, + { + R"cpp(// Struct + namespace ns1 { + struct MyClass {}; + } // namespace ns1 + int main() { + ns1::My^Class* Params; + } + )cpp", + "Declared in namespace ns1\n\nstruct MyClass {}", + }, + { + R"cpp(// Class + namespace ns1 { + class MyClass {}; + } // namespace ns1 + int main() { + ns1::My^Class* Params; + } + )cpp", + "Declared in namespace ns1\n\nclass MyClass {}", + }, + { + R"cpp(// Union + namespace ns1 { + union MyUnion { int x; int y; }; + } // namespace ns1 + int main() { + ns1::My^Union Params; + } + )cpp", + "Declared in namespace ns1\n\nunion MyUnion {}", + }, + { + R"cpp(// Function definition via pointer + int foo(int) {} + int main() { + auto *X = &^foo; + } + )cpp", + "Declared in global namespace\n\nint foo(int)", + }, + { + R"cpp(// Function declaration via call + int foo(int); + int main() { + return ^foo(42); + } + )cpp", + "Declared in global namespace\n\nint foo(int)", + }, + { + R"cpp(// Field + struct Foo { int x; }; + int main() { + Foo bar; + bar.^x; + } + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Field with initialization + struct Foo { int x = 5; }; + int main() { + Foo bar; + bar.^x; + } + )cpp", + "Declared in struct Foo\n\nint x = 5", + }, + { + R"cpp(// Static field + struct Foo { static int x; }; + int main() { + Foo::^x; + } + )cpp", + "Declared in struct Foo\n\nstatic int x", + }, + { + R"cpp(// Field, member initializer + struct Foo { + int x; + Foo() : ^x(0) {} + }; + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Field, GNU old-style field designator + struct Foo { int x; }; + int main() { + Foo bar = { ^x : 1 }; + } + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Field, field designator + struct Foo { int x; }; + int main() { + Foo bar = { .^x = 2 }; + } + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Method call + struct Foo { int x(); }; + int main() { + Foo bar; + bar.^x(); + } + )cpp", + "Declared in struct Foo\n\nint x()", + }, + { + R"cpp(// Static method call + struct Foo { static int x(); }; + int main() { + Foo::^x(); + } + )cpp", + "Declared in struct Foo\n\nstatic int x()", + }, + { + R"cpp(// Typedef + typedef int Foo; + int main() { + ^Foo bar; + } + )cpp", + "Declared in global namespace\n\ntypedef int Foo", + }, + { + R"cpp(// Namespace + namespace ns { + struct Foo { static void bar(); } + } // namespace ns + int main() { ^ns::Foo::bar(); } + )cpp", + "Declared in global namespace\n\nnamespace ns {\n}", + }, + { + R"cpp(// Anonymous namespace + namespace ns { + namespace { + int foo; + } // anonymous namespace + } // namespace ns + int main() { ns::f^oo++; } + )cpp", + "Declared in namespace ns::(anonymous)\n\nint foo", + }, + { + R"cpp(// Macro + #define MACRO 0 + #define MACRO 1 + int main() { return ^MACRO; } + #define MACRO 2 + #undef macro + )cpp", + "#define MACRO 1", + }, + { + R"cpp(// Macro + #define MACRO 0 + #define MACRO2 ^MACRO + )cpp", + "#define MACRO 0", + }, + { + R"cpp(// Macro + #define MACRO {\ + return 0;\ + } + int main() ^MACRO + )cpp", + R"cpp(#define MACRO {\ + return 0;\ + })cpp", + }, + { + R"cpp(// Forward class declaration + class Foo; + class Foo {}; + F^oo* foo(); + )cpp", + "Declared in global namespace\n\nclass Foo {}", + }, + { + R"cpp(// Function declaration + void foo(); + void g() { f^oo(); } + void foo() {} + )cpp", + "Declared in global namespace\n\nvoid foo()", + }, + { + R"cpp(// Enum declaration + enum Hello { + ONE, TWO, THREE, + }; + void foo() { + Hel^lo hello = ONE; + } + )cpp", + "Declared in global namespace\n\nenum Hello {\n}", + }, + { + R"cpp(// Enumerator + enum Hello { + ONE, TWO, THREE, + }; + void foo() { + Hello hello = O^NE; + } + )cpp", + "Declared in enum Hello\n\nONE", + }, + { + R"cpp(// Enumerator in anonymous enum + enum { + ONE, TWO, THREE, + }; + void foo() { + int hello = O^NE; + } + )cpp", + "Declared in enum (anonymous)\n\nONE", + }, + { + R"cpp(// Global variable + static int hey = 10; + void foo() { + he^y++; + } + )cpp", + "Declared in global namespace\n\nstatic int hey = 10", + }, + { + R"cpp(// Global variable in namespace + namespace ns1 { + static int hey = 10; + } + void foo() { + ns1::he^y++; + } + )cpp", + "Declared in namespace ns1\n\nstatic int hey = 10", + }, + { + R"cpp(// Field in anonymous struct + static struct { + int hello; + } s; + void foo() { + s.he^llo++; + } + )cpp", + "Declared in struct (anonymous)\n\nint hello", + }, + { + R"cpp(// Templated function + template <typename T> + T foo() { + return 17; + } + void g() { auto x = f^oo<int>(); } + )cpp", + "Declared in global namespace\n\ntemplate <typename T> T foo()", + }, + { + R"cpp(// Anonymous union + struct outer { + union { + int abc, def; + } v; + }; + void g() { struct outer o; o.v.d^ef++; } + )cpp", + "Declared in union outer::(anonymous)\n\nint def", + }, + { + R"cpp(// Nothing + void foo() { + ^ + } + )cpp", + "", + }, + { + R"cpp(// Simple initialization with auto + void foo() { + ^auto i = 1; + } + )cpp", + "int", + }, + { + R"cpp(// Simple initialization with const auto + void foo() { + const ^auto i = 1; + } + )cpp", + "int", + }, + { + R"cpp(// Simple initialization with const auto& + void foo() { + const ^auto& i = 1; + } + )cpp", + "int", + }, + { + R"cpp(// Simple initialization with auto& + void foo() { + ^auto& i = 1; + } + )cpp", + "int", + }, + { + R"cpp(// Simple initialization with auto* + void foo() { + int a = 1; + ^auto* i = &a; + } + )cpp", + "int", + }, + { + R"cpp(// Auto with initializer list. + namespace std + { + template<class _E> + class initializer_list {}; + } + void foo() { + ^auto i = {1,2}; + } + )cpp", + "class std::initializer_list<int>", + }, + { + R"cpp(// User defined conversion to auto + struct Bar { + operator ^auto() const { return 10; } + }; + )cpp", + "int", + }, + { + R"cpp(// Simple initialization with decltype(auto) + void foo() { + ^decltype(auto) i = 1; + } + )cpp", + "int", + }, + { + R"cpp(// Simple initialization with const decltype(auto) + void foo() { + const int j = 0; + ^decltype(auto) i = j; + } + )cpp", + "const int", + }, + { + R"cpp(// Simple initialization with const& decltype(auto) + void foo() { + int k = 0; + const int& j = k; + ^decltype(auto) i = j; + } + )cpp", + "const int &", + }, + { + R"cpp(// Simple initialization with & decltype(auto) + void foo() { + int k = 0; + int& j = k; + ^decltype(auto) i = j; + } + )cpp", + "int &", + }, + { + R"cpp(// decltype with initializer list: nothing + namespace std + { + template<class _E> + class initializer_list {}; + } + void foo() { + ^decltype(auto) i = {1,2}; + } + )cpp", + "", + }, + { + R"cpp(// simple trailing return type + ^auto main() -> int { + return 0; + } + )cpp", + "int", + }, + { + R"cpp(// auto function return with trailing type + struct Bar {}; + ^auto test() -> decltype(Bar()) { + return Bar(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// trailing return type + struct Bar {}; + auto test() -> ^decltype(Bar()) { + return Bar(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// auto in function return + struct Bar {}; + ^auto test() { + return Bar(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// auto& in function return + struct Bar {}; + ^auto& test() { + return Bar(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// auto* in function return + struct Bar {}; + ^auto* test() { + Bar* bar; + return bar; + } + )cpp", + "struct Bar", + }, + { + R"cpp(// const auto& in function return + struct Bar {}; + const ^auto& test() { + return Bar(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// decltype(auto) in function return + struct Bar {}; + ^decltype(auto) test() { + return Bar(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// decltype(auto) reference in function return + struct Bar {}; + ^decltype(auto) test() { + int a; + return (a); + } + )cpp", + "int &", + }, + { + R"cpp(// decltype lvalue reference + void foo() { + int I = 0; + ^decltype(I) J = I; + } + )cpp", + "int", + }, + { + R"cpp(// decltype lvalue reference + void foo() { + int I= 0; + int &K = I; + ^decltype(K) J = I; + } + )cpp", + "int &", + }, + { + R"cpp(// decltype lvalue reference parenthesis + void foo() { + int I = 0; + ^decltype((I)) J = I; + } + )cpp", + "int &", + }, + { + R"cpp(// decltype rvalue reference + void foo() { + int I = 0; + ^decltype(static_cast<int&&>(I)) J = static_cast<int&&>(I); + } + )cpp", + "int &&", + }, + { + R"cpp(// decltype rvalue reference function call + int && bar(); + void foo() { + int I = 0; + ^decltype(bar()) J = bar(); + } + )cpp", + "int &&", + }, + { + R"cpp(// decltype of function with trailing return type. + struct Bar {}; + auto test() -> decltype(Bar()) { + return Bar(); + } + void foo() { + ^decltype(test()) i = test(); + } + )cpp", + "struct Bar", + }, + { + R"cpp(// decltype of var with decltype. + void foo() { + int I = 0; + decltype(I) J = I; + ^decltype(J) K = J; + } + )cpp", + "int", + }, + { + R"cpp(// structured binding. Not supported yet + struct Bar {}; + void foo() { + Bar a[2]; + ^auto [x,y] = a; + } + )cpp", + "", + }, + { + R"cpp(// Template auto parameter. Nothing (Not useful). + template<^auto T> + void func() { + } + void foo() { + func<1>(); + } + )cpp", + "", + }, + { + R"cpp(// More compilcated structured types. + int bar(); + ^auto (*foo)() = bar; + )cpp", + "int", + }, + }; + + for (const OneTest &Test : Tests) { + Annotations T(Test.Input); + TestTU TU = TestTU::withCode(T.code()); + TU.ExtraArgs.push_back("-std=c++17"); + auto AST = TU.build(); + if (auto H = getHover(AST, T.point())) { + EXPECT_NE("", Test.ExpectedHover) << Test.Input; + EXPECT_EQ(H->contents.value, Test.ExpectedHover.str()) << Test.Input; + } else + EXPECT_EQ("", Test.ExpectedHover.str()) << Test.Input; + } +} + +TEST(GoToInclude, All) { + MockFSProvider FS; + IgnoreDiagnostics DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + const char *SourceContents = R"cpp( + #include ^"$2^foo.h$3^" + #include "$4^invalid.h" + int b = a; + // test + int foo; + #in$5^clude "$6^foo.h"$7^ + )cpp"; + Annotations SourceAnnotations(SourceContents); + FS.Files[FooCpp] = SourceAnnotations.code(); + auto FooH = testPath("foo.h"); + + const char *HeaderContents = R"cpp([[]]#pragma once + int a; + )cpp"; + Annotations HeaderAnnotations(HeaderContents); + FS.Files[FooH] = HeaderAnnotations.code(); + + Server.addDocument(FooH, HeaderAnnotations.code()); + Server.addDocument(FooCpp, SourceAnnotations.code()); + + // Test include in preamble. + auto Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point()); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); + + // Test include in preamble, last char. + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("2")); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); + + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("3")); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); + + // Test include outside of preamble. + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("6")); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); + + // Test a few positions that do not result in Locations. + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("4")); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, IsEmpty()); + + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("5")); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); + + Locations = runLocateSymbolAt(Server, FooCpp, SourceAnnotations.point("7")); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); + + // Objective C #import directive. + Annotations ObjC(R"objc( + #import "^foo.h" + )objc"); + auto FooM = testPath("foo.m"); + FS.Files[FooM] = ObjC.code(); + + Server.addDocument(FooM, ObjC.code()); + Locations = runLocateSymbolAt(Server, FooM, ObjC.point()); + ASSERT_TRUE(bool(Locations)) << "locateSymbolAt returned an error"; + EXPECT_THAT(*Locations, ElementsAre(Sym("foo.h", HeaderAnnotations.range()))); +} + +TEST(LocateSymbol, WithPreamble) { + // Test stragety: AST should always use the latest preamble instead of last + // good preamble. + MockFSProvider FS; + IgnoreDiagnostics DiagConsumer; + MockCompilationDatabase CDB; + ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); + + auto FooCpp = testPath("foo.cpp"); + // The trigger locations must be the same. + Annotations FooWithHeader(R"cpp(#include "fo^o.h")cpp"); + Annotations FooWithoutHeader(R"cpp(double [[fo^o]]();)cpp"); + + FS.Files[FooCpp] = FooWithHeader.code(); + + auto FooH = testPath("foo.h"); + Annotations FooHeader(R"cpp([[]])cpp"); + FS.Files[FooH] = FooHeader.code(); + + runAddDocument(Server, FooCpp, FooWithHeader.code()); + // LocateSymbol goes to a #include file: the result comes from the preamble. + EXPECT_THAT( + cantFail(runLocateSymbolAt(Server, FooCpp, FooWithHeader.point())), + ElementsAre(Sym("foo.h", FooHeader.range()))); + + // Only preamble is built, and no AST is built in this request. + Server.addDocument(FooCpp, FooWithoutHeader.code(), WantDiagnostics::No); + // We build AST here, and it should use the latest preamble rather than the + // stale one. + EXPECT_THAT( + cantFail(runLocateSymbolAt(Server, FooCpp, FooWithoutHeader.point())), + ElementsAre(Sym("foo", FooWithoutHeader.range()))); + + // Reset test environment. + runAddDocument(Server, FooCpp, FooWithHeader.code()); + // Both preamble and AST are built in this request. + Server.addDocument(FooCpp, FooWithoutHeader.code(), WantDiagnostics::Yes); + // Use the AST being built in above request. + EXPECT_THAT( + cantFail(runLocateSymbolAt(Server, FooCpp, FooWithoutHeader.point())), + ElementsAre(Sym("foo", FooWithoutHeader.range()))); +} + +TEST(FindReferences, WithinAST) { + const char *Tests[] = { + R"cpp(// Local variable + int main() { + int [[foo]]; + [[^foo]] = 2; + int test1 = [[foo]]; + } + )cpp", + + R"cpp(// Struct + namespace ns1 { + struct [[Foo]] {}; + } // namespace ns1 + int main() { + ns1::[[Fo^o]]* Params; + } + )cpp", + + R"cpp(// Forward declaration + class [[Foo]]; + class [[Foo]] {} + int main() { + [[Fo^o]] foo; + } + )cpp", + + R"cpp(// Function + int [[foo]](int) {} + int main() { + auto *X = &[[^foo]]; + [[foo]](42) + } + )cpp", + + R"cpp(// Field + struct Foo { + int [[foo]]; + Foo() : [[foo]](0) {} + }; + int main() { + Foo f; + f.[[f^oo]] = 1; + } + )cpp", + + R"cpp(// Method call + struct Foo { int [[foo]](); }; + int Foo::[[foo]]() {} + int main() { + Foo f; + f.[[^foo]](); + } + )cpp", + + R"cpp(// Constructor + struct Foo { + [[F^oo]](int); + }; + void foo() { + Foo f = [[Foo]](42); + } + )cpp", + + R"cpp(// Typedef + typedef int [[Foo]]; + int main() { + [[^Foo]] bar; + } + )cpp", + + R"cpp(// Namespace + namespace [[ns]] { + struct Foo {}; + } // namespace ns + int main() { [[^ns]]::Foo foo; } + )cpp", + }; + for (const char *Test : Tests) { + Annotations T(Test); + auto AST = TestTU::withCode(T.code()).build(); + std::vector<Matcher<Location>> ExpectedLocations; + for (const auto &R : T.ranges()) + ExpectedLocations.push_back(RangeIs(R)); + EXPECT_THAT(findReferences(AST, T.point(), 0), + ElementsAreArray(ExpectedLocations)) + << Test; + } +} + +TEST(FindReferences, ExplicitSymbols) { + const char *Tests[] = { + R"cpp( + struct Foo { Foo* [self]() const; }; + void f() { + if (Foo* T = foo.[^self]()) {} // Foo member call expr. + } + )cpp", + + R"cpp( + struct Foo { Foo(int); }; + Foo f() { + int [b]; + return [^b]; // Foo constructor expr. + } + )cpp", + + R"cpp( + struct Foo {}; + void g(Foo); + Foo [f](); + void call() { + g([^f]()); // Foo constructor expr. + } + )cpp", + + R"cpp( + void [foo](int); + void [foo](double); + + namespace ns { + using ::[fo^o]; + } + )cpp", + }; + for (const char *Test : Tests) { + Annotations T(Test); + auto AST = TestTU::withCode(T.code()).build(); + std::vector<Matcher<Location>> ExpectedLocations; + for (const auto &R : T.ranges()) + ExpectedLocations.push_back(RangeIs(R)); + EXPECT_THAT(findReferences(AST, T.point(), 0), + ElementsAreArray(ExpectedLocations)) + << Test; + } +} + +TEST(FindReferences, NeedsIndex) { + const char *Header = "int foo();"; + Annotations Main("int main() { [[f^oo]](); }"); + TestTU TU; + TU.Code = Main.code(); + TU.HeaderCode = Header; + auto AST = TU.build(); + + // References in main file are returned without index. + EXPECT_THAT(findReferences(AST, Main.point(), 0, /*Index=*/nullptr), + ElementsAre(RangeIs(Main.range()))); + Annotations IndexedMain(R"cpp( + int main() { [[f^oo]](); } + )cpp"); + + // References from indexed files are included. + TestTU IndexedTU; + IndexedTU.Code = IndexedMain.code(); + IndexedTU.Filename = "Indexed.cpp"; + IndexedTU.HeaderCode = Header; + EXPECT_THAT(findReferences(AST, Main.point(), 0, IndexedTU.index().get()), + ElementsAre(RangeIs(Main.range()), RangeIs(IndexedMain.range()))); + + EXPECT_EQ(1u, findReferences(AST, Main.point(), /*Limit*/ 1, + IndexedTU.index().get()) + .size()); + + // If the main file is in the index, we don't return duplicates. + // (even if the references are in a different location) + TU.Code = ("\n\n" + Main.code()).str(); + EXPECT_THAT(findReferences(AST, Main.point(), 0, TU.index().get()), + ElementsAre(RangeIs(Main.range()))); +} + +TEST(FindReferences, NoQueryForLocalSymbols) { + struct RecordingIndex : public MemIndex { + mutable Optional<llvm::DenseSet<SymbolID>> RefIDs; + void refs(const RefsRequest &Req, + llvm::function_ref<void(const Ref &)>) const override { + RefIDs = Req.IDs; + } + }; + + struct Test { + StringRef AnnotatedCode; + bool WantQuery; + } Tests[] = { + {"int ^x;", true}, + // For now we don't assume header structure which would allow skipping. + {"namespace { int ^x; }", true}, + {"static int ^x;", true}, + // Anything in a function certainly can't be referenced though. + {"void foo() { int ^x; }", false}, + {"void foo() { struct ^x{}; }", false}, + {"auto lambda = []{ int ^x; };", false}, + }; + for (Test T : Tests) { + Annotations File(T.AnnotatedCode); + RecordingIndex Rec; + auto AST = TestTU::withCode(File.code()).build(); + findReferences(AST, File.point(), 0, &Rec); + if (T.WantQuery) + EXPECT_NE(Rec.RefIDs, None) << T.AnnotatedCode; + else + EXPECT_EQ(Rec.RefIDs, None) << T.AnnotatedCode; + } +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clangd/unittests/lit.cfg.py b/clangd/unittests/lit.cfg.py new file mode 100644 index 00000000..754835e6 --- /dev/null +++ b/clangd/unittests/lit.cfg.py @@ -0,0 +1,21 @@ +import lit.formats +config.name = "Clangd Unit Tests" +config.test_format = lit.formats.GoogleTest('.', 'Tests') +config.test_source_root = config.clangd_binary_dir + "/unittests" +config.test_exec_root = config.clangd_binary_dir + "/unittests" + +# Point the dynamic loader at dynamic libraries in 'lib'. +# FIXME: it seems every project has a copy of this logic. Move it somewhere. +import platform +if platform.system() == 'Darwin': + shlibpath_var = 'DYLD_LIBRARY_PATH' +elif platform.system() == 'Windows': + shlibpath_var = 'PATH' +else: + shlibpath_var = 'LD_LIBRARY_PATH' +config.environment[shlibpath_var] = os.path.pathsep.join(( + "@SHLIBDIR@", "@LLVM_LIBS_DIR@", + config.environment.get(shlibpath_var,''))) + + + diff --git a/clangd/unittests/lit.site.cfg.py.in b/clangd/unittests/lit.site.cfg.py.in new file mode 100644 index 00000000..3fdf084a --- /dev/null +++ b/clangd/unittests/lit.site.cfg.py.in @@ -0,0 +1,11 @@ +@LIT_SITE_CFG_IN_HEADER@ +# This is a shim to run the gtest unittests in ../unittests using lit. + +config.llvm_libs_dir = "@LLVM_LIBS_DIR@" +config.shlibdir = "@SHLIBDIR@" + +config.clangd_source_dir = "@CMAKE_CURRENT_SOURCE_DIR@/.." +config.clangd_binary_dir = "@CMAKE_CURRENT_BINARY_DIR@/.." + +# Delegate logic to lit.cfg.py. +lit_config.load_config(config, "@CMAKE_CURRENT_SOURCE_DIR@/lit.cfg.py") diff --git a/clangd/unittests/xpc/CMakeLists.txt b/clangd/unittests/xpc/CMakeLists.txt new file mode 100644 index 00000000..21a1667b --- /dev/null +++ b/clangd/unittests/xpc/CMakeLists.txt @@ -0,0 +1,22 @@ +set(LLVM_LINK_COMPONENTS + support + ) + +get_filename_component(CLANGD_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/../../clangd REALPATH) +include_directories( + ${CLANGD_SOURCE_DIR} + ) + +add_custom_target(ClangdXpcUnitTests) +add_unittest(ClangdXpcUnitTests ClangdXpcTests + ConversionTests.cpp + ) + +target_link_libraries(ClangdXpcTests + PRIVATE + clangdXpcJsonConversions + clangDaemon + LLVMSupport + LLVMTestingSupport + ) diff --git a/clangd/unittests/xpc/ConversionTests.cpp b/clangd/unittests/xpc/ConversionTests.cpp new file mode 100644 index 00000000..5d0efd83 --- /dev/null +++ b/clangd/unittests/xpc/ConversionTests.cpp @@ -0,0 +1,35 @@ +//===-- ConversionTests.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 "xpc/Conversion.h" +#include "gtest/gtest.h" + +#include <limits> + +namespace clang { +namespace clangd { +namespace { + +using namespace llvm; + +TEST(JsonXpcConversionTest, JsonToXpcToJson) { + + for (auto &testcase : + {json::Value(false), json::Value(3.14), json::Value(42), + json::Value(-100), json::Value("foo"), json::Value(""), + json::Value("123"), json::Value(" "), + json::Value{true, "foo", nullptr, 42}, + json::Value(json::Object{ + {"a", true}, {"b", "foo"}, {"c", nullptr}, {"d", 42}})}) { + EXPECT_TRUE(testcase == xpcToJson(jsonToXpc(testcase))) << testcase; + } +} + +} // namespace +} // namespace clangd +} // namespace clang |