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