Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -150,6 +150,7 @@ }; bool fromJSON(const json::Expr &, TextEdit &); json::Expr toJSON(const TextEdit &); +llvm::raw_ostream &operator<<(llvm::raw_ostream &, const TextEdit &); struct TextDocumentItem { /// The text document's URI. @@ -341,6 +342,7 @@ } }; bool fromJSON(const json::Expr &, Diagnostic &); +llvm::raw_ostream &operator<<(llvm::raw_ostream &, const Diagnostic &); struct CodeActionContext { /// An array of diagnostics. Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -137,6 +137,12 @@ }; } +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const TextEdit &TE) { + OS << TE.range << " => \""; + PrintEscapedString(TE.newText, OS); + return OS << '"'; +} + bool fromJSON(const json::Expr &E, TraceLevel &Out) { if (auto S = E.asString()) { if (*S == "off") { @@ -255,6 +261,28 @@ return O && O.map("diagnostics", R.diagnostics); } +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Diagnostic &D) { + OS << D.range << " ["; + switch (D.severity) { + case 1: + OS << "error"; + break; + case 2: + OS << "warning"; + break; + case 3: + OS << "note"; + break; + case 4: + OS << "remark"; + break; + default: + OS << "diagnostic"; + break; + } + return OS << '(' << D.severity << "): " << D.message << "]"; +} + bool fromJSON(const json::Expr &Params, CodeActionParams &R) { json::ObjectMapper O(Params); return O && O.map("textDocument", R.textDocument) && Index: test/clangd/diagnostics-preamble.test =================================================================== --- test/clangd/diagnostics-preamble.test +++ /dev/null @@ -1,22 +0,0 @@ -# RUN: clangd -pretty -run-synchronously < %s | FileCheck -strict-whitespace %s -# RUN: clangd -pretty -run-synchronously -pch-storage=memory < %s | FileCheck -strict-whitespace %s -# It is absolutely vital that this file has CRLF line endings. -# -Content-Length: 125 - -{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} - -Content-Length: 206 - -{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///main.cpp","languageId":"cpp","version":1,"text":"#ifndef FOO\n#define FOO\nint a;\n#else\nint a = b;#endif\n\n\n"}}} -# CHECK: "method": "textDocument/publishDiagnostics", -# CHECK-NEXT: "params": { -# CHECK-NEXT: "diagnostics": [], -# CHECK-NEXT: "uri": "file:///{{([A-Z]:/)?}}main.cpp" -# CHECK-NEXT: } -Content-Length: 58 - -{"jsonrpc":"2.0","id":2,"method":"shutdown","params":null} -Content-Length: 33 - -{"jsonrpc":"2.0":"method":"exit"} Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -11,6 +11,7 @@ add_extra_unittest(ClangdTests Annotations.cpp ClangdTests.cpp + ClangdUnitTests.cpp CodeCompleteTests.cpp ContextTests.cpp FileIndexTests.cpp Index: unittests/clangd/ClangdUnitTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/ClangdUnitTests.cpp @@ -0,0 +1,130 @@ +//===-- ClangdUnitTests.cpp - ClangdUnit tests ------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "ClangdUnit.h" +#include "Annotations.h" +#include "TestFS.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "clang/Frontend/PCHContainerOperations.h" +#include "clang/Frontend/Utils.h" +#include "llvm/Support/ScopedPrinter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +using namespace llvm; +void PrintTo(const DiagWithFixIts &D, std::ostream *O) { + llvm::raw_os_ostream OS(*O); + OS << D.Diag; + if (!D.FixIts.empty()) { + OS << " {"; + const char *Sep = ""; + for (const auto &F : D.FixIts) { + OS << Sep << F; + Sep = ", "; + } + OS << "}"; + } +} + +namespace { +using testing::ElementsAre; + +// FIXME: this is duplicated with FileIndexTests. Share it. +ParsedAST build(StringRef Code, std::vector Flags = {}) { + std::vector Cmd = {"clang", "main.cpp"}; + Cmd.insert(Cmd.begin() + 1, Flags.begin(), Flags.end()); + auto CI = createInvocationFromCommandLine(Cmd); + auto Buf = MemoryBuffer::getMemBuffer(Code); + auto AST = ParsedAST::Build( + Context::empty(), std::move(CI), nullptr, std::move(Buf), + std::make_shared(), vfs::getRealFileSystem()); + assert(AST.hasValue()); + return std::move(*AST); +} + +MATCHER_P2(Diag, Range, Message, + "Diagnostic at " + llvm::to_string(Range) + " = [" + Message + "]") { + return arg.Diag.range == Range && arg.Diag.message == Message && + arg.FixIts.empty(); +} + +MATCHER_P3(Fix, Range, Replacement, Message, + "Fix " + llvm::to_string(Range) + " => " + + testing::PrintToString(Replacement) + " = [" + Message + "]") { + return arg.Diag.range == Range && arg.Diag.message == Message && + arg.FixIts.size() == 1 && arg.FixIts[0].range == Range && + arg.FixIts[0].newText == Replacement; +} + +TEST(DiagnosticsTest, DiagnosticRanges) { + // Check we report correct ranges, including various edge-cases. + Annotations Test(R"cpp( + void $decl[[foo]](); + int main() { + $typo[[go\ +o]](); + foo()$semicolon[[]] + $unk[[unknown]](); + } + )cpp"); + llvm::errs() << Test.code(); + EXPECT_THAT( + build(Test.code()).getDiagnostics(), + ElementsAre( + // This range spans lines. + Fix(Test.range("typo"), "foo", + "use of undeclared identifier 'goo'; did you mean 'foo'?"), + // This is a pretty normal range. + Diag(Test.range("decl"), "'foo' declared here"), + // This range is zero-width, and at the end of a line. + Fix(Test.range("semicolon"), ";", + "expected ';' after expression"), + // This range isn't provided by clang, we expand to the token. + Diag(Test.range("unk"), + "use of undeclared identifier 'unknown'"))); +} + +TEST(DiagnosticsTest, FlagsMatter) { + Annotations Test("[[void]] main() {}"); + EXPECT_THAT( + build(Test.code()).getDiagnostics(), + ElementsAre(Fix(Test.range(), "int", "'main' must return 'int'"))); + // Same code built as C gets different diagnostics. + EXPECT_THAT( + build(Test.code(), {"-x", "c"}).getDiagnostics(), + ElementsAre( + // FIXME: ideally this would be one diagnostic with a named FixIt. + Diag(Test.range(), "return type of 'main' is not 'int'"), + Fix(Test.range(), "int", "change return type to 'int'"))); +} + +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( + build(Test.code()).getDiagnostics(), + ElementsAre(Diag(Test.range(), "use of undeclared identifier 'b'"))); +} + +} // namespace +} // namespace clangd +} // namespace clang