Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -279,6 +279,12 @@ // ClangdServer ClangdScheduler WorkScheduler; bool SnippetCompletions; + + /// Used to serialize diagnostic callbacks. + /// FIXME(ibiryukov): get rid of an extra map and put all version counters + /// into CppFile. + std::mutex DiagnosticsMutex; + llvm::StringMap ReportedDiagnosticVersions; }; } // namespace clangd Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -313,6 +313,19 @@ auto Diags = DeferredRebuild.get(); if (!Diags) return; // A new reparse was requested before this one completed. + + // We need to serialize access to resulting diagnostics to avoid calling + // `onDiagnosticsReady` in the wrong order. + std::lock_guard DiagsLock(DiagnosticsMutex); + DocVersion &LastReportedDiagsVersion = ReportedDiagnosticVersions[FileStr]; + // FIXME(ibiryukov): get rid of '<' comparison here. In the current + // implementation diagnostics will not be reported after version counters' + // overflow. This should not happen in practice, since DocVersion is a + // 64-bit unsigned integer. + if (Version < LastReportedDiagsVersion) + return; + LastReportedDiagsVersion = Version; + DiagConsumer.onDiagnosticsReady(FileStr, make_tagged(std::move(*Diags), Tag)); }; Index: clangd/DraftStore.h =================================================================== --- clangd/DraftStore.h +++ clangd/DraftStore.h @@ -13,6 +13,7 @@ #include "Path.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/StringMap.h" +#include #include #include #include @@ -20,8 +21,8 @@ namespace clang { namespace clangd { -/// Using 'unsigned' here to avoid undefined behaviour on overflow. -typedef unsigned DocVersion; +/// Using unsigned int type here to avoid undefined behaviour on overflow. +typedef uint64_t DocVersion; /// Document draft with a version of this draft. struct VersionedDraft { Index: unittests/clangd/ClangdTests.cpp =================================================================== --- unittests/clangd/ClangdTests.cpp +++ unittests/clangd/ClangdTests.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include namespace clang { @@ -892,5 +893,66 @@ } } +TEST_F(ClangdThreadingTest, NoConcurrentDiagnostics) { + class NoConcurrentAccessDiagConsumer : public DiagnosticsConsumer { + public: + NoConcurrentAccessDiagConsumer(std::promise StartSecondReparse) + : StartSecondReparse(std::move(StartSecondReparse)) {} + + void onDiagnosticsReady( + PathRef File, + Tagged> Diagnostics) override { + + std::unique_lock Lock(Mutex, std::try_to_lock_t()); + ASSERT_TRUE(Lock.owns_lock()) + << "Detected concurrent onDiagnosticsReady calls for the same file."; + if (FirstRequest) { + FirstRequest = false; + StartSecondReparse.set_value(); + // Sleep long enough for the second request to be processed. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + + private: + std::mutex Mutex; + bool FirstRequest = true; + std::promise 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 = getVirtualTestFilePath("foo.cpp"); + llvm::StringMap FileContents; + FileContents[FooCpp] = ""; + ConstantFSProvider FS(buildTestFS(FileContents)); + + std::promise StartSecondReparsePromise; + std::future StartSecondReparse = StartSecondReparsePromise.get_future(); + + NoConcurrentAccessDiagConsumer DiagConsumer( + std::move(StartSecondReparsePromise)); + + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + ClangdServer Server(CDB, DiagConsumer, FS, 4, /*SnippetCompletions=*/false); + Server.addDocument(FooCpp, SourceContentsWithErrors); + StartSecondReparse.wait(); + + auto Future = Server.addDocument(FooCpp, SourceContentsWithoutErrors); + Future.wait(); +} + } // namespace clangd } // namespace clang