1 //===--- Check.cpp - clangd self-diagnostics ------------------------------===//
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 // Many basic problems can occur processing a file in clangd, e.g.:
10 //  - system includes are not found
11 //  - crash when indexing its AST
12 // clangd --check provides a simplified, isolated way to reproduce these,
13 // with no editor, LSP, threads, background indexing etc to contend with.
14 //
15 // One important use case is gathering information for bug reports.
16 // Another is reproducing crashes, and checking which setting prevent them.
17 //
18 // It simulates opening a file (determining compile command, parsing, indexing)
19 // and then running features at many locations.
20 //
21 // Currently it adds some basic logging of progress and results.
22 // We should consider extending it to also recognize common symptoms and
23 // recommend solutions (e.g. standard library installation issues).
24 //
25 //===----------------------------------------------------------------------===//
26 
27 #include "ClangdLSPServer.h"
28 #include "CodeComplete.h"
29 #include "CompileCommands.h"
30 #include "Config.h"
31 #include "GlobalCompilationDatabase.h"
32 #include "Hover.h"
33 #include "InlayHints.h"
34 #include "ParsedAST.h"
35 #include "Preamble.h"
36 #include "Protocol.h"
37 #include "SourceCode.h"
38 #include "XRefs.h"
39 #include "index/CanonicalIncludes.h"
40 #include "index/FileIndex.h"
41 #include "refactor/Tweak.h"
42 #include "support/ThreadsafeFS.h"
43 #include "support/Trace.h"
44 #include "clang/AST/ASTContext.h"
45 #include "clang/Format/Format.h"
46 #include "clang/Frontend/CompilerInvocation.h"
47 #include "clang/Tooling/CompilationDatabase.h"
48 #include "llvm/ADT/ArrayRef.h"
49 #include "llvm/ADT/Optional.h"
50 #include "llvm/Support/Path.h"
51 
52 namespace clang {
53 namespace clangd {
54 namespace {
55 
56 // Print (and count) the error-level diagnostics (warnings are ignored).
57 unsigned showErrors(llvm::ArrayRef<Diag> Diags) {
58   unsigned ErrCount = 0;
59   for (const auto &D : Diags) {
60     if (D.Severity >= DiagnosticsEngine::Error) {
61       elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message);
62       ++ErrCount;
63     }
64   }
65   return ErrCount;
66 }
67 
68 // This class is just a linear pipeline whose functions get called in sequence.
69 // Each exercises part of clangd's logic on our test file and logs results.
70 // Later steps depend on state built in earlier ones (such as the AST).
71 // Many steps can fatally fail (return false), then subsequent ones cannot run.
72 // Nonfatal failures are logged and tracked in ErrCount.
73 class Checker {
74   // from constructor
75   std::string File;
76   ClangdLSPServer::Options Opts;
77   // from buildCommand
78   tooling::CompileCommand Cmd;
79   // from buildInvocation
80   ParseInputs Inputs;
81   std::unique_ptr<CompilerInvocation> Invocation;
82   format::FormatStyle Style;
83   // from buildAST
84   std::shared_ptr<const PreambleData> Preamble;
85   llvm::Optional<ParsedAST> AST;
86   FileIndex Index;
87 
88 public:
89   // Number of non-fatal errors seen.
90   unsigned ErrCount = 0;
91 
92   Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts)
93       : File(File), Opts(Opts) {}
94 
95   // Read compilation database and choose a compile command for the file.
96   bool buildCommand(const ThreadsafeFS &TFS) {
97     log("Loading compilation database...");
98     DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
99     CDBOpts.CompileCommandsDir =
100         Config::current().CompileFlags.CDBSearch.FixedCDBPath;
101     std::unique_ptr<GlobalCompilationDatabase> BaseCDB =
102         std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts);
103     BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs),
104                                      std::move(BaseCDB));
105     auto Mangler = CommandMangler::detect();
106     if (Opts.ResourceDir)
107       Mangler.ResourceDir = *Opts.ResourceDir;
108     auto CDB = std::make_unique<OverlayCDB>(
109         BaseCDB.get(), std::vector<std::string>{},
110         tooling::ArgumentsAdjuster(std::move(Mangler)));
111 
112     if (auto TrueCmd = CDB->getCompileCommand(File)) {
113       Cmd = std::move(*TrueCmd);
114       log("Compile command from CDB is: {0}", printArgv(Cmd.CommandLine));
115     } else {
116       Cmd = CDB->getFallbackCommand(File);
117       log("Generic fallback command is: {0}", printArgv(Cmd.CommandLine));
118     }
119 
120     return true;
121   }
122 
123   // Prepare inputs and build CompilerInvocation (parsed compile command).
124   bool buildInvocation(const ThreadsafeFS &TFS,
125                        llvm::Optional<std::string> Contents) {
126     StoreDiags CaptureInvocationDiags;
127     std::vector<std::string> CC1Args;
128     Inputs.CompileCommand = Cmd;
129     Inputs.TFS = &TFS;
130     Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
131     Inputs.Opts.PreambleParseForwardingFunctions =
132         Opts.PreambleParseForwardingFunctions;
133     if (Contents) {
134       Inputs.Contents = *Contents;
135       log("Imaginary source file contents:\n{0}", Inputs.Contents);
136     } else {
137       if (auto Contents = TFS.view(llvm::None)->getBufferForFile(File)) {
138         Inputs.Contents = Contents->get()->getBuffer().str();
139       } else {
140         elog("Couldn't read {0}: {1}", File, Contents.getError().message());
141         return false;
142       }
143     }
144     log("Parsing command...");
145     Invocation =
146         buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args);
147     auto InvocationDiags = CaptureInvocationDiags.take();
148     ErrCount += showErrors(InvocationDiags);
149     log("internal (cc1) args are: {0}", printArgv(CC1Args));
150     if (!Invocation) {
151       elog("Failed to parse command line");
152       return false;
153     }
154 
155     // FIXME: Check that resource-dir/built-in-headers exist?
156 
157     Style = getFormatStyleForFile(File, Inputs.Contents, TFS);
158 
159     return true;
160   }
161 
162   // Build preamble and AST, and index them.
163   bool buildAST() {
164     log("Building preamble...");
165     Preamble = buildPreamble(File, *Invocation, Inputs, /*StoreInMemory=*/true,
166                              [&](ASTContext &Ctx, Preprocessor &PP,
167                                  const CanonicalIncludes &Includes) {
168                                if (!Opts.BuildDynamicSymbolIndex)
169                                  return;
170                                log("Indexing headers...");
171                                Index.updatePreamble(File, /*Version=*/"null",
172                                                     Ctx, PP, Includes);
173                              });
174     if (!Preamble) {
175       elog("Failed to build preamble");
176       return false;
177     }
178     ErrCount += showErrors(Preamble->Diags);
179 
180     log("Building AST...");
181     AST = ParsedAST::build(File, Inputs, std::move(Invocation),
182                            /*InvocationDiags=*/std::vector<Diag>{}, Preamble);
183     if (!AST) {
184       elog("Failed to build AST");
185       return false;
186     }
187     ErrCount += showErrors(llvm::makeArrayRef(*AST->getDiagnostics())
188                                .drop_front(Preamble->Diags.size()));
189 
190     if (Opts.BuildDynamicSymbolIndex) {
191       log("Indexing AST...");
192       Index.updateMain(File, *AST);
193     }
194     return true;
195   }
196 
197   // Build Inlay Hints for the entire AST or the specified range
198   void buildInlayHints(llvm::Optional<Range> LineRange) {
199     log("Building inlay hints");
200     auto Hints = inlayHints(*AST, LineRange);
201 
202     for (const auto &Hint : Hints) {
203       vlog("  {0} {1} {2}", Hint.kind, Hint.position, Hint.label);
204     }
205   }
206 
207   // Run AST-based features at each token in the file.
208   void testLocationFeatures(llvm::Optional<Range> LineRange,
209                             const bool EnableCodeCompletion) {
210     trace::Span Trace("testLocationFeatures");
211     log("Testing features at each token (may be slow in large files)");
212     auto &SM = AST->getSourceManager();
213     auto SpelledTokens = AST->getTokens().spelledTokens(SM.getMainFileID());
214 
215     CodeCompleteOptions CCOpts = Opts.CodeComplete;
216     CCOpts.Index = &Index;
217 
218     for (const auto &Tok : SpelledTokens) {
219       unsigned Start = AST->getSourceManager().getFileOffset(Tok.location());
220       unsigned End = Start + Tok.length();
221       Position Pos = offsetToPosition(Inputs.Contents, Start);
222 
223       if (LineRange && !LineRange->contains(Pos))
224         continue;
225 
226       trace::Span Trace("Token");
227       SPAN_ATTACH(Trace, "pos", Pos);
228       SPAN_ATTACH(Trace, "text", Tok.text(AST->getSourceManager()));
229 
230       // FIXME: dumping the tokens may leak sensitive code into bug reports.
231       // Add an option to turn this off, once we decide how options work.
232       vlog("  {0} {1}", Pos, Tok.text(AST->getSourceManager()));
233       auto Tree = SelectionTree::createRight(AST->getASTContext(),
234                                              AST->getTokens(), Start, End);
235       Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree),
236                                  nullptr);
237       // FS is only populated when applying a tweak, not during prepare as
238       // prepare should not do any I/O to be fast.
239       auto Tweaks =
240           prepareTweaks(Selection, Opts.TweakFilter, Opts.FeatureModules);
241       Selection.FS =
242           &AST->getSourceManager().getFileManager().getVirtualFileSystem();
243       for (const auto &T : Tweaks) {
244         auto Result = T->apply(Selection);
245         if (!Result) {
246           elog("    tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError());
247           ++ErrCount;
248         } else {
249           vlog("    tweak: {0}", T->id());
250         }
251       }
252       unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size();
253       vlog("    definition: {0}", Definitions);
254 
255       auto Hover = getHover(*AST, Pos, Style, &Index);
256       vlog("    hover: {0}", Hover.has_value());
257 
258       unsigned DocHighlights = findDocumentHighlights(*AST, Pos).size();
259       vlog("    documentHighlight: {0}", DocHighlights);
260 
261       if (EnableCodeCompletion) {
262         Position EndPos = offsetToPosition(Inputs.Contents, End);
263         auto CC = codeComplete(File, EndPos, Preamble.get(), Inputs, CCOpts);
264         vlog("    code completion: {0}",
265              CC.Completions.empty() ? "<empty>" : CC.Completions[0].Name);
266       }
267     }
268   }
269 };
270 
271 } // namespace
272 
273 bool check(llvm::StringRef File, llvm::Optional<Range> LineRange,
274            const ThreadsafeFS &TFS, const ClangdLSPServer::Options &Opts,
275            bool EnableCodeCompletion) {
276   llvm::SmallString<0> FakeFile;
277   llvm::Optional<std::string> Contents;
278   if (File.empty()) {
279     llvm::sys::path::system_temp_directory(false, FakeFile);
280     llvm::sys::path::append(FakeFile, "test.cc");
281     File = FakeFile;
282     Contents = R"cpp(
283       #include <stddef.h>
284       #include <string>
285 
286       size_t N = 50;
287       auto xxx = std::string(N, 'x');
288     )cpp";
289   }
290   log("Testing on source file {0}", File);
291 
292   auto ContextProvider = ClangdServer::createConfiguredContextProvider(
293       Opts.ConfigProvider, nullptr);
294   WithContext Ctx(ContextProvider(
295       FakeFile.empty()
296           ? File
297           : /*Don't turn on local configs for an arbitrary temp path.*/ ""));
298   Checker C(File, Opts);
299   if (!C.buildCommand(TFS) || !C.buildInvocation(TFS, Contents) ||
300       !C.buildAST())
301     return false;
302   C.buildInlayHints(LineRange);
303   C.testLocationFeatures(LineRange, EnableCodeCompletion);
304 
305   log("All checks completed, {0} errors", C.ErrCount);
306   return C.ErrCount == 0;
307 }
308 
309 } // namespace clangd
310 } // namespace clang
311