1 //===--- Check.cpp - clangd self-diagnostics ------------------------------===//
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
7 //===----------------------------------------------------------------------===//
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.
15 // One important use case is gathering information for bug reports.
16 // Another is reproducing crashes, and checking which setting prevent them.
18 // It simulates opening a file (determining compile command, parsing, indexing)
19 // and then running features at many locations.
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).
25 //===----------------------------------------------------------------------===//
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"
37 #include "ConfigFragment.h"
38 #include "ConfigProvider.h"
39 #include "Diagnostics.h"
41 #include "GlobalCompilationDatabase.h"
43 #include "InlayHints.h"
44 #include "ParsedAST.h"
47 #include "Selection.h"
48 #include "SemanticHighlighting.h"
49 #include "SourceCode.h"
50 #include "TidyProvider.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"
85 // These will never be shown in --help, ClangdMain doesn't list the category.
86 llvm::cl::opt
<std::string
> CheckTidyTime
{
88 llvm::cl::desc("Print the overhead of checks matching this glob"),
90 llvm::cl::opt
<std::string
> CheckFileLines
{
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."),
98 llvm::cl::opt
<bool> CheckLocations
{
101 "Runs certain features (e.g. hover) at each point in the file. "
103 llvm::cl::init(true)};
104 llvm::cl::opt
<bool> CheckCompletion
{
106 llvm::cl::desc("Run code-completion at each point (slow)"),
107 llvm::cl::init(false)};
108 llvm::cl::opt
<bool> CheckWarnings
{
110 llvm::cl::desc("Print warnings as well as errors"),
111 llvm::cl::init(false)};
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
)
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());
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.
146 ClangdLSPServer::Options Opts
;
148 tooling::CompileCommand Cmd
;
149 std::unique_ptr
<GlobalCompilationDatabase
> BaseCDB
;
150 std::unique_ptr
<GlobalCompilationDatabase
> CDB
;
151 // from buildInvocation
153 std::unique_ptr
<CompilerInvocation
> Invocation
;
154 format::FormatStyle Style
;
155 std::optional
<ModulesBuilder
> ModulesManager
;
157 std::shared_ptr
<const PreambleData
> Preamble
;
158 std::optional
<ParsedAST
> AST
;
162 // Number of non-fatal errors seen.
163 unsigned ErrCount
= 0;
165 Checker(llvm::StringRef File
, const ClangdLSPServer::Options
&Opts
)
166 : File(File
), Opts(Opts
) {}
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
;
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
));
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
));
190 Cmd
= CDB
->getFallbackCommand(File
);
191 log("Generic fallback command is: [{0}] {1}", Cmd
.Directory
,
192 printArgv(Cmd
.CommandLine
));
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
;
205 Inputs
.ClangTidyProvider
= Opts
.ClangTidyProvider
;
206 Inputs
.Opts
.PreambleParseForwardingFunctions
=
207 Opts
.PreambleParseForwardingFunctions
;
209 Inputs
.Contents
= *Contents
;
210 log("Imaginary source file contents:\n{0}", Inputs
.Contents
);
212 if (auto Contents
= TFS
.view(std::nullopt
)->getBufferForFile(File
)) {
213 Inputs
.Contents
= Contents
->get()->getBuffer().str();
215 elog("Couldn't read {0}: {1}", File
, Contents
.getError().message());
219 if (Opts
.EnableExperimentalModulesSupport
) {
221 ModulesManager
.emplace(*CDB
);
222 Inputs
.ModulesManager
= &*ModulesManager
;
224 log("Parsing command...");
226 buildCompilerInvocation(Inputs
, CaptureInvocationDiags
, &CC1Args
);
227 auto InvocationDiags
= CaptureInvocationDiags
.take();
228 ErrCount
+= showErrors(InvocationDiags
);
229 log("internal (cc1) args are: {0}", printArgv(CC1Args
));
231 elog("Failed to parse command line");
235 // FIXME: Check that resource-dir/built-in-headers exist?
237 Style
= getFormatStyleForFile(File
, Inputs
.Contents
, TFS
, false);
242 // Build preamble and AST, and index them.
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
)
251 log("Indexing headers...");
252 Index
.updatePreamble(File
, /*Version=*/"null", Ctx
.getASTContext(),
253 Ctx
.getPreprocessor(), *PI
);
256 elog("Failed to build preamble");
259 ErrCount
+= showErrors(Preamble
->Diags
);
261 log("Building AST...");
262 AST
= ParsedAST::build(File
, Inputs
, std::move(Invocation
),
263 /*InvocationDiags=*/std::vector
<Diag
>{}, Preamble
);
265 elog("Failed to build AST");
269 showErrors(AST
->getDiagnostics().drop_front(Preamble
->Diags
.size()));
271 if (Opts
.BuildDynamicSymbolIndex
) {
272 log("Indexing AST...");
273 Index
.updateMain(File
, *AST
);
276 if (!CheckTidyTime
.empty()) {
277 if (!CLANGD_TIDY_CHECKS
) {
278 elog("-{0} requires -DCLANGD_TIDY_CHECKS!", CheckTidyTime
.ArgStr
);
282 elog("Timing clang-tidy checks in asserts-mode is not representative!");
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).
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 "
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
);
310 llvm::sys::Process::GetTimeUsage(Elapsed
, UserEnd
, System
);
311 return UserEnd
- UserBegin
;
313 auto Change
= [&](Duration Exp
, Duration Base
) -> double {
314 return (double)(Exp
.count() - Base
.count()) / Base
.count();
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
,
320 Opts
.Checks
= Checks
.str();
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
);
329 vlog(" Measured {0} ==> {1}", Checks
, Val
);
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
)
337 llvm::sort(Measurements
);
338 return Measurements
[Measurements
.size() / 2];
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 {
345 Duration Median
= MedianTime(("-*," + Check
).str());
346 Duration NewBase
= MedianTime("-*");
348 // Value only usable if baseline is fairly consistent before/after.
349 double DeltaFraction
= Change(NewBase
, Baseline
);
351 vlog(" Baseline = {0}", Baseline
);
352 if (DeltaFraction
< -Stability
|| DeltaFraction
> Stability
) {
353 elog(" Speed unstable, discarding measurement.");
356 return Change(Median
, Baseline
);
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
);
366 log("Finished individual clang-tidy checks");
368 // Restore old options.
369 Inputs
.ClangTidyProvider
= Opts
.ClangTidyProvider
;
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
);
377 for (const auto &Hint
: Hints
) {
378 vlog(" {0} {1} [{2}]", Hint
.kind
, Hint
.position
, [&] {
379 return llvm::join(llvm::map_range(Hint
.label
,
381 return llvm::formatv("{{{0}}", L
);
388 void buildSemanticHighlighting(std::optional
<Range
> LineRange
) {
389 log("Building semantic highlighting");
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
);
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());
404 CodeCompleteOptions CCOpts
= Opts
.CodeComplete
;
405 CCOpts
.Index
= &Index
;
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
);
412 if (LineRange
&& !LineRange
->contains(Pos
))
415 trace::Span
Trace("Token");
416 SPAN_ATTACH(Trace
, "pos", Pos
);
417 SPAN_ATTACH(Trace
, "text", Tok
.text(AST
->getSourceManager()));
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
),
426 // FS is only populated when applying a tweak, not during prepare as
427 // prepare should not do any I/O to be fast.
429 prepareTweaks(Selection
, Opts
.TweakFilter
, Opts
.FeatureModules
);
431 &AST
->getSourceManager().getFileManager().getVirtualFileSystem();
432 for (const auto &T
: Tweaks
) {
433 auto Result
= T
->apply(Selection
);
435 elog(" tweak: {0} ==> FAIL: {1}", T
->id(), Result
.takeError());
438 vlog(" tweak: {0}", T
->id());
441 unsigned Definitions
= locateSymbolAt(*AST
, Pos
, &Index
).size();
442 vlog(" definition: {0}", Definitions
);
444 auto Hover
= getHover(*AST
, Pos
, Style
, &Index
);
445 vlog(" hover: {0}", Hover
.has_value());
447 unsigned DocHighlights
= findDocumentHighlights(*AST
, Pos
).size();
448 vlog(" documentHighlight: {0}", DocHighlights
);
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
);
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()) {
472 ParseError
|= !RangeStr
.consume_front("-");
473 ParseError
|= RangeStr
.consumeInteger(0, End
);
475 if (ParseError
|| !RangeStr
.empty() || Begin
<= 0 || End
< Begin
) {
476 elog("Invalid --check-lines specified. Use Begin-End format, e.g. 3-17");
479 LineRange
= Range
{Position
{static_cast<int>(Begin
- 1), 0},
480 Position
{static_cast<int>(End
), 0}};
483 llvm::SmallString
<0> FakeFile
;
484 std::optional
<std::string
> Contents
;
486 llvm::sys::path::system_temp_directory(false, FakeFile
);
487 llvm::sys::path::append(FakeFile
, "test.cc");
494 auto xxx = std::string(N, 'x');
497 log("Testing on source file {0}", File
);
499 class OverrideConfigProvider
: public config::Provider
{
500 std::vector
<config::CompiledFragment
>
501 getFragments(const config::Params
&,
502 config::DiagnosticCallback Diag
) const override
{
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
)};
511 auto ConfigProvider
=
512 config::Provider::combine({Opts
.ConfigProvider
, &OverrideConfig
});
514 auto ContextProvider
= ClangdServer::createConfiguredContextProvider(
515 ConfigProvider
.get(), nullptr);
516 WithContext
Ctx(ContextProvider(
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
) ||
524 C
.buildInlayHints(LineRange
);
525 C
.buildSemanticHighlighting(LineRange
);
527 C
.testLocationFeatures(LineRange
);
529 log("All checks completed, {0} errors", C
.ErrCount
);
530 return C
.ErrCount
== 0;
533 } // namespace clangd