| 1 | //===-- ClangdLSPServerTests.cpp ------------------------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include "Annotations.h" |
| 10 | #include "ClangdLSPServer.h" |
| 11 | #include "ClangdServer.h" |
| 12 | #include "ConfigProvider.h" |
| 13 | #include "Diagnostics.h" |
| 14 | #include "Feature.h" |
| 15 | #include "FeatureModule.h" |
| 16 | #include "LSPBinder.h" |
| 17 | #include "LSPClient.h" |
| 18 | #include "TestFS.h" |
| 19 | #include "support/Function.h" |
| 20 | #include "support/Logger.h" |
| 21 | #include "support/TestTracer.h" |
| 22 | #include "support/Threading.h" |
| 23 | #include "clang/Basic/Diagnostic.h" |
| 24 | #include "clang/Basic/LLVM.h" |
| 25 | #include "llvm/ADT/FunctionExtras.h" |
| 26 | #include "llvm/ADT/StringRef.h" |
| 27 | #include "llvm/Support/Error.h" |
| 28 | #include "llvm/Support/FormatVariadic.h" |
| 29 | #include "llvm/Support/JSON.h" |
| 30 | #include "llvm/Support/raw_ostream.h" |
| 31 | #include "llvm/Testing/Support/Error.h" |
| 32 | #include "llvm/Testing/Support/SupportHelpers.h" |
| 33 | #include "gmock/gmock.h" |
| 34 | #include "gtest/gtest.h" |
| 35 | #include <cassert> |
| 36 | #include <condition_variable> |
| 37 | #include <cstddef> |
| 38 | #include <deque> |
| 39 | #include <memory> |
| 40 | #include <mutex> |
| 41 | #include <optional> |
| 42 | #include <thread> |
| 43 | #include <utility> |
| 44 | |
| 45 | namespace clang { |
| 46 | namespace clangd { |
| 47 | namespace { |
| 48 | using testing::ElementsAre; |
| 49 | |
| 50 | MATCHER_P(diagMessage, M, "" ) { |
| 51 | if (const auto *O = arg.getAsObject()) { |
| 52 | if (const auto Msg = O->getString("message" )) |
| 53 | return *Msg == M; |
| 54 | } |
| 55 | return false; |
| 56 | } |
| 57 | |
| 58 | class LSPTest : public ::testing::Test { |
| 59 | protected: |
| 60 | LSPTest() : LogSession(L) { |
| 61 | ClangdServer::Options &Base = Opts; |
| 62 | Base = ClangdServer::optsForTest(); |
| 63 | // This is needed to we can test index-based operations like call hierarchy. |
| 64 | Base.BuildDynamicSymbolIndex = true; |
| 65 | Base.FeatureModules = &FeatureModules; |
| 66 | } |
| 67 | |
| 68 | LSPClient &start() { |
| 69 | EXPECT_FALSE(Server) << "Already initialized" ; |
| 70 | Server.emplace(args&: Client.transport(), args&: FS, args&: Opts); |
| 71 | ServerThread.emplace(args: [&] { EXPECT_TRUE(Server->run()); }); |
| 72 | Client.call(Method: "initialize" , Params: llvm::json::Object{}); |
| 73 | return Client; |
| 74 | } |
| 75 | |
| 76 | void stop() { |
| 77 | assert(Server); |
| 78 | Client.call(Method: "shutdown" , Params: nullptr); |
| 79 | Client.notify(Method: "exit" , Params: nullptr); |
| 80 | Client.stop(); |
| 81 | ServerThread->join(); |
| 82 | Server.reset(); |
| 83 | ServerThread.reset(); |
| 84 | } |
| 85 | |
| 86 | ~LSPTest() { |
| 87 | if (Server) |
| 88 | stop(); |
| 89 | } |
| 90 | |
| 91 | MockFS FS; |
| 92 | ClangdLSPServer::Options Opts; |
| 93 | FeatureModuleSet FeatureModules; |
| 94 | |
| 95 | private: |
| 96 | class Logger : public clang::clangd::Logger { |
| 97 | // Color logs so we can distinguish them from test output. |
| 98 | void log(Level L, const char *Fmt, |
| 99 | const llvm::formatv_object_base &Message) override { |
| 100 | raw_ostream::Colors Color; |
| 101 | switch (L) { |
| 102 | case Level::Verbose: |
| 103 | Color = raw_ostream::BLUE; |
| 104 | break; |
| 105 | case Level::Error: |
| 106 | Color = raw_ostream::RED; |
| 107 | break; |
| 108 | default: |
| 109 | Color = raw_ostream::YELLOW; |
| 110 | break; |
| 111 | } |
| 112 | std::lock_guard<std::mutex> Lock(LogMu); |
| 113 | (llvm::outs().changeColor(Color) << Message << "\n" ).resetColor(); |
| 114 | } |
| 115 | std::mutex LogMu; |
| 116 | }; |
| 117 | |
| 118 | Logger L; |
| 119 | LoggingSession LogSession; |
| 120 | std::optional<ClangdLSPServer> Server; |
| 121 | std::optional<std::thread> ServerThread; |
| 122 | LSPClient Client; |
| 123 | }; |
| 124 | |
| 125 | TEST_F(LSPTest, GoToDefinition) { |
| 126 | Annotations Code(R"cpp( |
| 127 | int [[fib]](int n) { |
| 128 | return n >= 2 ? ^fib(n - 1) + fib(n - 2) : 1; |
| 129 | } |
| 130 | )cpp" ); |
| 131 | auto &Client = start(); |
| 132 | Client.didOpen(Path: "foo.cpp" , Content: Code.code()); |
| 133 | auto &Def = Client.call(Method: "textDocument/definition" , |
| 134 | Params: llvm::json::Object{ |
| 135 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
| 136 | {.K: "position" , .V: Code.point()}, |
| 137 | }); |
| 138 | llvm::json::Value Want = llvm::json::Array{llvm::json::Object{ |
| 139 | {.K: "uri" , .V: Client.uri(Path: "foo.cpp" )}, {.K: "range" , .V: Code.range()}}}; |
| 140 | EXPECT_EQ(Def.takeValue(), Want); |
| 141 | } |
| 142 | |
| 143 | TEST_F(LSPTest, Diagnostics) { |
| 144 | auto &Client = start(); |
| 145 | Client.didOpen(Path: "foo.cpp" , Content: "void main(int, char**);" ); |
| 146 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 147 | llvm::ValueIs(testing::ElementsAre( |
| 148 | diagMessage("'main' must return 'int' (fix available)" )))); |
| 149 | |
| 150 | Client.didChange(Path: "foo.cpp" , Content: "int x = \"42\";" ); |
| 151 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 152 | llvm::ValueIs(testing::ElementsAre( |
| 153 | diagMessage("Cannot initialize a variable of type 'int' with " |
| 154 | "an lvalue of type 'const char[3]'" )))); |
| 155 | |
| 156 | Client.didClose(Path: "foo.cpp" ); |
| 157 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), llvm::ValueIs(testing::IsEmpty())); |
| 158 | } |
| 159 | |
| 160 | TEST_F(LSPTest, DiagnosticsHeaderSaved) { |
| 161 | auto &Client = start(); |
| 162 | Client.didOpen(Path: "foo.cpp" , Content: R"cpp( |
| 163 | #include "foo.h" |
| 164 | int x = VAR; |
| 165 | )cpp" ); |
| 166 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 167 | llvm::ValueIs(testing::ElementsAre( |
| 168 | diagMessage("'foo.h' file not found" ), |
| 169 | diagMessage("Use of undeclared identifier 'VAR'" )))); |
| 170 | // Now create the header. |
| 171 | FS.Files["foo.h" ] = "#define VAR original" ; |
| 172 | Client.notify( |
| 173 | Method: "textDocument/didSave" , |
| 174 | Params: llvm::json::Object{{.K: "textDocument" , .V: Client.documentID(Path: "foo.h" )}}); |
| 175 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 176 | llvm::ValueIs(testing::ElementsAre( |
| 177 | diagMessage("Use of undeclared identifier 'original'" )))); |
| 178 | // Now modify the header from within the "editor". |
| 179 | FS.Files["foo.h" ] = "#define VAR changed" ; |
| 180 | Client.notify( |
| 181 | Method: "textDocument/didSave" , |
| 182 | Params: llvm::json::Object{{.K: "textDocument" , .V: Client.documentID(Path: "foo.h" )}}); |
| 183 | // Foo.cpp should be rebuilt with new diagnostics. |
| 184 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 185 | llvm::ValueIs(testing::ElementsAre( |
| 186 | diagMessage("Use of undeclared identifier 'changed'" )))); |
| 187 | } |
| 188 | |
| 189 | TEST_F(LSPTest, RecordsLatencies) { |
| 190 | trace::TestTracer Tracer; |
| 191 | auto &Client = start(); |
| 192 | llvm::StringLiteral MethodName = "method_name" ; |
| 193 | EXPECT_THAT(Tracer.takeMetric("lsp_latency" , MethodName), testing::SizeIs(0)); |
| 194 | llvm::consumeError(Err: Client.call(Method: MethodName, Params: {}).take().takeError()); |
| 195 | stop(); |
| 196 | EXPECT_THAT(Tracer.takeMetric("lsp_latency" , MethodName), testing::SizeIs(1)); |
| 197 | } |
| 198 | |
| 199 | // clang-tidy's renames are converted to clangd's internal rename functionality, |
| 200 | // see clangd#1589 and clangd#741 |
| 201 | TEST_F(LSPTest, ClangTidyRename) { |
| 202 | // This test requires clang-tidy checks to be linked in. |
| 203 | if (!CLANGD_TIDY_CHECKS) |
| 204 | return; |
| 205 | Annotations (R"cpp( |
| 206 | void [[foo]](); |
| 207 | )cpp" ); |
| 208 | Annotations Source(R"cpp( |
| 209 | void [[foo]]() {} |
| 210 | )cpp" ); |
| 211 | constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts, |
| 212 | llvm::StringRef) { |
| 213 | ClangTidyOpts.Checks = {"-*,readability-identifier-naming" }; |
| 214 | ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase" ] = |
| 215 | "CamelCase" ; |
| 216 | }; |
| 217 | Opts.ClangTidyProvider = ClangTidyProvider; |
| 218 | auto &Client = start(); |
| 219 | Client.didOpen(Path: "foo.hpp" , Content: Header.code()); |
| 220 | Client.didOpen(Path: "foo.cpp" , Content: Source.code()); |
| 221 | |
| 222 | auto Diags = Client.diagnostics(Path: "foo.cpp" ); |
| 223 | ASSERT_TRUE(Diags && !Diags->empty()); |
| 224 | auto RenameDiag = Diags->front(); |
| 225 | |
| 226 | auto RenameCommand = |
| 227 | (*Client |
| 228 | .call(Method: "textDocument/codeAction" , |
| 229 | Params: llvm::json::Object{ |
| 230 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
| 231 | {.K: "context" , |
| 232 | .V: llvm::json::Object{ |
| 233 | {.K: "diagnostics" , .V: llvm::json::Array{RenameDiag}}}}, |
| 234 | {.K: "range" , .V: Source.range()}}) |
| 235 | .takeValue() |
| 236 | .getAsArray())[0]; |
| 237 | |
| 238 | ASSERT_EQ((*RenameCommand.getAsObject())["title" ], "change 'foo' to 'Foo'" ); |
| 239 | |
| 240 | Client.expectServerCall(Method: "workspace/applyEdit" ); |
| 241 | Client.call(Method: "workspace/executeCommand" , Params: RenameCommand); |
| 242 | Client.sync(); |
| 243 | |
| 244 | auto Params = Client.takeCallParams(Method: "workspace/applyEdit" ); |
| 245 | auto Uri = [&](llvm::StringRef Path) { |
| 246 | return Client.uri(Path).getAsString().value().str(); |
| 247 | }; |
| 248 | llvm::json::Object ExpectedEdit = llvm::json::Object{ |
| 249 | {.K: "edit" , .V: llvm::json::Object{ |
| 250 | {.K: "changes" , |
| 251 | .V: llvm::json::Object{ |
| 252 | {.K: Uri("foo.hpp" ), .V: llvm::json::Array{llvm::json::Object{ |
| 253 | {.K: "range" , .V: Header.range()}, |
| 254 | {.K: "newText" , .V: "Foo" }, |
| 255 | }}}, |
| 256 | |
| 257 | {.K: Uri("foo.cpp" ), .V: llvm::json::Array{llvm::json::Object{ |
| 258 | {.K: "range" , .V: Source.range()}, |
| 259 | {.K: "newText" , .V: "Foo" }, |
| 260 | }}} |
| 261 | |
| 262 | }}}}}; |
| 263 | EXPECT_EQ(Params, std::vector{llvm::json::Value(std::move(ExpectedEdit))}); |
| 264 | } |
| 265 | |
| 266 | TEST_F(LSPTest, ClangTidyCrash_Issue109367) { |
| 267 | // This test requires clang-tidy checks to be linked in. |
| 268 | if (!CLANGD_TIDY_CHECKS) |
| 269 | return; |
| 270 | constexpr auto ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts, |
| 271 | llvm::StringRef) { |
| 272 | ClangTidyOpts.Checks = {"-*,boost-use-ranges" }; |
| 273 | }; |
| 274 | Opts.ClangTidyProvider = ClangTidyProvider; |
| 275 | // Check that registering the boost-use-ranges checker's matchers |
| 276 | // on two different threads does not cause a crash. |
| 277 | auto &Client = start(); |
| 278 | Client.didOpen(Path: "a.cpp" , Content: "" ); |
| 279 | Client.didOpen(Path: "b.cpp" , Content: "" ); |
| 280 | Client.sync(); |
| 281 | } |
| 282 | |
| 283 | TEST_F(LSPTest, IncomingCalls) { |
| 284 | Annotations Code(R"cpp( |
| 285 | void calle^e(int); |
| 286 | void caller1() { |
| 287 | [[callee]](42); |
| 288 | } |
| 289 | )cpp" ); |
| 290 | auto &Client = start(); |
| 291 | Client.didOpen(Path: "foo.cpp" , Content: Code.code()); |
| 292 | auto Items = Client |
| 293 | .call(Method: "textDocument/prepareCallHierarchy" , |
| 294 | Params: llvm::json::Object{ |
| 295 | {.K: "textDocument" , .V: Client.documentID(Path: "foo.cpp" )}, |
| 296 | {.K: "position" , .V: Code.point()}}) |
| 297 | .takeValue(); |
| 298 | auto FirstItem = (*Items.getAsArray())[0]; |
| 299 | auto Calls = Client |
| 300 | .call(Method: "callHierarchy/incomingCalls" , |
| 301 | Params: llvm::json::Object{{.K: "item" , .V: FirstItem}}) |
| 302 | .takeValue(); |
| 303 | auto FirstCall = *(*Calls.getAsArray())[0].getAsObject(); |
| 304 | EXPECT_EQ(FirstCall["fromRanges" ], llvm::json::Value{Code.range()}); |
| 305 | auto From = *FirstCall["from" ].getAsObject(); |
| 306 | EXPECT_EQ(From["name" ], "caller1" ); |
| 307 | } |
| 308 | |
| 309 | TEST_F(LSPTest, CDBConfigIntegration) { |
| 310 | auto CfgProvider = |
| 311 | config::Provider::fromAncestorRelativeYAMLFiles(RelPath: ".clangd" , FS); |
| 312 | Opts.ConfigProvider = CfgProvider.get(); |
| 313 | |
| 314 | // Map bar.cpp to a different compilation database which defines FOO->BAR. |
| 315 | FS.Files[".clangd" ] = R"yaml( |
| 316 | If: |
| 317 | PathMatch: bar.cpp |
| 318 | CompileFlags: |
| 319 | CompilationDatabase: bar |
| 320 | )yaml" ; |
| 321 | FS.Files["bar/compile_flags.txt" ] = "-DFOO=BAR" ; |
| 322 | |
| 323 | auto &Client = start(); |
| 324 | // foo.cpp gets parsed as normal. |
| 325 | Client.didOpen(Path: "foo.cpp" , Content: "int x = FOO;" ); |
| 326 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 327 | llvm::ValueIs(testing::ElementsAre( |
| 328 | diagMessage("Use of undeclared identifier 'FOO'" )))); |
| 329 | // bar.cpp shows the configured compile command. |
| 330 | Client.didOpen(Path: "bar.cpp" , Content: "int x = FOO;" ); |
| 331 | EXPECT_THAT(Client.diagnostics("bar.cpp" ), |
| 332 | llvm::ValueIs(testing::ElementsAre( |
| 333 | diagMessage("Use of undeclared identifier 'BAR'" )))); |
| 334 | } |
| 335 | |
| 336 | TEST_F(LSPTest, ModulesTest) { |
| 337 | class MathModule final : public FeatureModule { |
| 338 | OutgoingNotification<int> Changed; |
| 339 | void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
| 340 | llvm::json::Object &ServerCaps) override { |
| 341 | Bind.notification(Method: "add" , This: this, Handler: &MathModule::add); |
| 342 | Bind.method(Method: "get" , This: this, Handler: &MathModule::get); |
| 343 | Changed = Bind.outgoingNotification(Method: "changed" ); |
| 344 | } |
| 345 | |
| 346 | int Value = 0; |
| 347 | |
| 348 | void add(const int &X) { |
| 349 | Value += X; |
| 350 | Changed(Value); |
| 351 | } |
| 352 | void get(const std::nullptr_t &, Callback<int> Reply) { |
| 353 | scheduler().runQuick( |
| 354 | Name: "get" , Path: "" , |
| 355 | Action: [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); }); |
| 356 | } |
| 357 | }; |
| 358 | FeatureModules.add(M: std::make_unique<MathModule>()); |
| 359 | |
| 360 | auto &Client = start(); |
| 361 | Client.notify(Method: "add" , Params: 2); |
| 362 | Client.notify(Method: "add" , Params: 8); |
| 363 | EXPECT_EQ(10, Client.call("get" , nullptr).takeValue()); |
| 364 | EXPECT_THAT(Client.takeNotifications("changed" ), |
| 365 | ElementsAre(llvm::json::Value(2), llvm::json::Value(10))); |
| 366 | } |
| 367 | |
| 368 | // Creates a Callback that writes its received value into an |
| 369 | // std::optional<Expected>. |
| 370 | template <typename T> |
| 371 | llvm::unique_function<void(llvm::Expected<T>)> |
| 372 | capture(std::optional<llvm::Expected<T>> &Out) { |
| 373 | Out.reset(); |
| 374 | return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); }; |
| 375 | } |
| 376 | |
| 377 | TEST_F(LSPTest, FeatureModulesThreadingTest) { |
| 378 | // A feature module that does its work on a background thread, and so |
| 379 | // exercises the block/shutdown protocol. |
| 380 | class AsyncCounter final : public FeatureModule { |
| 381 | bool ShouldStop = false; |
| 382 | int State = 0; |
| 383 | std::deque<Callback<int>> Queue; // null = increment, non-null = read. |
| 384 | std::condition_variable CV; |
| 385 | std::mutex Mu; |
| 386 | std::thread Thread; |
| 387 | |
| 388 | void run() { |
| 389 | std::unique_lock<std::mutex> Lock(Mu); |
| 390 | while (true) { |
| 391 | CV.wait(lock&: Lock, p: [&] { return ShouldStop || !Queue.empty(); }); |
| 392 | if (ShouldStop) { |
| 393 | Queue.clear(); |
| 394 | CV.notify_all(); |
| 395 | return; |
| 396 | } |
| 397 | Callback<int> &Task = Queue.front(); |
| 398 | if (Task) |
| 399 | Task(State); |
| 400 | else |
| 401 | ++State; |
| 402 | Queue.pop_front(); |
| 403 | CV.notify_all(); |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | bool blockUntilIdle(Deadline D) override { |
| 408 | std::unique_lock<std::mutex> Lock(Mu); |
| 409 | return clangd::wait(Lock, CV, D, F: [this] { return Queue.empty(); }); |
| 410 | } |
| 411 | |
| 412 | void stop() override { |
| 413 | { |
| 414 | std::lock_guard<std::mutex> Lock(Mu); |
| 415 | ShouldStop = true; |
| 416 | } |
| 417 | CV.notify_all(); |
| 418 | } |
| 419 | |
| 420 | public: |
| 421 | AsyncCounter() : Thread([this] { run(); }) {} |
| 422 | ~AsyncCounter() { |
| 423 | // Verify shutdown sequence was performed. |
| 424 | // Real modules would not do this, to be robust to no ClangdServer. |
| 425 | { |
| 426 | // We still need the lock here, as Queue might be empty when |
| 427 | // ClangdServer calls blockUntilIdle, but run() might not have returned |
| 428 | // yet. |
| 429 | std::lock_guard<std::mutex> Lock(Mu); |
| 430 | EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown" ; |
| 431 | EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle" ; |
| 432 | } |
| 433 | Thread.join(); |
| 434 | } |
| 435 | |
| 436 | void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, |
| 437 | llvm::json::Object &ServerCaps) override { |
| 438 | Bind.notification(Method: "increment" , This: this, Handler: &AsyncCounter::increment); |
| 439 | } |
| 440 | |
| 441 | // Get the current value, bypassing the queue. |
| 442 | // Used to verify that sync->blockUntilIdle avoids races in tests. |
| 443 | int getSync() { |
| 444 | std::lock_guard<std::mutex> Lock(Mu); |
| 445 | return State; |
| 446 | } |
| 447 | |
| 448 | // Increment the current value asynchronously. |
| 449 | void increment(const std::nullptr_t &) { |
| 450 | { |
| 451 | std::lock_guard<std::mutex> Lock(Mu); |
| 452 | Queue.push_back(x: nullptr); |
| 453 | } |
| 454 | CV.notify_all(); |
| 455 | } |
| 456 | }; |
| 457 | |
| 458 | FeatureModules.add(M: std::make_unique<AsyncCounter>()); |
| 459 | auto &Client = start(); |
| 460 | |
| 461 | Client.notify(Method: "increment" , Params: nullptr); |
| 462 | Client.notify(Method: "increment" , Params: nullptr); |
| 463 | Client.notify(Method: "increment" , Params: nullptr); |
| 464 | Client.sync(); |
| 465 | EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync()); |
| 466 | // Throw some work on the queue to make sure shutdown blocks on it. |
| 467 | Client.notify(Method: "increment" , Params: nullptr); |
| 468 | Client.notify(Method: "increment" , Params: nullptr); |
| 469 | Client.notify(Method: "increment" , Params: nullptr); |
| 470 | // And immediately shut down. FeatureModule destructor verifies we blocked. |
| 471 | } |
| 472 | |
| 473 | TEST_F(LSPTest, DiagModuleTest) { |
| 474 | static constexpr llvm::StringLiteral DiagMsg = "DiagMsg" ; |
| 475 | class DiagModule final : public FeatureModule { |
| 476 | struct DiagHooks : public ASTListener { |
| 477 | void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override { |
| 478 | D.Message = DiagMsg.str(); |
| 479 | } |
| 480 | }; |
| 481 | |
| 482 | public: |
| 483 | std::unique_ptr<ASTListener> astListeners() override { |
| 484 | return std::make_unique<DiagHooks>(); |
| 485 | } |
| 486 | }; |
| 487 | FeatureModules.add(M: std::make_unique<DiagModule>()); |
| 488 | |
| 489 | auto &Client = start(); |
| 490 | Client.didOpen(Path: "foo.cpp" , Content: "test;" ); |
| 491 | EXPECT_THAT(Client.diagnostics("foo.cpp" ), |
| 492 | llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg)))); |
| 493 | } |
| 494 | } // namespace |
| 495 | } // namespace clangd |
| 496 | } // namespace clang |
| 497 | |