summaryrefslogtreecommitdiffstats
path: root/clangd/unittests
diff options
context:
space:
mode:
Diffstat (limited to 'clangd/unittests')
-rw-r--r--clangd/unittests/Annotations.cpp53
-rw-r--r--clangd/unittests/Annotations.h39
-rw-r--r--clangd/unittests/BackgroundIndexTests.cpp465
-rw-r--r--clangd/unittests/CMakeLists.txt97
-rw-r--r--clangd/unittests/CancellationTests.cpp65
-rw-r--r--clangd/unittests/CanonicalIncludesTests.cpp62
-rw-r--r--clangd/unittests/ClangdTests.cpp1162
-rw-r--r--clangd/unittests/ClangdUnitTests.cpp86
-rw-r--r--clangd/unittests/CodeCompleteTests.cpp2551
-rw-r--r--clangd/unittests/CodeCompletionStringsTests.cpp160
-rw-r--r--clangd/unittests/ContextTests.cpp56
-rw-r--r--clangd/unittests/DexTests.cpp753
-rw-r--r--clangd/unittests/DiagnosticsTests.cpp773
-rw-r--r--clangd/unittests/DraftStoreTests.cpp347
-rw-r--r--clangd/unittests/ExpectedTypeTest.cpp153
-rw-r--r--clangd/unittests/FSTests.cpp50
-rw-r--r--clangd/unittests/FileDistanceTests.cpp123
-rw-r--r--clangd/unittests/FileIndexTests.cpp371
-rw-r--r--clangd/unittests/FindSymbolsTests.cpp688
-rw-r--r--clangd/unittests/FunctionTests.cpp51
-rw-r--r--clangd/unittests/FuzzyMatchTests.cpp312
-rw-r--r--clangd/unittests/GlobalCompilationDatabaseTests.cpp151
-rw-r--r--clangd/unittests/HeadersTests.cpp279
-rw-r--r--clangd/unittests/IndexActionTests.cpp253
-rw-r--r--clangd/unittests/IndexTests.cpp408
-rw-r--r--clangd/unittests/JSONTransportTests.cpp205
-rw-r--r--clangd/unittests/Matchers.h199
-rw-r--r--clangd/unittests/PrintASTTests.cpp102
-rw-r--r--clangd/unittests/QualityTests.cpp493
-rw-r--r--clangd/unittests/RIFFTests.cpp37
-rw-r--r--clangd/unittests/SelectionTests.cpp259
-rw-r--r--clangd/unittests/SerializationTests.cpp220
-rw-r--r--clangd/unittests/SourceCodeTests.cpp409
-rw-r--r--clangd/unittests/SymbolCollectorTests.cpp1257
-rw-r--r--clangd/unittests/SymbolInfoTests.cpp339
-rw-r--r--clangd/unittests/SyncAPI.cpp151
-rw-r--r--clangd/unittests/SyncAPI.h59
-rw-r--r--clangd/unittests/TUSchedulerTests.cpp710
-rw-r--r--clangd/unittests/TestFS.cpp129
-rw-r--r--clangd/unittests/TestFS.h73
-rw-r--r--clangd/unittests/TestIndex.cpp118
-rw-r--r--clangd/unittests/TestIndex.h57
-rw-r--r--clangd/unittests/TestScheme.h0
-rw-r--r--clangd/unittests/TestTU.cpp157
-rw-r--r--clangd/unittests/TestTU.h84
-rw-r--r--clangd/unittests/ThreadingTests.cpp64
-rw-r--r--clangd/unittests/TraceTests.cpp127
-rw-r--r--clangd/unittests/TweakTests.cpp190
-rw-r--r--clangd/unittests/TypeHierarchyTests.cpp455
-rw-r--r--clangd/unittests/URITests.cpp187
-rw-r--r--clangd/unittests/XRefsTests.cpp1517
-rw-r--r--clangd/unittests/lit.cfg.py21
-rw-r--r--clangd/unittests/lit.site.cfg.py.in11
-rw-r--r--clangd/unittests/xpc/CMakeLists.txt22
-rw-r--r--clangd/unittests/xpc/ConversionTests.cpp35
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