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 "Config.h"
30 #include "GlobalCompilationDatabase.h"
31 #include "Hover.h"
32 #include "ParsedAST.h"
33 #include "Preamble.h"
34 #include "SourceCode.h"
35 #include "XRefs.h"
36 #include "index/CanonicalIncludes.h"
37 #include "index/FileIndex.h"
38 #include "refactor/Tweak.h"
39 #include "support/ThreadsafeFS.h"
40 #include "clang/AST/ASTContext.h"
41 #include "clang/Basic/DiagnosticIDs.h"
42 #include "clang/Format/Format.h"
43 #include "clang/Frontend/CompilerInvocation.h"
44 #include "clang/Tooling/CompilationDatabase.h"
45 #include "llvm/ADT/ArrayRef.h"
46 #include "llvm/ADT/Optional.h"
47 #include "llvm/ADT/StringExtras.h"
48 #include "llvm/Support/Path.h"
49 
50 namespace clang {
51 namespace clangd {
52 namespace {
53 
54 // Print (and count) the error-level diagnostics (warnings are ignored).
55 unsigned showErrors(llvm::ArrayRef<Diag> Diags) {
56   unsigned ErrCount = 0;
57   for (const auto &D : Diags) {
58     if (D.Severity >= DiagnosticsEngine::Error) {
59       elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message);
60       ++ErrCount;
61     }
62   }
63   return ErrCount;
64 }
65 
66 // This class is just a linear pipeline whose functions get called in sequence.
67 // Each exercises part of clangd's logic on our test file and logs results.
68 // Later steps depend on state built in earlier ones (such as the AST).
69 // Many steps can fatally fail (return false), then subsequent ones cannot run.
70 // Nonfatal failures are logged and tracked in ErrCount.
71 class Checker {
72   // from constructor
73   std::string File;
74   ClangdLSPServer::Options Opts;
75   // from buildCommand
76   tooling::CompileCommand Cmd;
77   // from buildInvocation
78   ParseInputs Inputs;
79   std::unique_ptr<CompilerInvocation> Invocation;
80   format::FormatStyle Style;
81   // from buildAST
82   std::shared_ptr<const PreambleData> Preamble;
83   llvm::Optional<ParsedAST> AST;
84   FileIndex Index;
85 
86 public:
87   // Number of non-fatal errors seen.
88   unsigned ErrCount = 0;
89 
90   Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts)
91       : File(File), Opts(Opts) {}
92 
93   // Read compilation database and choose a compile command for the file.
94   bool buildCommand(const ThreadsafeFS &TFS) {
95     log("Loading compilation database...");
96     DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
97     CDBOpts.CompileCommandsDir =
98         Config::current().CompileFlags.CDBSearch.FixedCDBPath;
99     std::unique_ptr<GlobalCompilationDatabase> BaseCDB =
100         std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts);
101     BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs),
102                                      std::move(BaseCDB));
103     auto Mangler = CommandMangler::detect();
104     if (Opts.ResourceDir)
105       Mangler.ResourceDir = *Opts.ResourceDir;
106     auto CDB = std::make_unique<OverlayCDB>(
107         BaseCDB.get(), std::vector<std::string>{},
108         tooling::ArgumentsAdjuster(std::move(Mangler)));
109 
110     if (auto TrueCmd = CDB->getCompileCommand(File)) {
111       Cmd = std::move(*TrueCmd);
112       log("Compile command from CDB is: {0}", printArgv(Cmd.CommandLine));
113     } else {
114       Cmd = CDB->getFallbackCommand(File);
115       log("Generic fallback command is: {0}", printArgv(Cmd.CommandLine));
116     }
117 
118     return true;
119   }
120 
121   // Prepare inputs and build CompilerInvocation (parsed compile command).
122   bool buildInvocation(const ThreadsafeFS &TFS,
123                        llvm::Optional<std::string> Contents) {
124     StoreDiags CaptureInvocationDiags;
125     std::vector<std::string> CC1Args;
126     Inputs.CompileCommand = Cmd;
127     Inputs.TFS = &TFS;
128     Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
129     if (Contents.hasValue()) {
130       Inputs.Contents = *Contents;
131       log("Imaginary source file contents:\n{0}", Inputs.Contents);
132     } else {
133       if (auto Contents = TFS.view(llvm::None)->getBufferForFile(File)) {
134         Inputs.Contents = Contents->get()->getBuffer().str();
135       } else {
136         elog("Couldn't read {0}: {1}", File, Contents.getError().message());
137         return false;
138       }
139     }
140     log("Parsing command...");
141     Invocation =
142         buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args);
143     auto InvocationDiags = CaptureInvocationDiags.take();
144     ErrCount += showErrors(InvocationDiags);
145     log("internal (cc1) args are: {0}", printArgv(CC1Args));
146     if (!Invocation) {
147       elog("Failed to parse command line");
148       return false;
149     }
150 
151     // FIXME: Check that resource-dir/built-in-headers exist?
152 
153     Style = getFormatStyleForFile(File, Inputs.Contents, TFS);
154 
155     return true;
156   }
157 
158   // Build preamble and AST, and index them.
159   bool buildAST() {
160     log("Building preamble...");
161     Preamble =
162         buildPreamble(File, *Invocation, Inputs, /*StoreInMemory=*/true,
163                       [&](ASTContext &Ctx, std::shared_ptr<Preprocessor> PP,
164                           const CanonicalIncludes &Includes) {
165                         if (!Opts.BuildDynamicSymbolIndex)
166                           return;
167                         log("Indexing headers...");
168                         Index.updatePreamble(File, /*Version=*/"null", Ctx,
169                                              std::move(PP), Includes);
170                       });
171     if (!Preamble) {
172       elog("Failed to build preamble");
173       return false;
174     }
175     ErrCount += showErrors(Preamble->Diags);
176 
177     log("Building AST...");
178     AST = ParsedAST::build(File, Inputs, std::move(Invocation),
179                            /*InvocationDiags=*/std::vector<Diag>{}, Preamble);
180     if (!AST) {
181       elog("Failed to build AST");
182       return false;
183     }
184     ErrCount += showErrors(llvm::makeArrayRef(*AST->getDiagnostics())
185                                .drop_front(Preamble->Diags.size()));
186 
187     if (Opts.BuildDynamicSymbolIndex) {
188       log("Indexing AST...");
189       Index.updateMain(File, *AST);
190     }
191     return true;
192   }
193 
194   // Run AST-based features at each token in the file.
195   void testLocationFeatures(
196       llvm::function_ref<bool(const Position &)> ShouldCheckLine) {
197     log("Testing features at each token (may be slow in large files)");
198     auto &SM = AST->getSourceManager();
199     auto SpelledTokens = AST->getTokens().spelledTokens(SM.getMainFileID());
200     for (const auto &Tok : SpelledTokens) {
201       unsigned Start = AST->getSourceManager().getFileOffset(Tok.location());
202       unsigned End = Start + Tok.length();
203       Position Pos = offsetToPosition(Inputs.Contents, Start);
204 
205       if (!ShouldCheckLine(Pos))
206         continue;
207 
208       // FIXME: dumping the tokens may leak sensitive code into bug reports.
209       // Add an option to turn this off, once we decide how options work.
210       vlog("  {0} {1}", Pos, Tok.text(AST->getSourceManager()));
211       auto Tree = SelectionTree::createRight(AST->getASTContext(),
212                                              AST->getTokens(), Start, End);
213       Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree),
214                                  nullptr);
215       for (const auto &T :
216            prepareTweaks(Selection, Opts.TweakFilter, Opts.FeatureModules)) {
217         auto Result = T->apply(Selection);
218         if (!Result) {
219           elog("    tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError());
220           ++ErrCount;
221         } else {
222           vlog("    tweak: {0}", T->id());
223         }
224       }
225       unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size();
226       vlog("    definition: {0}", Definitions);
227 
228       auto Hover = getHover(*AST, Pos, Style, &Index);
229       vlog("    hover: {0}", Hover.hasValue());
230 
231       // FIXME: it'd be nice to include code completion, but it's too slow.
232       // Maybe in combination with a line restriction?
233     }
234   }
235 };
236 
237 } // namespace
238 
239 bool check(llvm::StringRef File,
240            llvm::function_ref<bool(const Position &)> ShouldCheckLine,
241            const ThreadsafeFS &TFS, const ClangdLSPServer::Options &Opts) {
242   llvm::SmallString<0> FakeFile;
243   llvm::Optional<std::string> Contents;
244   if (File.empty()) {
245     llvm::sys::path::system_temp_directory(false, FakeFile);
246     llvm::sys::path::append(FakeFile, "test.cc");
247     File = FakeFile;
248     Contents = R"cpp(
249       #include <stddef.h>
250       #include <string>
251 
252       size_t N = 50;
253       auto xxx = std::string(N, 'x');
254     )cpp";
255   }
256   log("Testing on source file {0}", File);
257 
258   auto ContextProvider = ClangdServer::createConfiguredContextProvider(
259       Opts.ConfigProvider, nullptr);
260   WithContext Ctx(ContextProvider(""));
261   Checker C(File, Opts);
262   if (!C.buildCommand(TFS) || !C.buildInvocation(TFS, Contents) ||
263       !C.buildAST())
264     return false;
265   C.testLocationFeatures(ShouldCheckLine);
266 
267   log("All checks completed, {0} errors", C.ErrCount);
268   return C.ErrCount == 0;
269 }
270 
271 } // namespace clangd
272 } // namespace clang
273