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
45namespace clang {
46namespace clangd {
47namespace {
48using testing::ElementsAre;
49
50MATCHER_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
58class LSPTest : public ::testing::Test {
59protected:
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
95private:
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
125TEST_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
143TEST_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
160TEST_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
189TEST_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
201TEST_F(LSPTest, ClangTidyRename) {
202 // This test requires clang-tidy checks to be linked in.
203 if (!CLANGD_TIDY_CHECKS)
204 return;
205 Annotations Header(R"cpp(
206 void [[foo]]();
207 )cpp");
208 Annotations Source(R"cpp(
209 void [[foo]]() {}
210 )cpp");
211 Opts.ClangTidyProvider = [](tidy::ClangTidyOptions &ClangTidyOpts,
212 llvm::StringRef) {
213 ClangTidyOpts.Checks = {"-*,readability-identifier-naming"};
214 ClangTidyOpts.CheckOptions["readability-identifier-naming.FunctionCase"] =
215 "CamelCase";
216 };
217 auto &Client = start();
218 Client.didOpen(Path: "foo.hpp", Content: Header.code());
219 Client.didOpen(Path: "foo.cpp", Content: Source.code());
220
221 auto Diags = Client.diagnostics(Path: "foo.cpp");
222 ASSERT_TRUE(Diags && !Diags->empty());
223 auto RenameDiag = Diags->front();
224
225 auto RenameCommand =
226 (*Client
227 .call(Method: "textDocument/codeAction",
228 Params: llvm::json::Object{
229 {.K: "textDocument", .V: Client.documentID(Path: "foo.cpp")},
230 {.K: "context",
231 .V: llvm::json::Object{
232 {.K: "diagnostics", .V: llvm::json::Array{RenameDiag}}}},
233 {.K: "range", .V: Source.range()}})
234 .takeValue()
235 .getAsArray())[0];
236
237 ASSERT_EQ((*RenameCommand.getAsObject())["title"], "change 'foo' to 'Foo'");
238
239 Client.expectServerCall(Method: "workspace/applyEdit");
240 Client.call(Method: "workspace/executeCommand", Params: RenameCommand);
241 Client.sync();
242
243 auto Params = Client.takeCallParams(Method: "workspace/applyEdit");
244 auto Uri = [&](llvm::StringRef Path) {
245 return Client.uri(Path).getAsString().value().str();
246 };
247 llvm::json::Object ExpectedEdit = llvm::json::Object{
248 {.K: "edit", .V: llvm::json::Object{
249 {.K: "changes",
250 .V: llvm::json::Object{
251 {.K: Uri("foo.hpp"), .V: llvm::json::Array{llvm::json::Object{
252 {.K: "range", .V: Header.range()},
253 {.K: "newText", .V: "Foo"},
254 }}},
255
256 {.K: Uri("foo.cpp"), .V: llvm::json::Array{llvm::json::Object{
257 {.K: "range", .V: Source.range()},
258 {.K: "newText", .V: "Foo"},
259 }}}
260
261 }}}}};
262 EXPECT_EQ(Params, std::vector{llvm::json::Value(std::move(ExpectedEdit))});
263}
264
265TEST_F(LSPTest, IncomingCalls) {
266 Annotations Code(R"cpp(
267 void calle^e(int);
268 void caller1() {
269 [[callee]](42);
270 }
271 )cpp");
272 auto &Client = start();
273 Client.didOpen(Path: "foo.cpp", Content: Code.code());
274 auto Items = Client
275 .call(Method: "textDocument/prepareCallHierarchy",
276 Params: llvm::json::Object{
277 {.K: "textDocument", .V: Client.documentID(Path: "foo.cpp")},
278 {.K: "position", .V: Code.point()}})
279 .takeValue();
280 auto FirstItem = (*Items.getAsArray())[0];
281 auto Calls = Client
282 .call(Method: "callHierarchy/incomingCalls",
283 Params: llvm::json::Object{{.K: "item", .V: FirstItem}})
284 .takeValue();
285 auto FirstCall = *(*Calls.getAsArray())[0].getAsObject();
286 EXPECT_EQ(FirstCall["fromRanges"], llvm::json::Value{Code.range()});
287 auto From = *FirstCall["from"].getAsObject();
288 EXPECT_EQ(From["name"], "caller1");
289}
290
291TEST_F(LSPTest, CDBConfigIntegration) {
292 auto CfgProvider =
293 config::Provider::fromAncestorRelativeYAMLFiles(RelPath: ".clangd", FS);
294 Opts.ConfigProvider = CfgProvider.get();
295
296 // Map bar.cpp to a different compilation database which defines FOO->BAR.
297 FS.Files[".clangd"] = R"yaml(
298If:
299 PathMatch: bar.cpp
300CompileFlags:
301 CompilationDatabase: bar
302)yaml";
303 FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR";
304
305 auto &Client = start();
306 // foo.cpp gets parsed as normal.
307 Client.didOpen(Path: "foo.cpp", Content: "int x = FOO;");
308 EXPECT_THAT(Client.diagnostics("foo.cpp"),
309 llvm::ValueIs(testing::ElementsAre(
310 diagMessage("Use of undeclared identifier 'FOO'"))));
311 // bar.cpp shows the configured compile command.
312 Client.didOpen(Path: "bar.cpp", Content: "int x = FOO;");
313 EXPECT_THAT(Client.diagnostics("bar.cpp"),
314 llvm::ValueIs(testing::ElementsAre(
315 diagMessage("Use of undeclared identifier 'BAR'"))));
316}
317
318TEST_F(LSPTest, ModulesTest) {
319 class MathModule final : public FeatureModule {
320 OutgoingNotification<int> Changed;
321 void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,
322 llvm::json::Object &ServerCaps) override {
323 Bind.notification(Method: "add", This: this, Handler: &MathModule::add);
324 Bind.method(Method: "get", This: this, Handler: &MathModule::get);
325 Changed = Bind.outgoingNotification(Method: "changed");
326 }
327
328 int Value = 0;
329
330 void add(const int &X) {
331 Value += X;
332 Changed(Value);
333 }
334 void get(const std::nullptr_t &, Callback<int> Reply) {
335 scheduler().runQuick(
336 Name: "get", Path: "",
337 Action: [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); });
338 }
339 };
340 FeatureModules.add(M: std::make_unique<MathModule>());
341
342 auto &Client = start();
343 Client.notify(Method: "add", Params: 2);
344 Client.notify(Method: "add", Params: 8);
345 EXPECT_EQ(10, Client.call("get", nullptr).takeValue());
346 EXPECT_THAT(Client.takeNotifications("changed"),
347 ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));
348}
349
350// Creates a Callback that writes its received value into an
351// std::optional<Expected>.
352template <typename T>
353llvm::unique_function<void(llvm::Expected<T>)>
354capture(std::optional<llvm::Expected<T>> &Out) {
355 Out.reset();
356 return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); };
357}
358
359TEST_F(LSPTest, FeatureModulesThreadingTest) {
360 // A feature module that does its work on a background thread, and so
361 // exercises the block/shutdown protocol.
362 class AsyncCounter final : public FeatureModule {
363 bool ShouldStop = false;
364 int State = 0;
365 std::deque<Callback<int>> Queue; // null = increment, non-null = read.
366 std::condition_variable CV;
367 std::mutex Mu;
368 std::thread Thread;
369
370 void run() {
371 std::unique_lock<std::mutex> Lock(Mu);
372 while (true) {
373 CV.wait(lock&: Lock, p: [&] { return ShouldStop || !Queue.empty(); });
374 if (ShouldStop) {
375 Queue.clear();
376 CV.notify_all();
377 return;
378 }
379 Callback<int> &Task = Queue.front();
380 if (Task)
381 Task(State);
382 else
383 ++State;
384 Queue.pop_front();
385 CV.notify_all();
386 }
387 }
388
389 bool blockUntilIdle(Deadline D) override {
390 std::unique_lock<std::mutex> Lock(Mu);
391 return clangd::wait(Lock, CV, D, F: [this] { return Queue.empty(); });
392 }
393
394 void stop() override {
395 {
396 std::lock_guard<std::mutex> Lock(Mu);
397 ShouldStop = true;
398 }
399 CV.notify_all();
400 }
401
402 public:
403 AsyncCounter() : Thread([this] { run(); }) {}
404 ~AsyncCounter() {
405 // Verify shutdown sequence was performed.
406 // Real modules would not do this, to be robust to no ClangdServer.
407 {
408 // We still need the lock here, as Queue might be empty when
409 // ClangdServer calls blockUntilIdle, but run() might not have returned
410 // yet.
411 std::lock_guard<std::mutex> Lock(Mu);
412 EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown";
413 EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle";
414 }
415 Thread.join();
416 }
417
418 void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,
419 llvm::json::Object &ServerCaps) override {
420 Bind.notification(Method: "increment", This: this, Handler: &AsyncCounter::increment);
421 }
422
423 // Get the current value, bypassing the queue.
424 // Used to verify that sync->blockUntilIdle avoids races in tests.
425 int getSync() {
426 std::lock_guard<std::mutex> Lock(Mu);
427 return State;
428 }
429
430 // Increment the current value asynchronously.
431 void increment(const std::nullptr_t &) {
432 {
433 std::lock_guard<std::mutex> Lock(Mu);
434 Queue.push_back(x: nullptr);
435 }
436 CV.notify_all();
437 }
438 };
439
440 FeatureModules.add(M: std::make_unique<AsyncCounter>());
441 auto &Client = start();
442
443 Client.notify(Method: "increment", Params: nullptr);
444 Client.notify(Method: "increment", Params: nullptr);
445 Client.notify(Method: "increment", Params: nullptr);
446 Client.sync();
447 EXPECT_EQ(3, FeatureModules.get<AsyncCounter>()->getSync());
448 // Throw some work on the queue to make sure shutdown blocks on it.
449 Client.notify(Method: "increment", Params: nullptr);
450 Client.notify(Method: "increment", Params: nullptr);
451 Client.notify(Method: "increment", Params: nullptr);
452 // And immediately shut down. FeatureModule destructor verifies we blocked.
453}
454
455TEST_F(LSPTest, DiagModuleTest) {
456 static constexpr llvm::StringLiteral DiagMsg = "DiagMsg";
457 class DiagModule final : public FeatureModule {
458 struct DiagHooks : public ASTListener {
459 void sawDiagnostic(const clang::Diagnostic &, clangd::Diag &D) override {
460 D.Message = DiagMsg.str();
461 }
462 };
463
464 public:
465 std::unique_ptr<ASTListener> astListeners() override {
466 return std::make_unique<DiagHooks>();
467 }
468 };
469 FeatureModules.add(M: std::make_unique<DiagModule>());
470
471 auto &Client = start();
472 Client.didOpen(Path: "foo.cpp", Content: "test;");
473 EXPECT_THAT(Client.diagnostics("foo.cpp"),
474 llvm::ValueIs(testing::ElementsAre(diagMessage(DiagMsg))));
475}
476} // namespace
477} // namespace clangd
478} // namespace clang
479

source code of clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp