xref: /llvm-project/clang-tools-extra/clangd/tool/Check.cpp (revision 61fe67a4017375fd675f75652e857e837f77fa51)
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 "../clang-tidy/ClangTidyModule.h"
28 #include "../clang-tidy/ClangTidyModuleRegistry.h"
29 #include "../clang-tidy/ClangTidyOptions.h"
30 #include "../clang-tidy/GlobList.h"
31 #include "ClangdLSPServer.h"
32 #include "ClangdServer.h"
33 #include "CodeComplete.h"
34 #include "CompileCommands.h"
35 #include "Compiler.h"
36 #include "Config.h"
37 #include "ConfigFragment.h"
38 #include "ConfigProvider.h"
39 #include "Diagnostics.h"
40 #include "Feature.h"
41 #include "GlobalCompilationDatabase.h"
42 #include "Hover.h"
43 #include "InlayHints.h"
44 #include "ParsedAST.h"
45 #include "Preamble.h"
46 #include "Protocol.h"
47 #include "Selection.h"
48 #include "SemanticHighlighting.h"
49 #include "SourceCode.h"
50 #include "TidyProvider.h"
51 #include "XRefs.h"
52 #include "clang-include-cleaner/Record.h"
53 #include "index/FileIndex.h"
54 #include "refactor/Tweak.h"
55 #include "support/Context.h"
56 #include "support/Logger.h"
57 #include "support/ThreadsafeFS.h"
58 #include "support/Trace.h"
59 #include "clang/AST/ASTContext.h"
60 #include "clang/Basic/Diagnostic.h"
61 #include "clang/Basic/LLVM.h"
62 #include "clang/Format/Format.h"
63 #include "clang/Frontend/CompilerInvocation.h"
64 #include "clang/Tooling/CompilationDatabase.h"
65 #include "llvm/ADT/ArrayRef.h"
66 #include "llvm/ADT/STLExtras.h"
67 #include "llvm/ADT/SmallString.h"
68 #include "llvm/Support/Chrono.h"
69 #include "llvm/Support/CommandLine.h"
70 #include "llvm/Support/Path.h"
71 #include "llvm/Support/Process.h"
72 #include <array>
73 #include <chrono>
74 #include <cstdint>
75 #include <limits>
76 #include <memory>
77 #include <optional>
78 #include <utility>
79 #include <vector>
80 
81 namespace clang {
82 namespace clangd {
83 namespace {
84 
85 // These will never be shown in --help, ClangdMain doesn't list the category.
86 llvm::cl::opt<std::string> CheckTidyTime{
87     "check-tidy-time",
88     llvm::cl::desc("Print the overhead of checks matching this glob"),
89     llvm::cl::init("")};
90 llvm::cl::opt<std::string> CheckFileLines{
91     "check-lines",
92     llvm::cl::desc(
93         "Limits the range of tokens in -check file on which "
94         "various features are tested. Example --check-lines=3-7 restricts "
95         "testing to lines 3 to 7 (inclusive) or --check-lines=5 to restrict "
96         "to one line. Default is testing entire file."),
97     llvm::cl::init("")};
98 llvm::cl::opt<bool> CheckLocations{
99     "check-locations",
100     llvm::cl::desc(
101         "Runs certain features (e.g. hover) at each point in the file. "
102         "Somewhat slow."),
103     llvm::cl::init(true)};
104 llvm::cl::opt<bool> CheckCompletion{
105     "check-completion",
106     llvm::cl::desc("Run code-completion at each point (slow)"),
107     llvm::cl::init(false)};
108 llvm::cl::opt<bool> CheckWarnings{
109     "check-warnings",
110     llvm::cl::desc("Print warnings as well as errors"),
111     llvm::cl::init(false)};
112 
113 // Print the diagnostics meeting severity threshold, and return count of errors.
114 unsigned showErrors(llvm::ArrayRef<Diag> Diags) {
115   unsigned ErrCount = 0;
116   for (const auto &D : Diags) {
117     if (D.Severity >= DiagnosticsEngine::Error || CheckWarnings)
118       elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message);
119     if (D.Severity >= DiagnosticsEngine::Error)
120       ++ErrCount;
121   }
122   return ErrCount;
123 }
124 
125 std::vector<std::string> listTidyChecks(llvm::StringRef Glob) {
126   tidy::GlobList G(Glob);
127   tidy::ClangTidyCheckFactories CTFactories;
128   for (const auto &E : tidy::ClangTidyModuleRegistry::entries())
129     E.instantiate()->addCheckFactories(CTFactories);
130   std::vector<std::string> Result;
131   for (const auto &E : CTFactories)
132     if (G.contains(E.getKey()))
133       Result.push_back(E.getKey().str());
134   llvm::sort(Result);
135   return Result;
136 }
137 
138 // This class is just a linear pipeline whose functions get called in sequence.
139 // Each exercises part of clangd's logic on our test file and logs results.
140 // Later steps depend on state built in earlier ones (such as the AST).
141 // Many steps can fatally fail (return false), then subsequent ones cannot run.
142 // Nonfatal failures are logged and tracked in ErrCount.
143 class Checker {
144   // from constructor
145   std::string File;
146   ClangdLSPServer::Options Opts;
147   // from buildCommand
148   tooling::CompileCommand Cmd;
149   std::unique_ptr<GlobalCompilationDatabase> BaseCDB;
150   std::unique_ptr<GlobalCompilationDatabase> CDB;
151   // from buildInvocation
152   ParseInputs Inputs;
153   std::unique_ptr<CompilerInvocation> Invocation;
154   format::FormatStyle Style;
155   std::optional<ModulesBuilder> ModulesManager;
156   // from buildAST
157   std::shared_ptr<const PreambleData> Preamble;
158   std::optional<ParsedAST> AST;
159   FileIndex Index;
160 
161 public:
162   // Number of non-fatal errors seen.
163   unsigned ErrCount = 0;
164 
165   Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts)
166       : File(File), Opts(Opts), Index(/*SupportContainedRefs=*/true) {}
167 
168   // Read compilation database and choose a compile command for the file.
169   bool buildCommand(const ThreadsafeFS &TFS) {
170     log("Loading compilation database...");
171     DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
172     CDBOpts.CompileCommandsDir =
173         Config::current().CompileFlags.CDBSearch.FixedCDBPath;
174     BaseCDB =
175         std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts);
176     auto Mangler = CommandMangler::detect();
177     Mangler.SystemIncludeExtractor =
178         getSystemIncludeExtractor(llvm::ArrayRef(Opts.QueryDriverGlobs));
179     if (Opts.ResourceDir)
180       Mangler.ResourceDir = *Opts.ResourceDir;
181     CDB = std::make_unique<OverlayCDB>(
182         BaseCDB.get(), std::vector<std::string>{}, std::move(Mangler));
183 
184     if (auto TrueCmd = CDB->getCompileCommand(File)) {
185       Cmd = std::move(*TrueCmd);
186       log("Compile command {0} is: [{1}] {2}",
187           Cmd.Heuristic.empty() ? "from CDB" : Cmd.Heuristic, Cmd.Directory,
188           printArgv(Cmd.CommandLine));
189     } else {
190       Cmd = CDB->getFallbackCommand(File);
191       log("Generic fallback command is: [{0}] {1}", Cmd.Directory,
192           printArgv(Cmd.CommandLine));
193     }
194 
195     return true;
196   }
197 
198   // Prepare inputs and build CompilerInvocation (parsed compile command).
199   bool buildInvocation(const ThreadsafeFS &TFS,
200                        std::optional<std::string> Contents) {
201     StoreDiags CaptureInvocationDiags;
202     std::vector<std::string> CC1Args;
203     Inputs.CompileCommand = Cmd;
204     Inputs.TFS = &TFS;
205     Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
206     Inputs.Opts.PreambleParseForwardingFunctions =
207         Opts.PreambleParseForwardingFunctions;
208     if (Contents) {
209       Inputs.Contents = *Contents;
210       log("Imaginary source file contents:\n{0}", Inputs.Contents);
211     } else {
212       if (auto Contents = TFS.view(std::nullopt)->getBufferForFile(File)) {
213         Inputs.Contents = Contents->get()->getBuffer().str();
214       } else {
215         elog("Couldn't read {0}: {1}", File, Contents.getError().message());
216         return false;
217       }
218     }
219     if (Opts.EnableExperimentalModulesSupport) {
220       if (!ModulesManager)
221         ModulesManager.emplace(*CDB);
222       Inputs.ModulesManager = &*ModulesManager;
223     }
224     log("Parsing command...");
225     Invocation =
226         buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args);
227     auto InvocationDiags = CaptureInvocationDiags.take();
228     ErrCount += showErrors(InvocationDiags);
229     log("internal (cc1) args are: {0}", printArgv(CC1Args));
230     if (!Invocation) {
231       elog("Failed to parse command line");
232       return false;
233     }
234 
235     // FIXME: Check that resource-dir/built-in-headers exist?
236 
237     Style = getFormatStyleForFile(File, Inputs.Contents, TFS, false);
238 
239     return true;
240   }
241 
242   // Build preamble and AST, and index them.
243   bool buildAST() {
244     log("Building preamble...");
245     Preamble = buildPreamble(
246         File, *Invocation, Inputs, /*StoreInMemory=*/true,
247         [&](CapturedASTCtx Ctx,
248             std::shared_ptr<const include_cleaner::PragmaIncludes> PI) {
249           if (!Opts.BuildDynamicSymbolIndex)
250             return;
251           log("Indexing headers...");
252           Index.updatePreamble(File, /*Version=*/"null", Ctx.getASTContext(),
253                                Ctx.getPreprocessor(), *PI);
254         });
255     if (!Preamble) {
256       elog("Failed to build preamble");
257       return false;
258     }
259     ErrCount += showErrors(Preamble->Diags);
260 
261     log("Building AST...");
262     AST = ParsedAST::build(File, Inputs, std::move(Invocation),
263                            /*InvocationDiags=*/std::vector<Diag>{}, Preamble);
264     if (!AST) {
265       elog("Failed to build AST");
266       return false;
267     }
268     ErrCount +=
269         showErrors(AST->getDiagnostics().drop_front(Preamble->Diags.size()));
270 
271     if (Opts.BuildDynamicSymbolIndex) {
272       log("Indexing AST...");
273       Index.updateMain(File, *AST);
274     }
275 
276     if (!CheckTidyTime.empty()) {
277       if (!CLANGD_TIDY_CHECKS) {
278         elog("-{0} requires -DCLANGD_TIDY_CHECKS!", CheckTidyTime.ArgStr);
279         return false;
280       }
281       #ifndef NDEBUG
282       elog("Timing clang-tidy checks in asserts-mode is not representative!");
283       #endif
284       checkTidyTimes();
285     }
286 
287     return true;
288   }
289 
290   // For each check foo, we want to build with checks=-* and checks=-*,foo.
291   // (We do a full build rather than just AST matchers to meausre PPCallbacks).
292   //
293   // However, performance has both random noise and systematic changes, such as
294   // step-function slowdowns due to CPU scaling.
295   // We take the median of 5 measurements, and after every check discard the
296   // measurement if the baseline changed by >3%.
297   void checkTidyTimes() {
298     double Stability = 0.03;
299     log("Timing AST build with individual clang-tidy checks (target accuracy "
300         "{0:P0})",
301         Stability);
302 
303     using Duration = std::chrono::nanoseconds;
304     // Measure time elapsed by a block of code. Currently: user CPU time.
305     auto Time = [&](auto &&Run) -> Duration {
306       llvm::sys::TimePoint<> Elapsed;
307       std::chrono::nanoseconds UserBegin, UserEnd, System;
308       llvm::sys::Process::GetTimeUsage(Elapsed, UserBegin, System);
309       Run();
310       llvm::sys::Process::GetTimeUsage(Elapsed, UserEnd, System);
311       return UserEnd - UserBegin;
312     };
313     auto Change = [&](Duration Exp, Duration Base) -> double {
314       return (double)(Exp.count() - Base.count()) / Base.count();
315     };
316     // Build ParsedAST with a fixed check glob, and return the time taken.
317     auto Build = [&](llvm::StringRef Checks) -> Duration {
318       TidyProvider CTProvider = [&](tidy::ClangTidyOptions &Opts,
319                                     llvm::StringRef) {
320         Opts.Checks = Checks.str();
321       };
322       Inputs.ClangTidyProvider = CTProvider;
323       // Sigh, can't reuse the CompilerInvocation.
324       IgnoringDiagConsumer IgnoreDiags;
325       auto Invocation = buildCompilerInvocation(Inputs, IgnoreDiags);
326       Duration Val = Time([&] {
327         ParsedAST::build(File, Inputs, std::move(Invocation), {}, Preamble);
328       });
329       vlog("    Measured {0} ==> {1}", Checks, Val);
330       return Val;
331     };
332     // Measure several times, return the median.
333     auto MedianTime = [&](llvm::StringRef Checks) -> Duration {
334       std::array<Duration, 5> Measurements;
335       for (auto &M : Measurements)
336         M = Build(Checks);
337       llvm::sort(Measurements);
338       return Measurements[Measurements.size() / 2];
339     };
340     Duration Baseline = MedianTime("-*");
341     log("  Baseline = {0}", Baseline);
342     // Attempt to time a check, may update Baseline if it is unstable.
343     auto Measure = [&](llvm::StringRef Check) -> double {
344       for (;;) {
345         Duration Median = MedianTime(("-*," + Check).str());
346         Duration NewBase = MedianTime("-*");
347 
348         // Value only usable if baseline is fairly consistent before/after.
349         double DeltaFraction = Change(NewBase, Baseline);
350         Baseline = NewBase;
351         vlog("  Baseline = {0}", Baseline);
352         if (DeltaFraction < -Stability || DeltaFraction > Stability) {
353           elog("  Speed unstable, discarding measurement.");
354           continue;
355         }
356         return Change(Median, Baseline);
357       }
358     };
359 
360     for (const auto& Check : listTidyChecks(CheckTidyTime)) {
361       // vlog the check name in case we crash!
362       vlog("  Timing {0}", Check);
363       double Fraction = Measure(Check);
364       log("  {0} = {1:P0}", Check, Fraction);
365     }
366     log("Finished individual clang-tidy checks");
367 
368     // Restore old options.
369     Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
370   }
371 
372   // Build Inlay Hints for the entire AST or the specified range
373   void buildInlayHints(std::optional<Range> LineRange) {
374     log("Building inlay hints");
375     auto Hints = inlayHints(*AST, LineRange);
376 
377     for (const auto &Hint : Hints) {
378       vlog("  {0} {1} [{2}]", Hint.kind, Hint.position, [&] {
379         return llvm::join(llvm::map_range(Hint.label,
380                                           [&](auto &L) {
381                                             return llvm::formatv("{{{0}}", L);
382                                           }),
383                           ", ");
384       }());
385     }
386   }
387 
388   void buildSemanticHighlighting(std::optional<Range> LineRange) {
389     log("Building semantic highlighting");
390     auto Highlights =
391         getSemanticHighlightings(*AST, /*IncludeInactiveRegionTokens=*/true);
392     for (const auto HL : Highlights)
393       if (!LineRange || LineRange->contains(HL.R))
394         vlog(" {0} {1} {2}", HL.R, HL.Kind, HL.Modifiers);
395   }
396 
397   // Run AST-based features at each token in the file.
398   void testLocationFeatures(std::optional<Range> LineRange) {
399     trace::Span Trace("testLocationFeatures");
400     log("Testing features at each token (may be slow in large files)");
401     auto &SM = AST->getSourceManager();
402     auto SpelledTokens = AST->getTokens().spelledTokens(SM.getMainFileID());
403 
404     CodeCompleteOptions CCOpts = Opts.CodeComplete;
405     CCOpts.Index = &Index;
406 
407     for (const auto &Tok : SpelledTokens) {
408       unsigned Start = AST->getSourceManager().getFileOffset(Tok.location());
409       unsigned End = Start + Tok.length();
410       Position Pos = offsetToPosition(Inputs.Contents, Start);
411 
412       if (LineRange && !LineRange->contains(Pos))
413         continue;
414 
415       trace::Span Trace("Token");
416       SPAN_ATTACH(Trace, "pos", Pos);
417       SPAN_ATTACH(Trace, "text", Tok.text(AST->getSourceManager()));
418 
419       // FIXME: dumping the tokens may leak sensitive code into bug reports.
420       // Add an option to turn this off, once we decide how options work.
421       vlog("  {0} {1}", Pos, Tok.text(AST->getSourceManager()));
422       auto Tree = SelectionTree::createRight(AST->getASTContext(),
423                                              AST->getTokens(), Start, End);
424       Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree),
425                                  nullptr);
426       // FS is only populated when applying a tweak, not during prepare as
427       // prepare should not do any I/O to be fast.
428       auto Tweaks =
429           prepareTweaks(Selection, Opts.TweakFilter, Opts.FeatureModules);
430       Selection.FS =
431           &AST->getSourceManager().getFileManager().getVirtualFileSystem();
432       for (const auto &T : Tweaks) {
433         auto Result = T->apply(Selection);
434         if (!Result) {
435           elog("    tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError());
436           ++ErrCount;
437         } else {
438           vlog("    tweak: {0}", T->id());
439         }
440       }
441       unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size();
442       vlog("    definition: {0}", Definitions);
443 
444       auto Hover = getHover(*AST, Pos, Style, &Index);
445       vlog("    hover: {0}", Hover.has_value());
446 
447       unsigned DocHighlights = findDocumentHighlights(*AST, Pos).size();
448       vlog("    documentHighlight: {0}", DocHighlights);
449 
450       if (CheckCompletion) {
451         Position EndPos = offsetToPosition(Inputs.Contents, End);
452         auto CC = codeComplete(File, EndPos, Preamble.get(), Inputs, CCOpts);
453         vlog("    code completion: {0}",
454              CC.Completions.empty() ? "<empty>" : CC.Completions[0].Name);
455       }
456     }
457   }
458 };
459 
460 } // namespace
461 
462 bool check(llvm::StringRef File, const ThreadsafeFS &TFS,
463            const ClangdLSPServer::Options &Opts) {
464   std::optional<Range> LineRange;
465   if (!CheckFileLines.empty()) {
466     uint32_t Begin = 0, End = std::numeric_limits<uint32_t>::max();
467     StringRef RangeStr(CheckFileLines);
468     bool ParseError = RangeStr.consumeInteger(0, Begin);
469     if (RangeStr.empty()) {
470       End = Begin;
471     } else {
472       ParseError |= !RangeStr.consume_front("-");
473       ParseError |= RangeStr.consumeInteger(0, End);
474     }
475     if (ParseError || !RangeStr.empty() || Begin <= 0 || End < Begin) {
476       elog("Invalid --check-lines specified. Use Begin-End format, e.g. 3-17");
477       return false;
478     }
479     LineRange = Range{Position{static_cast<int>(Begin - 1), 0},
480                       Position{static_cast<int>(End), 0}};
481   }
482 
483   llvm::SmallString<0> FakeFile;
484   std::optional<std::string> Contents;
485   if (File.empty()) {
486     llvm::sys::path::system_temp_directory(false, FakeFile);
487     llvm::sys::path::append(FakeFile, "test.cc");
488     File = FakeFile;
489     Contents = R"cpp(
490       #include <stddef.h>
491       #include <string>
492 
493       size_t N = 50;
494       auto xxx = std::string(N, 'x');
495     )cpp";
496   }
497   log("Testing on source file {0}", File);
498 
499   class OverrideConfigProvider : public config::Provider {
500     std::vector<config::CompiledFragment>
501     getFragments(const config::Params &,
502                  config::DiagnosticCallback Diag) const override {
503       config::Fragment F;
504       // If we're timing clang-tidy checks, implicitly disabling the slow ones
505       // is counterproductive!
506       if (CheckTidyTime.getNumOccurrences())
507         F.Diagnostics.ClangTidy.FastCheckFilter.emplace("None");
508       return {std::move(F).compile(Diag)};
509     }
510   } OverrideConfig;
511   auto ConfigProvider =
512       config::Provider::combine({Opts.ConfigProvider, &OverrideConfig});
513 
514   auto ContextProvider = ClangdServer::createConfiguredContextProvider(
515       ConfigProvider.get(), nullptr);
516   WithContext Ctx(ContextProvider(
517       FakeFile.empty()
518           ? File
519           : /*Don't turn on local configs for an arbitrary temp path.*/ ""));
520   Checker C(File, Opts);
521   if (!C.buildCommand(TFS) || !C.buildInvocation(TFS, Contents) ||
522       !C.buildAST())
523     return false;
524   C.buildInlayHints(LineRange);
525   C.buildSemanticHighlighting(LineRange);
526   if (CheckLocations)
527     C.testLocationFeatures(LineRange);
528 
529   log("All checks completed, {0} errors", C.ErrCount);
530   return C.ErrCount == 0;
531 }
532 
533 } // namespace clangd
534 } // namespace clang
535