xref: /llvm-project/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp (revision a3b4d9147406cbd90090466a9b2b9bb2e9f6000c)
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(Client.transport(), FS, Opts);
71     ServerThread.emplace([&] { EXPECT_TRUE(Server->run()); });
72     Client.call("initialize", llvm::json::Object{});
73     return Client;
74   }
75 
76   void stop() {
77     assert(Server);
78     Client.call("shutdown", nullptr);
79     Client.notify("exit", 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("foo.cpp", Code.code());
133   auto &Def = Client.call("textDocument/definition",
134                           llvm::json::Object{
135                               {"textDocument", Client.documentID("foo.cpp")},
136                               {"position", Code.point()},
137                           });
138   llvm::json::Value Want = llvm::json::Array{llvm::json::Object{
139       {"uri", Client.uri("foo.cpp")}, {"range", Code.range()}}};
140   EXPECT_EQ(Def.takeValue(), Want);
141 }
142 
143 TEST_F(LSPTest, Diagnostics) {
144   auto &Client = start();
145   Client.didOpen("foo.cpp", "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("foo.cpp", "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("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("foo.cpp", 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       "textDocument/didSave",
174       llvm::json::Object{{"textDocument", Client.documentID("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       "textDocument/didSave",
182       llvm::json::Object{{"textDocument", Client.documentID("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(Client.call(MethodName, {}).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 Header(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("foo.hpp", Header.code());
220   Client.didOpen("foo.cpp", Source.code());
221 
222   auto Diags = Client.diagnostics("foo.cpp");
223   ASSERT_TRUE(Diags && !Diags->empty());
224   auto RenameDiag = Diags->front();
225 
226   auto RenameCommand =
227       (*Client
228             .call("textDocument/codeAction",
229                   llvm::json::Object{
230                       {"textDocument", Client.documentID("foo.cpp")},
231                       {"context",
232                        llvm::json::Object{
233                            {"diagnostics", llvm::json::Array{RenameDiag}}}},
234                       {"range", Source.range()}})
235             .takeValue()
236             .getAsArray())[0];
237 
238   ASSERT_EQ((*RenameCommand.getAsObject())["title"], "change 'foo' to 'Foo'");
239 
240   Client.expectServerCall("workspace/applyEdit");
241   Client.call("workspace/executeCommand", RenameCommand);
242   Client.sync();
243 
244   auto Params = Client.takeCallParams("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       {"edit", llvm::json::Object{
250                    {"changes",
251                     llvm::json::Object{
252                         {Uri("foo.hpp"), llvm::json::Array{llvm::json::Object{
253                                              {"range", Header.range()},
254                                              {"newText", "Foo"},
255                                          }}},
256 
257                         {Uri("foo.cpp"), llvm::json::Array{llvm::json::Object{
258                                              {"range", Source.range()},
259                                              {"newText", "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("a.cpp", "");
279   Client.didOpen("b.cpp", "");
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("foo.cpp", Code.code());
292   auto Items = Client
293                    .call("textDocument/prepareCallHierarchy",
294                          llvm::json::Object{
295                              {"textDocument", Client.documentID("foo.cpp")},
296                              {"position", Code.point()}})
297                    .takeValue();
298   auto FirstItem = (*Items.getAsArray())[0];
299   auto Calls = Client
300                    .call("callHierarchy/incomingCalls",
301                          llvm::json::Object{{"item", 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(".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("foo.cpp", "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("bar.cpp", "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("add", this, &MathModule::add);
342       Bind.method("get", this, &MathModule::get);
343       Changed = Bind.outgoingNotification("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           "get", "",
355           [Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); });
356     }
357   };
358   FeatureModules.add(std::make_unique<MathModule>());
359 
360   auto &Client = start();
361   Client.notify("add", 2);
362   Client.notify("add", 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, [&] { 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, [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("increment", this, &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(nullptr);
453       }
454       CV.notify_all();
455     }
456   };
457 
458   FeatureModules.add(std::make_unique<AsyncCounter>());
459   auto &Client = start();
460 
461   Client.notify("increment", nullptr);
462   Client.notify("increment", nullptr);
463   Client.notify("increment", 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("increment", nullptr);
468   Client.notify("increment", nullptr);
469   Client.notify("increment", 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(std::make_unique<DiagModule>());
488 
489   auto &Client = start();
490   Client.didOpen("foo.cpp", "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