Index: clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp @@ -199,6 +199,7 @@ : Out(Out), Server(llvm::make_unique(), llvm::make_unique(*this), + llvm::make_unique(), RunSynchronously) {} void ClangdLSPServer::run(std::istream &In) { Index: clang-tools-extra/trunk/clangd/ClangdServer.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdServer.h +++ clang-tools-extra/trunk/clangd/ClangdServer.h @@ -50,6 +50,17 @@ std::vector Diagnostics) = 0; }; +class FileSystemProvider { +public: + virtual ~FileSystemProvider() = default; + virtual IntrusiveRefCntPtr getFileSystem() = 0; +}; + +class RealFileSystemProvider : public FileSystemProvider { +public: + IntrusiveRefCntPtr getFileSystem() override; +}; + class ClangdServer; /// Handles running WorkerRequests of ClangdServer on a separate threads. @@ -94,6 +105,7 @@ public: ClangdServer(std::unique_ptr CDB, std::unique_ptr DiagConsumer, + std::unique_ptr FSProvider, bool RunSynchronously); /// Add a \p File to the list of tracked C++ files or update the contents if @@ -104,6 +116,8 @@ /// Remove \p File from list of tracked files, schedule a request to free /// resources associated with it. void removeDocument(PathRef File); + /// Force \p File to be reparsed using the latest contents. + void forceReparse(PathRef File); /// Run code completion for \p File at \p Pos. std::vector codeComplete(PathRef File, Position Pos); @@ -129,6 +143,7 @@ private: std::unique_ptr CDB; std::unique_ptr DiagConsumer; + std::unique_ptr FSProvider; DraftStore DraftMgr; ClangdUnitStore Units; std::shared_ptr PCHs; Index: clang-tools-extra/trunk/clangd/ClangdServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdServer.cpp +++ clang-tools-extra/trunk/clangd/ClangdServer.cpp @@ -58,6 +58,10 @@ return {Lines, Cols}; } +IntrusiveRefCntPtr RealFileSystemProvider::getFileSystem() { + return vfs::getRealFileSystem(); +} + ClangdScheduler::ClangdScheduler(bool RunSynchronously) : RunSynchronously(RunSynchronously) { if (RunSynchronously) { @@ -135,8 +139,10 @@ ClangdServer::ClangdServer(std::unique_ptr CDB, std::unique_ptr DiagConsumer, + std::unique_ptr FSProvider, bool RunSynchronously) : CDB(std::move(CDB)), DiagConsumer(std::move(DiagConsumer)), + FSProvider(std::move(FSProvider)), PCHs(std::make_shared()), WorkScheduler(RunSynchronously) {} @@ -150,10 +156,11 @@ assert(FileContents.Draft && "No contents inside a file that was scheduled for reparse"); - Units.runOnUnit( - FileStr, *FileContents.Draft, *CDB, PCHs, [&](ClangdUnit const &Unit) { - DiagConsumer->onDiagnosticsReady(FileStr, Unit.getLocalDiagnostics()); - }); + Units.runOnUnit(FileStr, *FileContents.Draft, *CDB, PCHs, + FSProvider->getFileSystem(), [&](ClangdUnit const &Unit) { + DiagConsumer->onDiagnosticsReady( + FileStr, Unit.getLocalDiagnostics()); + }); }); } @@ -168,15 +175,22 @@ }); } +void ClangdServer::forceReparse(PathRef File) { + // The addDocument schedules the reparse even if the contents of the file + // never changed, so we just call it here. + addDocument(File, getDocument(File)); +} + std::vector ClangdServer::codeComplete(PathRef File, Position Pos) { auto FileContents = DraftMgr.getDraft(File); assert(FileContents.Draft && "codeComplete is called for non-added document"); std::vector Result; + auto VFS = FSProvider->getFileSystem(); Units.runOnUnitWithoutReparse( - File, *FileContents.Draft, *CDB, PCHs, [&](ClangdUnit &Unit) { - Result = Unit.codeComplete(*FileContents.Draft, Pos); + File, *FileContents.Draft, *CDB, PCHs, VFS, [&](ClangdUnit &Unit) { + Result = Unit.codeComplete(*FileContents.Draft, Pos, VFS); }); return Result; } Index: clang-tools-extra/trunk/clangd/ClangdUnit.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdUnit.h +++ clang-tools-extra/trunk/clangd/ClangdUnit.h @@ -10,8 +10,8 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDUNIT_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDUNIT_H -#include "Protocol.h" #include "Path.h" +#include "Protocol.h" #include "clang/Frontend/ASTUnit.h" #include "clang/Tooling/Core/Replacement.h" #include @@ -24,6 +24,10 @@ class ASTUnit; class PCHContainerOperations; +namespace vfs { +class FileSystem; +} + namespace tooling { struct CompileCommand; } @@ -42,16 +46,19 @@ public: ClangdUnit(PathRef FileName, StringRef Contents, std::shared_ptr PCHs, - std::vector Commands); + std::vector Commands, + IntrusiveRefCntPtr VFS); /// Reparse with new contents. - void reparse(StringRef Contents); + void reparse(StringRef Contents, IntrusiveRefCntPtr VFS); /// Get code completions at a specified \p Line and \p Column in \p File. /// /// This function is thread-safe and returns completion items that own the /// data they contain. - std::vector codeComplete(StringRef Contents, Position Pos); + std::vector + codeComplete(StringRef Contents, Position Pos, + IntrusiveRefCntPtr VFS); /// Returns diagnostics and corresponding FixIts for each diagnostic that are /// located in the current file. std::vector getLocalDiagnostics() const; Index: clang-tools-extra/trunk/clangd/ClangdUnit.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdUnit.cpp +++ clang-tools-extra/trunk/clangd/ClangdUnit.cpp @@ -11,6 +11,7 @@ #include "clang/Frontend/ASTUnit.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/Utils.h" #include "clang/Tooling/CompilationDatabase.h" using namespace clang::clangd; @@ -18,7 +19,8 @@ ClangdUnit::ClangdUnit(PathRef FileName, StringRef Contents, std::shared_ptr PCHs, - std::vector Commands) + std::vector Commands, + IntrusiveRefCntPtr VFS) : FileName(FileName), PCHs(PCHs) { assert(!Commands.empty() && "No compile commands provided"); @@ -48,10 +50,16 @@ /*PrecompilePreambleAfterNParses=*/1, /*TUKind=*/TU_Prefix, /*CacheCodeCompletionResults=*/true, /*IncludeBriefCommentsInCodeCompletion=*/true, - /*AllowPCHWithCompilerErrors=*/true)); + /*AllowPCHWithCompilerErrors=*/true, + /*SkipFunctionBodies=*/false, + /*UserFilesAreVolatile=*/false, /*ForSerialization=*/false, + /*ModuleFormat=*/llvm::None, + /*ErrAST=*/nullptr, VFS)); + assert(Unit && "Unit wasn't created"); } -void ClangdUnit::reparse(StringRef Contents) { +void ClangdUnit::reparse(StringRef Contents, + IntrusiveRefCntPtr VFS) { // Do a reparse if this wasn't the first parse. // FIXME: This might have the wrong working directory if it changed in the // meantime. @@ -59,7 +67,7 @@ FileName, llvm::MemoryBuffer::getMemBufferCopy(Contents, FileName).release()); - Unit->Reparse(PCHs, RemappedSource); + Unit->Reparse(PCHs, RemappedSource, VFS); } namespace { @@ -146,8 +154,9 @@ }; } // namespace -std::vector ClangdUnit::codeComplete(StringRef Contents, - Position Pos) { +std::vector +ClangdUnit::codeComplete(StringRef Contents, Position Pos, + IntrusiveRefCntPtr VFS) { CodeCompleteOptions CCO; CCO.IncludeBriefComments = 1; // This is where code completion stores dirty buffers. Need to free after @@ -163,8 +172,10 @@ FileName, llvm::MemoryBuffer::getMemBufferCopy(Contents, FileName).release()); + IntrusiveRefCntPtr FileMgr( + new FileManager(Unit->getFileSystemOpts(), VFS)); IntrusiveRefCntPtr SourceMgr( - new SourceManager(*DiagEngine, Unit->getFileManager())); + new SourceManager(*DiagEngine, *FileMgr)); // CodeComplete seems to require fresh LangOptions. LangOptions LangOpts = Unit->getLangOpts(); // The language server protocol uses zero-based line and column numbers. @@ -172,8 +183,8 @@ Unit->CodeComplete(FileName, Pos.line + 1, Pos.character + 1, RemappedSource, CCO.IncludeMacros, CCO.IncludeCodePatterns, CCO.IncludeBriefComments, Collector, PCHs, *DiagEngine, - LangOpts, *SourceMgr, Unit->getFileManager(), - StoredDiagnostics, OwnedBuffers); + LangOpts, *SourceMgr, *FileMgr, StoredDiagnostics, + OwnedBuffers); for (const llvm::MemoryBuffer *Buffer : OwnedBuffers) delete Buffer; return Items; Index: clang-tools-extra/trunk/clangd/ClangdUnitStore.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdUnitStore.h +++ clang-tools-extra/trunk/clangd/ClangdUnitStore.h @@ -32,9 +32,10 @@ template void runOnUnit(PathRef File, StringRef FileContents, GlobalCompilationDatabase &CDB, - std::shared_ptr PCHs, Func Action) { + std::shared_ptr PCHs, + IntrusiveRefCntPtr VFS, Func Action) { runOnUnitImpl(File, FileContents, CDB, PCHs, /*ReparseBeforeAction=*/true, - std::forward(Action)); + VFS, std::forward(Action)); } /// Run specified \p Action on the ClangdUnit for \p File. @@ -45,9 +46,10 @@ void runOnUnitWithoutReparse(PathRef File, StringRef FileContents, GlobalCompilationDatabase &CDB, std::shared_ptr PCHs, + IntrusiveRefCntPtr VFS, Func Action) { runOnUnitImpl(File, FileContents, CDB, PCHs, /*ReparseBeforeAction=*/false, - std::forward(Action)); + VFS, std::forward(Action)); } /// Run the specified \p Action on the ClangdUnit for \p File. @@ -71,24 +73,23 @@ void runOnUnitImpl(PathRef File, StringRef FileContents, GlobalCompilationDatabase &CDB, std::shared_ptr PCHs, - bool ReparseBeforeAction, Func Action) { + bool ReparseBeforeAction, + IntrusiveRefCntPtr VFS, Func Action) { std::lock_guard Lock(Mutex); auto Commands = getCompileCommands(CDB, File); assert(!Commands.empty() && "getCompileCommands should add default command"); - // chdir. This is thread hostile. - // FIXME(ibiryukov): get rid of this - llvm::sys::fs::set_current_path(Commands.front().Directory); + VFS->setCurrentWorkingDirectory(Commands.front().Directory); auto It = OpenedFiles.find(File); if (It == OpenedFiles.end()) { It = OpenedFiles .insert(std::make_pair( - File, ClangdUnit(File, FileContents, PCHs, Commands))) + File, ClangdUnit(File, FileContents, PCHs, Commands, VFS))) .first; } else if (ReparseBeforeAction) { - It->second.reparse(FileContents); + It->second.reparse(FileContents, VFS); } return Action(It->second); } Index: clang-tools-extra/trunk/unittests/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/unittests/CMakeLists.txt +++ clang-tools-extra/trunk/unittests/CMakeLists.txt @@ -11,4 +11,5 @@ add_subdirectory(clang-query) add_subdirectory(clang-tidy) add_subdirectory(clang-rename) +add_subdirectory(clangd) add_subdirectory(include-fixer) 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 @@ -0,0 +1,24 @@ +set(LLVM_LINK_COMPONENTS + support + ) + +get_filename_component(CLANGD_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/../../clangd REALPATH) +include_directories( + ${CLANGD_SOURCE_DIR} + ) + +add_extra_unittest(ClangdTests + ClangdTests.cpp + ) + +target_link_libraries(ClangdTests + clangBasic + clangDaemon + clangFormat + clangFrontend + clangSema + clangTooling + clangToolingCore + LLVMSupport + ) 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 @@ -0,0 +1,364 @@ +//===-- ClangdTests.cpp - Clangd unit tests ---------------------*- 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 "clang/Basic/VirtualFileSystem.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Config/config.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Regex.h" +#include "gtest/gtest.h" +#include +#include +#include +#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 (TmpDir2 != TmpDir2) + TmpDirs.push_back(TmpDir2.str()); + return new vfs::FilteredFileSystem(std::move(TmpDirs), + vfs::getRealFileSystem()); +} +} // namespace vfs + +namespace clangd { +namespace { + +class ErrorCheckingDiagConsumer : public DiagnosticsConsumer { +public: + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override { + bool HadError = false; + for (const auto &DiagAndFixIts : Diagnostics) { + // FIXME: severities returned by clangd should have a descriptive + // diagnostic severity enum + const int ErrorSeverity = 1; + HadError = DiagAndFixIts.Diag.severity == ErrorSeverity; + } + + std::lock_guard Lock(Mutex); + HadErrorInLastDiags = HadError; + } + + bool hadErrorInLastDiags() { + std::lock_guard Lock(Mutex); + return HadErrorInLastDiags; + } + +private: + std::mutex Mutex; + bool HadErrorInLastDiags = false; +}; + +class MockCompilationDatabase : public GlobalCompilationDatabase { +public: + std::vector + getCompileCommands(PathRef File) override { + return {}; + } +}; + +class MockFSProvider : public FileSystemProvider { +public: + IntrusiveRefCntPtr getFileSystem() override { + 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; + } + + llvm::StringMap Files; +}; + +/// Replaces all patterns of the form 0x123abc with spaces +void replacePtrsInDump(std::string &Dump) { + llvm::Regex RE("0x[0-9a-fA-F]+"); + llvm::SmallVector Matches; + while (RE.match(Dump, &Matches)) { + assert(Matches.size() == 1 && "Exactly one match expected"); + auto MatchPos = Matches[0].data() - Dump.data(); + std::fill(Dump.begin() + MatchPos, + Dump.begin() + MatchPos + Matches[0].size(), ' '); + } +} + +std::string dumpASTWithoutMemoryLocs(ClangdServer &Server, PathRef File) { + auto Dump = Server.dumpAST(File); + replacePtrsInDump(Dump); + return Dump; +} + +template +std::unique_ptr getAndMove(std::unique_ptr Ptr, T *&Output) { + Output = Ptr.get(); + return Ptr; +} +} // namespace + +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 = {}, + bool ExpectErrors = false) { + MockFSProvider *FS; + ErrorCheckingDiagConsumer *DiagConsumer; + ClangdServer Server( + llvm::make_unique(), + getAndMove(llvm::make_unique(), + DiagConsumer), + getAndMove(llvm::make_unique(), FS), + /*RunSynchronously=*/false); + for (const auto &FileWithContents : ExtraFiles) + FS->Files[getVirtualTestFilePath(FileWithContents.first)] = + FileWithContents.second; + + auto SourceFilename = getVirtualTestFilePath(SourceFileRelPath); + Server.addDocument(SourceFilename, SourceContents); + auto Result = dumpASTWithoutMemoryLocs(Server, SourceFilename); + 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; + ClangdServer Server( + llvm::make_unique(), + getAndMove(llvm::make_unique(), DiagConsumer), + getAndMove(llvm::make_unique(), FS), + /*RunSynchronously=*/false); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + auto FooH = getVirtualTestFilePath("foo.h"); + + FS->Files[FooH] = "int a;"; + FS->Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_FALSE(DiagConsumer->hadErrorInLastDiags()); + + Server.addDocument(FooCpp, ""); + auto DumpParseEmpty = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_FALSE(DiagConsumer->hadErrorInLastDiags()); + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_FALSE(DiagConsumer->hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseEmpty); +} + +TEST_F(ClangdVFSTest, ReparseOnHeaderChange) { + MockFSProvider *FS; + ErrorCheckingDiagConsumer *DiagConsumer; + + ClangdServer Server( + llvm::make_unique(), + getAndMove(llvm::make_unique(), DiagConsumer), + getAndMove(llvm::make_unique(), FS), + /*RunSynchronously=*/false); + + const auto SourceContents = R"cpp( +#include "foo.h" +int b = a; +)cpp"; + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + auto FooH = getVirtualTestFilePath("foo.h"); + + FS->Files[FooH] = "int a;"; + FS->Files[FooCpp] = SourceContents; + + Server.addDocument(FooCpp, SourceContents); + auto DumpParse1 = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_FALSE(DiagConsumer->hadErrorInLastDiags()); + + FS->Files[FooH] = ""; + Server.forceReparse(FooCpp); + auto DumpParseDifferent = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_TRUE(DiagConsumer->hadErrorInLastDiags()); + + FS->Files[FooH] = "int a;"; + Server.forceReparse(FooCpp); + auto DumpParse2 = dumpASTWithoutMemoryLocs(Server, FooCpp); + EXPECT_FALSE(DiagConsumer->hadErrorInLastDiags()); + + EXPECT_EQ(DumpParse1, DumpParse2); + EXPECT_NE(DumpParse1, DumpParseDifferent); +} + +} // namespace clangd +} // namespace clang