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 // from buildInvocation
151 std::unique_ptr
<CompilerInvocation
> Invocation
;
152 format::FormatStyle Style
;
154 std::shared_ptr
<const PreambleData
> Preamble
;
155 std::optional
<ParsedAST
> AST
;
159 // Number of non-fatal errors seen.
160 unsigned ErrCount
= 0;
162 Checker(llvm::StringRef File
, const ClangdLSPServer::Options
&Opts
)
163 : File(File
), Opts(Opts
) {}
165 // Read compilation database and choose a compile command for the file.
166 bool buildCommand(const ThreadsafeFS
&TFS
) {
167 log("Loading compilation database...");
168 DirectoryBasedGlobalCompilationDatabase::Options
CDBOpts(TFS
);
169 CDBOpts
.CompileCommandsDir
=
170 Config::current().CompileFlags
.CDBSearch
.FixedCDBPath
;
171 std::unique_ptr
<GlobalCompilationDatabase
> BaseCDB
=
172 std::make_unique
<DirectoryBasedGlobalCompilationDatabase
>(CDBOpts
);
173 auto Mangler
= CommandMangler::detect();
174 Mangler
.SystemIncludeExtractor
=
175 getSystemIncludeExtractor(llvm::ArrayRef(Opts
.QueryDriverGlobs
));
176 if (Opts
.ResourceDir
)
177 Mangler
.ResourceDir
= *Opts
.ResourceDir
;
178 auto CDB
= std::make_unique
<OverlayCDB
>(
179 BaseCDB
.get(), std::vector
<std::string
>{}, std::move(Mangler
));
181 if (auto TrueCmd
= CDB
->getCompileCommand(File
)) {
182 Cmd
= std::move(*TrueCmd
);
183 log("Compile command {0} is: [{1}] {2}",
184 Cmd
.Heuristic
.empty() ? "from CDB" : Cmd
.Heuristic
, Cmd
.Directory
,
185 printArgv(Cmd
.CommandLine
));
187 Cmd
= CDB
->getFallbackCommand(File
);
188 log("Generic fallback command is: [{0}] {1}", Cmd
.Directory
,
189 printArgv(Cmd
.CommandLine
));
195 // Prepare inputs and build CompilerInvocation (parsed compile command).
196 bool buildInvocation(const ThreadsafeFS
&TFS
,
197 std::optional
<std::string
> Contents
) {
198 StoreDiags CaptureInvocationDiags
;
199 std::vector
<std::string
> CC1Args
;
200 Inputs
.CompileCommand
= Cmd
;
202 Inputs
.ClangTidyProvider
= Opts
.ClangTidyProvider
;
203 Inputs
.Opts
.PreambleParseForwardingFunctions
=
204 Opts
.PreambleParseForwardingFunctions
;
206 Inputs
.Contents
= *Contents
;
207 log("Imaginary source file contents:\n{0}", Inputs
.Contents
);
209 if (auto Contents
= TFS
.view(std::nullopt
)->getBufferForFile(File
)) {
210 Inputs
.Contents
= Contents
->get()->getBuffer().str();
212 elog("Couldn't read {0}: {1}", File
, Contents
.getError().message());
216 log("Parsing command...");
218 buildCompilerInvocation(Inputs
, CaptureInvocationDiags
, &CC1Args
);
219 auto InvocationDiags
= CaptureInvocationDiags
.take();
220 ErrCount
+= showErrors(InvocationDiags
);
221 log("internal (cc1) args are: {0}", printArgv(CC1Args
));
223 elog("Failed to parse command line");
227 // FIXME: Check that resource-dir/built-in-headers exist?
229 Style
= getFormatStyleForFile(File
, Inputs
.Contents
, TFS
);
234 // Build preamble and AST, and index them.
236 log("Building preamble...");
237 Preamble
= buildPreamble(
238 File
, *Invocation
, Inputs
, /*StoreInMemory=*/true,
239 [&](CapturedASTCtx Ctx
,
240 std::shared_ptr
<const include_cleaner::PragmaIncludes
> PI
) {
241 if (!Opts
.BuildDynamicSymbolIndex
)
243 log("Indexing headers...");
244 Index
.updatePreamble(File
, /*Version=*/"null", Ctx
.getASTContext(),
245 Ctx
.getPreprocessor(), *PI
);
248 elog("Failed to build preamble");
251 ErrCount
+= showErrors(Preamble
->Diags
);
253 log("Building AST...");
254 AST
= ParsedAST::build(File
, Inputs
, std::move(Invocation
),
255 /*InvocationDiags=*/std::vector
<Diag
>{}, Preamble
);
257 elog("Failed to build AST");
261 showErrors(AST
->getDiagnostics().drop_front(Preamble
->Diags
.size()));
263 if (Opts
.BuildDynamicSymbolIndex
) {
264 log("Indexing AST...");
265 Index
.updateMain(File
, *AST
);
268 if (!CheckTidyTime
.empty()) {
269 if (!CLANGD_TIDY_CHECKS
) {
270 elog("-{0} requires -DCLANGD_TIDY_CHECKS!", CheckTidyTime
.ArgStr
);
274 elog("Timing clang-tidy checks in asserts-mode is not representative!");
282 // For each check foo, we want to build with checks=-* and checks=-*,foo.
283 // (We do a full build rather than just AST matchers to meausre PPCallbacks).
285 // However, performance has both random noise and systematic changes, such as
286 // step-function slowdowns due to CPU scaling.
287 // We take the median of 5 measurements, and after every check discard the
288 // measurement if the baseline changed by >3%.
289 void checkTidyTimes() {
290 double Stability
= 0.03;
291 log("Timing AST build with individual clang-tidy checks (target accuracy "
295 using Duration
= std::chrono::nanoseconds
;
296 // Measure time elapsed by a block of code. Currently: user CPU time.
297 auto Time
= [&](auto &&Run
) -> Duration
{
298 llvm::sys::TimePoint
<> Elapsed
;
299 std::chrono::nanoseconds UserBegin
, UserEnd
, System
;
300 llvm::sys::Process::GetTimeUsage(Elapsed
, UserBegin
, System
);
302 llvm::sys::Process::GetTimeUsage(Elapsed
, UserEnd
, System
);
303 return UserEnd
- UserBegin
;
305 auto Change
= [&](Duration Exp
, Duration Base
) -> double {
306 return (double)(Exp
.count() - Base
.count()) / Base
.count();
308 // Build ParsedAST with a fixed check glob, and return the time taken.
309 auto Build
= [&](llvm::StringRef Checks
) -> Duration
{
310 TidyProvider CTProvider
= [&](tidy::ClangTidyOptions
&Opts
,
312 Opts
.Checks
= Checks
.str();
314 Inputs
.ClangTidyProvider
= CTProvider
;
315 // Sigh, can't reuse the CompilerInvocation.
316 IgnoringDiagConsumer IgnoreDiags
;
317 auto Invocation
= buildCompilerInvocation(Inputs
, IgnoreDiags
);
318 Duration Val
= Time([&] {
319 ParsedAST::build(File
, Inputs
, std::move(Invocation
), {}, Preamble
);
321 vlog(" Measured {0} ==> {1}", Checks
, Val
);
324 // Measure several times, return the median.
325 auto MedianTime
= [&](llvm::StringRef Checks
) -> Duration
{
326 std::array
<Duration
, 5> Measurements
;
327 for (auto &M
: Measurements
)
329 llvm::sort(Measurements
);
330 return Measurements
[Measurements
.size() / 2];
332 Duration Baseline
= MedianTime("-*");
333 log(" Baseline = {0}", Baseline
);
334 // Attempt to time a check, may update Baseline if it is unstable.
335 auto Measure
= [&](llvm::StringRef Check
) -> double {
337 Duration Median
= MedianTime(("-*," + Check
).str());
338 Duration NewBase
= MedianTime("-*");
340 // Value only usable if baseline is fairly consistent before/after.
341 double DeltaFraction
= Change(NewBase
, Baseline
);
343 vlog(" Baseline = {0}", Baseline
);
344 if (DeltaFraction
< -Stability
|| DeltaFraction
> Stability
) {
345 elog(" Speed unstable, discarding measurement.");
348 return Change(Median
, Baseline
);
352 for (const auto& Check
: listTidyChecks(CheckTidyTime
)) {
353 // vlog the check name in case we crash!
354 vlog(" Timing {0}", Check
);
355 double Fraction
= Measure(Check
);
356 log(" {0} = {1:P0}", Check
, Fraction
);
358 log("Finished individual clang-tidy checks");
360 // Restore old options.
361 Inputs
.ClangTidyProvider
= Opts
.ClangTidyProvider
;
364 // Build Inlay Hints for the entire AST or the specified range
365 void buildInlayHints(std::optional
<Range
> LineRange
) {
366 log("Building inlay hints");
367 auto Hints
= inlayHints(*AST
, LineRange
);
369 for (const auto &Hint
: Hints
) {
370 vlog(" {0} {1} {2}", Hint
.kind
, Hint
.position
, Hint
.label
);
374 void buildSemanticHighlighting(std::optional
<Range
> LineRange
) {
375 log("Building semantic highlighting");
377 getSemanticHighlightings(*AST
, /*IncludeInactiveRegionTokens=*/true);
378 for (const auto HL
: Highlights
)
379 if (!LineRange
|| LineRange
->contains(HL
.R
))
380 vlog(" {0} {1} {2}", HL
.R
, HL
.Kind
, HL
.Modifiers
);
383 // Run AST-based features at each token in the file.
384 void testLocationFeatures(std::optional
<Range
> LineRange
) {
385 trace::Span
Trace("testLocationFeatures");
386 log("Testing features at each token (may be slow in large files)");
387 auto &SM
= AST
->getSourceManager();
388 auto SpelledTokens
= AST
->getTokens().spelledTokens(SM
.getMainFileID());
390 CodeCompleteOptions CCOpts
= Opts
.CodeComplete
;
391 CCOpts
.Index
= &Index
;
393 for (const auto &Tok
: SpelledTokens
) {
394 unsigned Start
= AST
->getSourceManager().getFileOffset(Tok
.location());
395 unsigned End
= Start
+ Tok
.length();
396 Position Pos
= offsetToPosition(Inputs
.Contents
, Start
);
398 if (LineRange
&& !LineRange
->contains(Pos
))
401 trace::Span
Trace("Token");
402 SPAN_ATTACH(Trace
, "pos", Pos
);
403 SPAN_ATTACH(Trace
, "text", Tok
.text(AST
->getSourceManager()));
405 // FIXME: dumping the tokens may leak sensitive code into bug reports.
406 // Add an option to turn this off, once we decide how options work.
407 vlog(" {0} {1}", Pos
, Tok
.text(AST
->getSourceManager()));
408 auto Tree
= SelectionTree::createRight(AST
->getASTContext(),
409 AST
->getTokens(), Start
, End
);
410 Tweak::Selection
Selection(&Index
, *AST
, Start
, End
, std::move(Tree
),
412 // FS is only populated when applying a tweak, not during prepare as
413 // prepare should not do any I/O to be fast.
415 prepareTweaks(Selection
, Opts
.TweakFilter
, Opts
.FeatureModules
);
417 &AST
->getSourceManager().getFileManager().getVirtualFileSystem();
418 for (const auto &T
: Tweaks
) {
419 auto Result
= T
->apply(Selection
);
421 elog(" tweak: {0} ==> FAIL: {1}", T
->id(), Result
.takeError());
424 vlog(" tweak: {0}", T
->id());
427 unsigned Definitions
= locateSymbolAt(*AST
, Pos
, &Index
).size();
428 vlog(" definition: {0}", Definitions
);
430 auto Hover
= getHover(*AST
, Pos
, Style
, &Index
);
431 vlog(" hover: {0}", Hover
.has_value());
433 unsigned DocHighlights
= findDocumentHighlights(*AST
, Pos
).size();
434 vlog(" documentHighlight: {0}", DocHighlights
);
436 if (CheckCompletion
) {
437 Position EndPos
= offsetToPosition(Inputs
.Contents
, End
);
438 auto CC
= codeComplete(File
, EndPos
, Preamble
.get(), Inputs
, CCOpts
);
439 vlog(" code completion: {0}",
440 CC
.Completions
.empty() ? "<empty>" : CC
.Completions
[0].Name
);
448 bool check(llvm::StringRef File
, const ThreadsafeFS
&TFS
,
449 const ClangdLSPServer::Options
&Opts
) {
450 std::optional
<Range
> LineRange
;
451 if (!CheckFileLines
.empty()) {
452 uint32_t Begin
= 0, End
= std::numeric_limits
<uint32_t>::max();
453 StringRef
RangeStr(CheckFileLines
);
454 bool ParseError
= RangeStr
.consumeInteger(0, Begin
);
455 if (RangeStr
.empty()) {
458 ParseError
|= !RangeStr
.consume_front("-");
459 ParseError
|= RangeStr
.consumeInteger(0, End
);
461 if (ParseError
|| !RangeStr
.empty() || Begin
<= 0 || End
< Begin
) {
462 elog("Invalid --check-lines specified. Use Begin-End format, e.g. 3-17");
465 LineRange
= Range
{Position
{static_cast<int>(Begin
- 1), 0},
466 Position
{static_cast<int>(End
), 0}};
469 llvm::SmallString
<0> FakeFile
;
470 std::optional
<std::string
> Contents
;
472 llvm::sys::path::system_temp_directory(false, FakeFile
);
473 llvm::sys::path::append(FakeFile
, "test.cc");
480 auto xxx = std::string(N, 'x');
483 log("Testing on source file {0}", File
);
485 class OverrideConfigProvider
: public config::Provider
{
486 std::vector
<config::CompiledFragment
>
487 getFragments(const config::Params
&,
488 config::DiagnosticCallback Diag
) const override
{
490 // If we're timing clang-tidy checks, implicitly disabling the slow ones
491 // is counterproductive!
492 if (CheckTidyTime
.getNumOccurrences())
493 F
.Diagnostics
.ClangTidy
.FastCheckFilter
.emplace("None");
494 return {std::move(F
).compile(Diag
)};
497 auto ConfigProvider
=
498 config::Provider::combine({Opts
.ConfigProvider
, &OverrideConfig
});
500 auto ContextProvider
= ClangdServer::createConfiguredContextProvider(
501 ConfigProvider
.get(), nullptr);
502 WithContext
Ctx(ContextProvider(
505 : /*Don't turn on local configs for an arbitrary temp path.*/ ""));
506 Checker
C(File
, Opts
);
507 if (!C
.buildCommand(TFS
) || !C
.buildInvocation(TFS
, Contents
) ||
510 C
.buildInlayHints(LineRange
);
511 C
.buildSemanticHighlighting(LineRange
);
513 C
.testLocationFeatures(LineRange
);
515 log("All checks completed, {0} errors", C
.ErrCount
);
516 return C
.ErrCount
== 0;
519 } // namespace clangd