Index: clang-tools-extra/trunk/clangd/CodeComplete.cpp =================================================================== --- clang-tools-extra/trunk/clangd/CodeComplete.cpp +++ clang-tools-extra/trunk/clangd/CodeComplete.cpp @@ -163,7 +163,6 @@ return Result; } - /// A scored code completion result. /// It may be promoted to a CompletionItem if it's among the top-ranked results. struct CompletionCandidate { @@ -349,9 +348,8 @@ : public CompletionItemsCollector { public: - PlainTextCompletionItemsCollector( - const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) + PlainTextCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, + CompletionList &Items) : CompletionItemsCollector(CodeCompleteOpts, Items) {} private: @@ -386,9 +384,8 @@ class SnippetCompletionItemsCollector final : public CompletionItemsCollector { public: - SnippetCompletionItemsCollector( - const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) + SnippetCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, + CompletionList &Items) : CompletionItemsCollector(CodeCompleteOpts, Items) {} private: Index: clang-tools-extra/trunk/unittests/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/unittests/clangd/CMakeLists.txt +++ clang-tools-extra/trunk/unittests/clangd/CMakeLists.txt @@ -10,8 +10,10 @@ add_extra_unittest(ClangdTests ClangdTests.cpp + CodeCompleteTests.cpp FuzzyMatchTests.cpp JSONExprTests.cpp + TestFS.cpp TraceTests.cpp ) Index: clang-tools-extra/trunk/unittests/clangd/ClangdTests.cpp =================================================================== --- clang-tools-extra/trunk/unittests/clangd/ClangdTests.cpp +++ clang-tools-extra/trunk/unittests/clangd/ClangdTests.cpp @@ -10,7 +10,7 @@ #include "ClangdLSPServer.h" #include "ClangdServer.h" #include "Logger.h" -#include "clang/Basic/VirtualFileSystem.h" +#include "TestFS.h" #include "clang/Config/config.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringMap.h" @@ -27,141 +27,9 @@ #include namespace clang { -namespace vfs { - -/// An implementation of vfs::FileSystem that only allows access to -/// files and folders inside a set of whitelisted directories. -/// -/// FIXME(ibiryukov): should it also emulate access to parents of whitelisted -/// directories with only whitelisted contents? -class FilteredFileSystem : public vfs::FileSystem { -public: - /// The paths inside \p WhitelistedDirs should be absolute - FilteredFileSystem(std::vector WhitelistedDirs, - IntrusiveRefCntPtr InnerFS) - : WhitelistedDirs(std::move(WhitelistedDirs)), InnerFS(InnerFS) { - assert(std::all_of(WhitelistedDirs.begin(), WhitelistedDirs.end(), - [](const std::string &Path) -> bool { - return llvm::sys::path::is_absolute(Path); - }) && - "Not all WhitelistedDirs are absolute"); - } - - virtual llvm::ErrorOr status(const Twine &Path) { - if (!isInsideWhitelistedDir(Path)) - return llvm::errc::no_such_file_or_directory; - return InnerFS->status(Path); - } - - virtual llvm::ErrorOr> - openFileForRead(const Twine &Path) { - if (!isInsideWhitelistedDir(Path)) - return llvm::errc::no_such_file_or_directory; - return InnerFS->openFileForRead(Path); - } - - llvm::ErrorOr> - getBufferForFile(const Twine &Name, int64_t FileSize = -1, - bool RequiresNullTerminator = true, - bool IsVolatile = false) { - if (!isInsideWhitelistedDir(Name)) - return llvm::errc::no_such_file_or_directory; - return InnerFS->getBufferForFile(Name, FileSize, RequiresNullTerminator, - IsVolatile); - } - - virtual directory_iterator dir_begin(const Twine &Dir, std::error_code &EC) { - if (!isInsideWhitelistedDir(Dir)) { - EC = llvm::errc::no_such_file_or_directory; - return directory_iterator(); - } - return InnerFS->dir_begin(Dir, EC); - } - - virtual std::error_code setCurrentWorkingDirectory(const Twine &Path) { - return InnerFS->setCurrentWorkingDirectory(Path); - } - - virtual llvm::ErrorOr getCurrentWorkingDirectory() const { - return InnerFS->getCurrentWorkingDirectory(); - } - - bool exists(const Twine &Path) { - if (!isInsideWhitelistedDir(Path)) - return false; - return InnerFS->exists(Path); - } - - std::error_code makeAbsolute(SmallVectorImpl &Path) const { - return InnerFS->makeAbsolute(Path); - } - -private: - bool isInsideWhitelistedDir(const Twine &InputPath) const { - SmallString<128> Path; - InputPath.toVector(Path); - - if (makeAbsolute(Path)) - return false; - - for (const auto &Dir : WhitelistedDirs) { - if (Path.startswith(Dir)) - return true; - } - return false; - } - - std::vector WhitelistedDirs; - IntrusiveRefCntPtr InnerFS; -}; - -/// Create a vfs::FileSystem that has access only to temporary directories -/// (obtained by calling system_temp_directory). -IntrusiveRefCntPtr getTempOnlyFS() { - llvm::SmallString<128> TmpDir1; - llvm::sys::path::system_temp_directory(/*erasedOnReboot=*/false, TmpDir1); - llvm::SmallString<128> TmpDir2; - llvm::sys::path::system_temp_directory(/*erasedOnReboot=*/true, TmpDir2); - - std::vector TmpDirs; - TmpDirs.push_back(TmpDir1.str()); - if (TmpDir1 != TmpDir2) - TmpDirs.push_back(TmpDir2.str()); - return new vfs::FilteredFileSystem(std::move(TmpDirs), - vfs::getRealFileSystem()); -} -} // namespace vfs - namespace clangd { namespace { -struct StringWithPos { - std::string Text; - clangd::Position MarkerPos; -}; - -/// Returns location of "{mark}" substring in \p Text and removes it from \p -/// Text. Note that \p Text must contain exactly one occurence of "{mark}". -/// -/// Marker name can be configured using \p MarkerName parameter. -StringWithPos parseTextMarker(StringRef Text, StringRef MarkerName = "mark") { - SmallString<16> Marker; - Twine("{" + MarkerName + "}").toVector(/*ref*/ Marker); - - std::size_t MarkerOffset = Text.find(Marker); - assert(MarkerOffset != StringRef::npos && "{mark} wasn't found in Text."); - - std::string WithoutMarker; - WithoutMarker += Text.take_front(MarkerOffset); - WithoutMarker += Text.drop_front(MarkerOffset + Marker.size()); - assert(StringRef(WithoutMarker).find(Marker) == StringRef::npos && - "There were multiple occurences of {mark} inside Text"); - - clangd::Position MarkerPos = - clangd::offsetToPosition(WithoutMarker, MarkerOffset); - return {std::move(WithoutMarker), MarkerPos}; -} - // Don't wait for async ops in clangd test more than that to avoid blocking // indefinitely in case of bugs. static const std::chrono::seconds DefaultFutureTimeout = @@ -203,46 +71,6 @@ VFSTag LastVFSTag = VFSTag(); }; -class MockCompilationDatabase : public GlobalCompilationDatabase { -public: - MockCompilationDatabase(bool AddFreestandingFlag) { - // We have to add -ffreestanding to VFS-specific tests to avoid errors on - // implicit includes of stdc-predef.h. - if (AddFreestandingFlag) - ExtraClangFlags.push_back("-ffreestanding"); - } - - llvm::Optional - getCompileCommand(PathRef File) const override { - if (ExtraClangFlags.empty()) - return llvm::None; - - auto CommandLine = ExtraClangFlags; - CommandLine.insert(CommandLine.begin(), "clang"); - CommandLine.insert(CommandLine.end(), File.str()); - return {tooling::CompileCommand(llvm::sys::path::parent_path(File), - llvm::sys::path::filename(File), - std::move(CommandLine), "")}; - } - - std::vector ExtraClangFlags; -}; - -IntrusiveRefCntPtr -buildTestFS(llvm::StringMap const &Files) { - IntrusiveRefCntPtr MemFS( - new vfs::InMemoryFileSystem); - for (auto &FileAndContents : Files) - MemFS->addFile(FileAndContents.first(), time_t(), - llvm::MemoryBuffer::getMemBuffer(FileAndContents.second, - FileAndContents.first())); - - auto OverlayFS = IntrusiveRefCntPtr( - new vfs::OverlayFileSystem(vfs::getTempOnlyFS())); - OverlayFS->pushOverlay(std::move(MemFS)); - return OverlayFS; -} - class ConstantFSProvider : public FileSystemProvider { public: ConstantFSProvider(IntrusiveRefCntPtr FS, @@ -259,23 +87,6 @@ VFSTag Tag; }; -class MockFSProvider : public FileSystemProvider { -public: - Tagged> - getTaggedFileSystem(PathRef File) override { - if (ExpectedFile) { - EXPECT_EQ(*ExpectedFile, File); - } - - auto FS = buildTestFS(Files); - return make_tagged(FS, Tag); - } - - llvm::Optional> ExpectedFile; - llvm::StringMap Files; - VFSTag Tag = VFSTag(); -}; - /// Replaces all patterns of the form 0x123abc with spaces std::string replacePtrsInDump(std::string const &Dump) { llvm::Regex RE("0x[0-9a-fA-F]+"); @@ -304,22 +115,6 @@ class ClangdVFSTest : public ::testing::Test { protected: - SmallString<16> getVirtualTestRoot() { -#ifdef LLVM_ON_WIN32 - return SmallString<16>("C:\\clangd-test"); -#else - return SmallString<16>("/clangd-test"); -#endif - } - - llvm::SmallString<32> getVirtualTestFilePath(PathRef File) { - assert(llvm::sys::path::is_relative(File) && "FileName should be relative"); - - llvm::SmallString<32> Path; - llvm::sys::path::append(Path, getVirtualTestRoot(), File); - return Path; - } - std::string parseSourceAndDumpAST( PathRef SourceFileRelPath, StringRef SourceContents, std::vector> ExtraFiles = {}, @@ -621,352 +416,6 @@ EXPECT_FALSE(DiagConsumer.hadErrorInLastDiags()); } -class ClangdCompletionTest : public ClangdVFSTest { -protected: - template - bool ContainsItemPred(CompletionList const &Items, Predicate Pred) { - for (const auto &Item : Items.items) { - if (Pred(Item)) - return true; - } - return false; - } - - bool ContainsItem(CompletionList const &Items, StringRef Name) { - return ContainsItemPred(Items, [Name](clangd::CompletionItem Item) { - return Item.insertText == Name; - }); - return false; - } -}; - -TEST_F(ClangdCompletionTest, CheckContentsOverride) { - MockFSProvider FS; - ErrorCheckingDiagConsumer DiagConsumer; - MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); - - ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), - /*StorePreamblesInMemory=*/true, - clangd::CodeCompleteOptions(), - EmptyLogger::getInstance()); - - auto FooCpp = getVirtualTestFilePath("foo.cpp"); - const auto SourceContents = R"cpp( -int aba; -int b = ; -)cpp"; - - const auto OverridenSourceContents = R"cpp( -int cbc; -int b = ; -)cpp"; - // Complete after '=' sign. We need to be careful to keep the SourceContents' - // size the same. - // We complete on the 3rd line (2nd in zero-based numbering), because raw - // string literal of the SourceContents starts with a newline(it's easy to - // miss). - Position CompletePos = {2, 8}; - FS.Files[FooCpp] = SourceContents; - FS.ExpectedFile = FooCpp; - - // No need to sync reparses here as there are no asserts on diagnostics (or - // other async operations). - Server.addDocument(FooCpp, SourceContents); - - { - auto CodeCompletionResults1 = - Server.codeComplete(FooCpp, CompletePos, None).get().Value; - EXPECT_TRUE(ContainsItem(CodeCompletionResults1, "aba")); - EXPECT_FALSE(ContainsItem(CodeCompletionResults1, "cbc")); - } - - { - auto CodeCompletionResultsOverriden = - Server - .codeComplete(FooCpp, CompletePos, - StringRef(OverridenSourceContents)) - .get() - .Value; - EXPECT_TRUE(ContainsItem(CodeCompletionResultsOverriden, "cbc")); - EXPECT_FALSE(ContainsItem(CodeCompletionResultsOverriden, "aba")); - } - - { - auto CodeCompletionResults2 = - Server.codeComplete(FooCpp, CompletePos, None).get().Value; - EXPECT_TRUE(ContainsItem(CodeCompletionResults2, "aba")); - EXPECT_FALSE(ContainsItem(CodeCompletionResults2, "cbc")); - } -} - -TEST_F(ClangdCompletionTest, Limit) { - MockFSProvider FS; - MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); - CDB.ExtraClangFlags.push_back("-xc++"); - ErrorCheckingDiagConsumer DiagConsumer; - clangd::CodeCompleteOptions Opts; - Opts.Limit = 2; - ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), - /*StorePreamblesInMemory=*/true, Opts, - EmptyLogger::getInstance()); - - auto FooCpp = getVirtualTestFilePath("foo.cpp"); - FS.Files[FooCpp] = ""; - FS.ExpectedFile = FooCpp; - StringWithPos Completion = parseTextMarker(R"cpp( -struct ClassWithMembers { - int AAA(); - int BBB(); - int CCC(); -} -int main() { ClassWithMembers().{complete} } - )cpp", - "complete"); - Server.addDocument(FooCpp, Completion.Text); - - /// For after-dot completion we must always get consistent results. - auto Results = Server - .codeComplete(FooCpp, Completion.MarkerPos, - StringRef(Completion.Text)) - .get() - .Value; - - EXPECT_TRUE(Results.isIncomplete); - EXPECT_EQ(Opts.Limit, Results.items.size()); - EXPECT_TRUE(ContainsItem(Results, "AAA")); - EXPECT_TRUE(ContainsItem(Results, "BBB")); - EXPECT_FALSE(ContainsItem(Results, "CCC")); -} - -TEST_F(ClangdCompletionTest, Filter) { - MockFSProvider FS; - MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); - CDB.ExtraClangFlags.push_back("-xc++"); - ErrorCheckingDiagConsumer DiagConsumer; - clangd::CodeCompleteOptions Opts; - ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), - /*StorePreamblesInMemory=*/true, Opts, - EmptyLogger::getInstance()); - - auto FooCpp = getVirtualTestFilePath("foo.cpp"); - FS.Files[FooCpp] = ""; - FS.ExpectedFile = FooCpp; - const char *Body = R"cpp( - int Abracadabra; - int Alakazam; - struct S { - int FooBar; - int FooBaz; - int Qux; - }; - )cpp"; - auto Complete = [&](StringRef Query) { - StringWithPos Completion = parseTextMarker( - formatv("{0} int main() { {1}{{complete}} }", Body, Query).str(), - "complete"); - Server.addDocument(FooCpp, Completion.Text); - return Server - .codeComplete(FooCpp, Completion.MarkerPos, StringRef(Completion.Text)) - .get() - .Value; - }; - - auto Foba = Complete("S().Foba"); - EXPECT_TRUE(ContainsItem(Foba, "FooBar")); - EXPECT_TRUE(ContainsItem(Foba, "FooBaz")); - EXPECT_FALSE(ContainsItem(Foba, "Qux")); - - auto FR = Complete("S().FR"); - EXPECT_TRUE(ContainsItem(FR, "FooBar")); - EXPECT_FALSE(ContainsItem(FR, "FooBaz")); - EXPECT_FALSE(ContainsItem(FR, "Qux")); - - auto Op = Complete("S().opr"); - EXPECT_TRUE(ContainsItem(Op, "operator=")); - - auto Aaa = Complete("aaa"); - EXPECT_TRUE(ContainsItem(Aaa, "Abracadabra")); - EXPECT_TRUE(ContainsItem(Aaa, "Alakazam")); - - auto UA = Complete("_a"); - EXPECT_TRUE(ContainsItem(UA, "static_cast")); - EXPECT_FALSE(ContainsItem(UA, "Abracadabra")); -} - -TEST_F(ClangdCompletionTest, CompletionOptions) { - MockFSProvider FS; - ErrorCheckingDiagConsumer DiagConsumer; - MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); - CDB.ExtraClangFlags.push_back("-xc++"); - - auto FooCpp = getVirtualTestFilePath("foo.cpp"); - FS.Files[FooCpp] = ""; - FS.ExpectedFile = FooCpp; - - const auto GlobalCompletionSourceTemplate = R"cpp( -#define MACRO X - -int global_var; -int global_func(); - -struct GlobalClass {}; - -struct ClassWithMembers { - /// Doc for method. - int method(); -}; - -int test() { - struct LocalClass {}; - - /// Doc for local_var. - int local_var; - - {complete} -} -)cpp"; - const auto MemberCompletionSourceTemplate = R"cpp( -#define MACRO X - -int global_var; - -int global_func(); - -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().{complete} -} -)cpp"; - - StringWithPos GlobalCompletion = - parseTextMarker(GlobalCompletionSourceTemplate, "complete"); - StringWithPos MemberCompletion = - parseTextMarker(MemberCompletionSourceTemplate, "complete"); - - auto TestWithOpts = [&](clangd::CodeCompleteOptions Opts) { - ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), - /*StorePreamblesInMemory=*/true, Opts, - EmptyLogger::getInstance()); - // No need to sync reparses here as there are no asserts on diagnostics (or - // other async operations). - Server.addDocument(FooCpp, GlobalCompletion.Text); - - StringRef MethodItemText = Opts.EnableSnippets ? "method()" : "method"; - StringRef GlobalFuncItemText = - Opts.EnableSnippets ? "global_func()" : "global_func"; - - /// For after-dot completion we must always get consistent results. - { - auto Results = Server - .codeComplete(FooCpp, MemberCompletion.MarkerPos, - StringRef(MemberCompletion.Text)) - .get() - .Value; - - // Class members. The only items that must be present in after-dor - // completion. - EXPECT_TRUE(ContainsItem(Results, MethodItemText)); - EXPECT_TRUE(ContainsItem(Results, MethodItemText)); - EXPECT_TRUE(ContainsItem(Results, "field")); - EXPECT_EQ(Opts.IncludeIneligibleResults, - ContainsItem(Results, "private_field")); - // Global items. - EXPECT_FALSE(ContainsItem(Results, "global_var")); - EXPECT_FALSE(ContainsItem(Results, GlobalFuncItemText)); - EXPECT_FALSE(ContainsItem(Results, "GlobalClass")); - // A macro. - EXPECT_FALSE(ContainsItem(Results, "MACRO")); - // Local items. - EXPECT_FALSE(ContainsItem(Results, "LocalClass")); - // There should be no code patterns (aka snippets) in after-dot - // completion. At least there aren't any we're aware of. - EXPECT_FALSE( - ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { - return Item.kind == clangd::CompletionItemKind::Snippet; - })); - // Check documentation. - EXPECT_EQ( - Opts.IncludeBriefComments, - ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { - return !Item.documentation.empty(); - })); - } - // Global completion differs based on the Opts that were passed. - { - auto Results = Server - .codeComplete(FooCpp, GlobalCompletion.MarkerPos, - StringRef(GlobalCompletion.Text)) - .get() - .Value; - - // Class members. Should never be present in global completions. - EXPECT_FALSE(ContainsItem(Results, MethodItemText)); - EXPECT_FALSE(ContainsItem(Results, "field")); - // Global items. - EXPECT_EQ(ContainsItem(Results, "global_var"), Opts.IncludeGlobals); - EXPECT_EQ(ContainsItem(Results, GlobalFuncItemText), Opts.IncludeGlobals); - EXPECT_EQ(ContainsItem(Results, "GlobalClass"), Opts.IncludeGlobals); - // A macro. - EXPECT_EQ(ContainsItem(Results, "MACRO"), Opts.IncludeMacros); - // Local items. Must be present always. - EXPECT_TRUE(ContainsItem(Results, "local_var")); - EXPECT_TRUE(ContainsItem(Results, "LocalClass")); - // FIXME(ibiryukov): snippets have wrong Item.kind now. Reenable this - // check after https://reviews.llvm.org/D38720 makes it in. - // - // Code patterns (aka snippets). - // EXPECT_EQ( - // Opts.IncludeCodePatterns && Opts.EnableSnippets, - // ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { - // return Item.kind == clangd::CompletionItemKind::Snippet; - // })); - - // Check documentation. - EXPECT_EQ( - Opts.IncludeBriefComments, - ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { - return !Item.documentation.empty(); - })); - } - }; - - clangd::CodeCompleteOptions CCOpts; - for (bool IncludeMacros : {true, false}){ - CCOpts.IncludeMacros = IncludeMacros; - for (bool IncludeGlobals : {true, false}){ - CCOpts.IncludeGlobals = IncludeGlobals; - for (bool IncludeBriefComments : {true, false}){ - CCOpts.IncludeBriefComments = IncludeBriefComments; - for (bool EnableSnippets : {true, false}){ - CCOpts.EnableSnippets = EnableSnippets; - for (bool IncludeCodePatterns : {true, false}) { - CCOpts.IncludeCodePatterns = IncludeCodePatterns; - for (bool IncludeIneligibleResults : {true, false}) { - CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; - TestWithOpts(CCOpts); - } - } - } - } - } - } -} - class ClangdThreadingTest : public ClangdVFSTest {}; TEST_F(ClangdThreadingTest, StressTest) { Index: clang-tools-extra/trunk/unittests/clangd/CodeCompleteTests.cpp =================================================================== --- clang-tools-extra/trunk/unittests/clangd/CodeCompleteTests.cpp +++ clang-tools-extra/trunk/unittests/clangd/CodeCompleteTests.cpp @@ -0,0 +1,400 @@ +//===-- CodeCompleteTests.cpp -----------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "ClangdServer.h" +#include "Compiler.h" +#include "Protocol.h" +#include "TestFS.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { +using namespace llvm; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady( + PathRef File, Tagged> Diagnostics) override {} +}; + +struct StringWithPos { + std::string Text; + clangd::Position MarkerPos; +}; + +/// Returns location of "{mark}" substring in \p Text and removes it from \p +/// Text. Note that \p Text must contain exactly one occurence of "{mark}". +/// +/// Marker name can be configured using \p MarkerName parameter. +StringWithPos parseTextMarker(StringRef Text, StringRef MarkerName = "mark") { + SmallString<16> Marker; + Twine("{" + MarkerName + "}").toVector(/*ref*/ Marker); + + std::size_t MarkerOffset = Text.find(Marker); + assert(MarkerOffset != StringRef::npos && "{mark} wasn't found in Text."); + + std::string WithoutMarker; + WithoutMarker += Text.take_front(MarkerOffset); + WithoutMarker += Text.drop_front(MarkerOffset + Marker.size()); + assert(StringRef(WithoutMarker).find(Marker) == StringRef::npos && + "There were multiple occurences of {mark} inside Text"); + + clangd::Position MarkerPos = + clangd::offsetToPosition(WithoutMarker, MarkerOffset); + return {std::move(WithoutMarker), MarkerPos}; +} + +class ClangdCompletionTest : public ::testing::Test { +protected: + template + bool ContainsItemPred(CompletionList const &Items, Predicate Pred) { + for (const auto &Item : Items.items) { + if (Pred(Item)) + return true; + } + return false; + } + + bool ContainsItem(CompletionList const &Items, StringRef Name) { + return ContainsItemPred(Items, [Name](clangd::CompletionItem Item) { + return Item.insertText == Name; + }); + return false; + } +}; + +TEST_F(ClangdCompletionTest, CheckContentsOverride) { + MockFSProvider FS; + IgnoreDiagnostics DiagConsumer; + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + + ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, + clangd::CodeCompleteOptions(), + EmptyLogger::getInstance()); + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + const auto SourceContents = R"cpp( +int aba; +int b = ; +)cpp"; + + const auto OverridenSourceContents = R"cpp( +int cbc; +int b = ; +)cpp"; + // Complete after '=' sign. We need to be careful to keep the SourceContents' + // size the same. + // We complete on the 3rd line (2nd in zero-based numbering), because raw + // string literal of the SourceContents starts with a newline(it's easy to + // miss). + Position CompletePos = {2, 8}; + FS.Files[FooCpp] = SourceContents; + FS.ExpectedFile = FooCpp; + + // No need to sync reparses here as there are no asserts on diagnostics (or + // other async operations). + Server.addDocument(FooCpp, SourceContents); + + { + auto CodeCompletionResults1 = + Server.codeComplete(FooCpp, CompletePos, None).get().Value; + EXPECT_TRUE(ContainsItem(CodeCompletionResults1, "aba")); + EXPECT_FALSE(ContainsItem(CodeCompletionResults1, "cbc")); + } + + { + auto CodeCompletionResultsOverriden = + Server + .codeComplete(FooCpp, CompletePos, + StringRef(OverridenSourceContents)) + .get() + .Value; + EXPECT_TRUE(ContainsItem(CodeCompletionResultsOverriden, "cbc")); + EXPECT_FALSE(ContainsItem(CodeCompletionResultsOverriden, "aba")); + } + + { + auto CodeCompletionResults2 = + Server.codeComplete(FooCpp, CompletePos, None).get().Value; + EXPECT_TRUE(ContainsItem(CodeCompletionResults2, "aba")); + EXPECT_FALSE(ContainsItem(CodeCompletionResults2, "cbc")); + } +} + +TEST_F(ClangdCompletionTest, Limit) { + MockFSProvider FS; + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + CDB.ExtraClangFlags.push_back("-xc++"); + IgnoreDiagnostics DiagConsumer; + clangd::CodeCompleteOptions Opts; + Opts.Limit = 2; + ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, Opts, + EmptyLogger::getInstance()); + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + FS.Files[FooCpp] = ""; + FS.ExpectedFile = FooCpp; + StringWithPos Completion = parseTextMarker(R"cpp( +struct ClassWithMembers { + int AAA(); + int BBB(); + int CCC(); +} +int main() { ClassWithMembers().{complete} } + )cpp", + "complete"); + Server.addDocument(FooCpp, Completion.Text); + + /// For after-dot completion we must always get consistent results. + auto Results = Server + .codeComplete(FooCpp, Completion.MarkerPos, + StringRef(Completion.Text)) + .get() + .Value; + + EXPECT_TRUE(Results.isIncomplete); + EXPECT_EQ(Opts.Limit, Results.items.size()); + EXPECT_TRUE(ContainsItem(Results, "AAA")); + EXPECT_TRUE(ContainsItem(Results, "BBB")); + EXPECT_FALSE(ContainsItem(Results, "CCC")); +} + +TEST_F(ClangdCompletionTest, Filter) { + MockFSProvider FS; + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + CDB.ExtraClangFlags.push_back("-xc++"); + IgnoreDiagnostics DiagConsumer; + clangd::CodeCompleteOptions Opts; + ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, Opts, + EmptyLogger::getInstance()); + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + FS.Files[FooCpp] = ""; + FS.ExpectedFile = FooCpp; + const char *Body = R"cpp( + int Abracadabra; + int Alakazam; + struct S { + int FooBar; + int FooBaz; + int Qux; + }; + )cpp"; + auto Complete = [&](StringRef Query) { + StringWithPos Completion = parseTextMarker( + formatv("{0} int main() { {1}{{complete}} }", Body, Query).str(), + "complete"); + Server.addDocument(FooCpp, Completion.Text); + return Server + .codeComplete(FooCpp, Completion.MarkerPos, StringRef(Completion.Text)) + .get() + .Value; + }; + + auto Foba = Complete("S().Foba"); + EXPECT_TRUE(ContainsItem(Foba, "FooBar")); + EXPECT_TRUE(ContainsItem(Foba, "FooBaz")); + EXPECT_FALSE(ContainsItem(Foba, "Qux")); + + auto FR = Complete("S().FR"); + EXPECT_TRUE(ContainsItem(FR, "FooBar")); + EXPECT_FALSE(ContainsItem(FR, "FooBaz")); + EXPECT_FALSE(ContainsItem(FR, "Qux")); + + auto Op = Complete("S().opr"); + EXPECT_TRUE(ContainsItem(Op, "operator=")); + + auto Aaa = Complete("aaa"); + EXPECT_TRUE(ContainsItem(Aaa, "Abracadabra")); + EXPECT_TRUE(ContainsItem(Aaa, "Alakazam")); + + auto UA = Complete("_a"); + EXPECT_TRUE(ContainsItem(UA, "static_cast")); + EXPECT_FALSE(ContainsItem(UA, "Abracadabra")); +} + +TEST_F(ClangdCompletionTest, CompletionOptions) { + MockFSProvider FS; + IgnoreDiagnostics DiagConsumer; + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + CDB.ExtraClangFlags.push_back("-xc++"); + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + FS.Files[FooCpp] = ""; + FS.ExpectedFile = FooCpp; + + const auto GlobalCompletionSourceTemplate = R"cpp( +#define MACRO X + +int global_var; +int global_func(); + +struct GlobalClass {}; + +struct ClassWithMembers { + /// Doc for method. + int method(); +}; + +int test() { + struct LocalClass {}; + + /// Doc for local_var. + int local_var; + + {complete} +} +)cpp"; + const auto MemberCompletionSourceTemplate = R"cpp( +#define MACRO X + +int global_var; + +int global_func(); + +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().{complete} +} +)cpp"; + + StringWithPos GlobalCompletion = + parseTextMarker(GlobalCompletionSourceTemplate, "complete"); + StringWithPos MemberCompletion = + parseTextMarker(MemberCompletionSourceTemplate, "complete"); + + auto TestWithOpts = [&](clangd::CodeCompleteOptions Opts) { + ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), + /*StorePreamblesInMemory=*/true, Opts, + EmptyLogger::getInstance()); + // No need to sync reparses here as there are no asserts on diagnostics (or + // other async operations). + Server.addDocument(FooCpp, GlobalCompletion.Text); + + StringRef MethodItemText = Opts.EnableSnippets ? "method()" : "method"; + StringRef GlobalFuncItemText = + Opts.EnableSnippets ? "global_func()" : "global_func"; + + /// For after-dot completion we must always get consistent results. + { + auto Results = Server + .codeComplete(FooCpp, MemberCompletion.MarkerPos, + StringRef(MemberCompletion.Text)) + .get() + .Value; + + // Class members. The only items that must be present in after-dor + // completion. + EXPECT_TRUE(ContainsItem(Results, MethodItemText)); + EXPECT_TRUE(ContainsItem(Results, MethodItemText)); + EXPECT_TRUE(ContainsItem(Results, "field")); + EXPECT_EQ(Opts.IncludeIneligibleResults, + ContainsItem(Results, "private_field")); + // Global items. + EXPECT_FALSE(ContainsItem(Results, "global_var")); + EXPECT_FALSE(ContainsItem(Results, GlobalFuncItemText)); + EXPECT_FALSE(ContainsItem(Results, "GlobalClass")); + // A macro. + EXPECT_FALSE(ContainsItem(Results, "MACRO")); + // Local items. + EXPECT_FALSE(ContainsItem(Results, "LocalClass")); + // There should be no code patterns (aka snippets) in after-dot + // completion. At least there aren't any we're aware of. + EXPECT_FALSE( + ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { + return Item.kind == clangd::CompletionItemKind::Snippet; + })); + // Check documentation. + EXPECT_EQ( + Opts.IncludeBriefComments, + ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { + return !Item.documentation.empty(); + })); + } + // Global completion differs based on the Opts that were passed. + { + auto Results = Server + .codeComplete(FooCpp, GlobalCompletion.MarkerPos, + StringRef(GlobalCompletion.Text)) + .get() + .Value; + + // Class members. Should never be present in global completions. + EXPECT_FALSE(ContainsItem(Results, MethodItemText)); + EXPECT_FALSE(ContainsItem(Results, "field")); + // Global items. + EXPECT_EQ(ContainsItem(Results, "global_var"), Opts.IncludeGlobals); + EXPECT_EQ(ContainsItem(Results, GlobalFuncItemText), Opts.IncludeGlobals); + EXPECT_EQ(ContainsItem(Results, "GlobalClass"), Opts.IncludeGlobals); + // A macro. + EXPECT_EQ(ContainsItem(Results, "MACRO"), Opts.IncludeMacros); + // Local items. Must be present always. + EXPECT_TRUE(ContainsItem(Results, "local_var")); + EXPECT_TRUE(ContainsItem(Results, "LocalClass")); + // FIXME(ibiryukov): snippets have wrong Item.kind now. Reenable this + // check after https://reviews.llvm.org/D38720 makes it in. + // + // Code patterns (aka snippets). + // EXPECT_EQ( + // Opts.IncludeCodePatterns && Opts.EnableSnippets, + // ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { + // return Item.kind == clangd::CompletionItemKind::Snippet; + // })); + + // Check documentation. + EXPECT_EQ( + Opts.IncludeBriefComments, + ContainsItemPred(Results, [](clangd::CompletionItem const &Item) { + return !Item.documentation.empty(); + })); + } + }; + + clangd::CodeCompleteOptions CCOpts; + for (bool IncludeMacros : {true, false}) { + CCOpts.IncludeMacros = IncludeMacros; + for (bool IncludeGlobals : {true, false}) { + CCOpts.IncludeGlobals = IncludeGlobals; + for (bool IncludeBriefComments : {true, false}) { + CCOpts.IncludeBriefComments = IncludeBriefComments; + for (bool EnableSnippets : {true, false}) { + CCOpts.EnableSnippets = EnableSnippets; + for (bool IncludeCodePatterns : {true, false}) { + CCOpts.IncludeCodePatterns = IncludeCodePatterns; + for (bool IncludeIneligibleResults : {true, false}) { + CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; + TestWithOpts(CCOpts); + } + } + } + } + } + } +} + +} // namespace +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/unittests/clangd/TestFS.h =================================================================== --- clang-tools-extra/trunk/unittests/clangd/TestFS.h +++ clang-tools-extra/trunk/unittests/clangd/TestFS.h @@ -0,0 +1,57 @@ +//===-- TestFS.h ------------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// Allows setting up fake filesystem environments for tests. +// +//===----------------------------------------------------------------------===// +#include "ClangdServer.h" +#include "clang/Basic/VirtualFileSystem.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/Support/Path.h" + +namespace clang { +namespace clangd { + +// Builds a VFS that provides access to the provided files, plus temporary +// directories. +llvm::IntrusiveRefCntPtr +buildTestFS(llvm::StringMap const &Files); + +// A VFS provider that returns TestFSes containing a provided set of files. +class MockFSProvider : public FileSystemProvider { +public: + Tagged> + getTaggedFileSystem(PathRef File) override; + + llvm::Optional> ExpectedFile; + llvm::StringMap Files; + VFSTag Tag = VFSTag(); +}; + +// A Compilation database that returns a fixed set of compile flags. +class MockCompilationDatabase : public GlobalCompilationDatabase { +public: + MockCompilationDatabase(bool AddFreestandingFlag) { + // We have to add -ffreestanding to VFS-specific tests to avoid errors on + // implicit includes of stdc-predef.h. + if (AddFreestandingFlag) + ExtraClangFlags.push_back("-ffreestanding"); + } + + llvm::Optional + getCompileCommand(PathRef File) const override; + + std::vector ExtraClangFlags; +}; + +// Returns a suitable absolute path for this OS. +llvm::SmallString<32> getVirtualTestFilePath(PathRef File); + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/trunk/unittests/clangd/TestFS.cpp =================================================================== --- clang-tools-extra/trunk/unittests/clangd/TestFS.cpp +++ clang-tools-extra/trunk/unittests/clangd/TestFS.cpp @@ -0,0 +1,176 @@ +//===-- TestFS.cpp ----------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "TestFS.h" +#include "llvm/Support/Errc.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +/// An implementation of vfs::FileSystem that only allows access to +/// files and folders inside a set of whitelisted directories. +/// +/// FIXME(ibiryukov): should it also emulate access to parents of whitelisted +/// directories with only whitelisted contents? +class FilteredFileSystem : public vfs::FileSystem { +public: + /// The paths inside \p WhitelistedDirs should be absolute + FilteredFileSystem(std::vector WhitelistedDirs, + IntrusiveRefCntPtr InnerFS) + : WhitelistedDirs(std::move(WhitelistedDirs)), InnerFS(InnerFS) { + assert(std::all_of(WhitelistedDirs.begin(), WhitelistedDirs.end(), + [](const std::string &Path) -> bool { + return llvm::sys::path::is_absolute(Path); + }) && + "Not all WhitelistedDirs are absolute"); + } + + virtual llvm::ErrorOr status(const Twine &Path) { + if (!isInsideWhitelistedDir(Path)) + return llvm::errc::no_such_file_or_directory; + return InnerFS->status(Path); + } + + virtual llvm::ErrorOr> + openFileForRead(const Twine &Path) { + if (!isInsideWhitelistedDir(Path)) + return llvm::errc::no_such_file_or_directory; + return InnerFS->openFileForRead(Path); + } + + llvm::ErrorOr> + getBufferForFile(const Twine &Name, int64_t FileSize = -1, + bool RequiresNullTerminator = true, + bool IsVolatile = false) { + if (!isInsideWhitelistedDir(Name)) + return llvm::errc::no_such_file_or_directory; + return InnerFS->getBufferForFile(Name, FileSize, RequiresNullTerminator, + IsVolatile); + } + + virtual vfs::directory_iterator dir_begin(const Twine &Dir, + std::error_code &EC) { + if (!isInsideWhitelistedDir(Dir)) { + EC = llvm::errc::no_such_file_or_directory; + return vfs::directory_iterator(); + } + return InnerFS->dir_begin(Dir, EC); + } + + virtual std::error_code setCurrentWorkingDirectory(const Twine &Path) { + return InnerFS->setCurrentWorkingDirectory(Path); + } + + virtual llvm::ErrorOr getCurrentWorkingDirectory() const { + return InnerFS->getCurrentWorkingDirectory(); + } + + bool exists(const Twine &Path) { + if (!isInsideWhitelistedDir(Path)) + return false; + return InnerFS->exists(Path); + } + + std::error_code makeAbsolute(SmallVectorImpl &Path) const { + return InnerFS->makeAbsolute(Path); + } + +private: + bool isInsideWhitelistedDir(const Twine &InputPath) const { + SmallString<128> Path; + InputPath.toVector(Path); + + if (makeAbsolute(Path)) + return false; + + for (const auto &Dir : WhitelistedDirs) { + if (Path.startswith(Dir)) + return true; + } + return false; + } + + std::vector WhitelistedDirs; + IntrusiveRefCntPtr InnerFS; +}; + +/// Create a vfs::FileSystem that has access only to temporary directories +/// (obtained by calling system_temp_directory). +IntrusiveRefCntPtr getTempOnlyFS() { + llvm::SmallString<128> TmpDir1; + llvm::sys::path::system_temp_directory(/*erasedOnReboot=*/false, TmpDir1); + llvm::SmallString<128> TmpDir2; + llvm::sys::path::system_temp_directory(/*erasedOnReboot=*/true, TmpDir2); + + std::vector TmpDirs; + TmpDirs.push_back(TmpDir1.str()); + if (TmpDir1 != TmpDir2) + TmpDirs.push_back(TmpDir2.str()); + return new FilteredFileSystem(std::move(TmpDirs), vfs::getRealFileSystem()); +} + +} // namespace + +IntrusiveRefCntPtr +buildTestFS(llvm::StringMap const &Files) { + IntrusiveRefCntPtr MemFS( + new vfs::InMemoryFileSystem); + for (auto &FileAndContents : Files) + MemFS->addFile(FileAndContents.first(), time_t(), + llvm::MemoryBuffer::getMemBuffer(FileAndContents.second, + FileAndContents.first())); + + auto OverlayFS = IntrusiveRefCntPtr( + new vfs::OverlayFileSystem(getTempOnlyFS())); + OverlayFS->pushOverlay(std::move(MemFS)); + return OverlayFS; +} + +Tagged> +MockFSProvider::getTaggedFileSystem(PathRef File) { + if (ExpectedFile) { + EXPECT_EQ(*ExpectedFile, File); + } + + auto FS = buildTestFS(Files); + return make_tagged(FS, Tag); +} + +llvm::Optional +MockCompilationDatabase::getCompileCommand(PathRef File) const { + if (ExtraClangFlags.empty()) + return llvm::None; + + auto CommandLine = ExtraClangFlags; + CommandLine.insert(CommandLine.begin(), "clang"); + CommandLine.insert(CommandLine.end(), File.str()); + return {tooling::CompileCommand(llvm::sys::path::parent_path(File), + llvm::sys::path::filename(File), + std::move(CommandLine), "")}; +} + +static const char *getVirtualTestRoot() { +#ifdef LLVM_ON_WIN32 + return "C:\\clangd-test"; +#else + return "/clangd-test"; +#endif +} + +llvm::SmallString<32> getVirtualTestFilePath(PathRef File) { + assert(llvm::sys::path::is_relative(File) && "FileName should be relative"); + + llvm::SmallString<32> Path; + llvm::sys::path::append(Path, getVirtualTestRoot(), File); + return Path; +} + +} // namespace clangd +} // namespace clang