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
81namespace clang {
82namespace clangd {
83namespace {
84
85// These will never be shown in --help, ClangdMain doesn't list the category.
86llvm::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(Val: "")};
90llvm::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(Val: "")};
98llvm::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(Val: true)};
104llvm::cl::opt<bool> CheckCompletion{
105 "check-completion",
106 llvm::cl::desc("Run code-completion at each point (slow)"),
107 llvm::cl::init(Val: false)};
108llvm::cl::opt<bool> CheckWarnings{
109 "check-warnings",
110 llvm::cl::desc("Print warnings as well as errors"),
111 llvm::cl::init(Val: false)};
112
113// Print the diagnostics meeting severity threshold, and return count of errors.
114unsigned showErrors(llvm::ArrayRef<Diag> Diags) {
115 unsigned ErrCount = 0;
116 for (const auto &D : Diags) {
117 if (D.Severity >= DiagnosticsEngine::Error || CheckWarnings)
118 elog(Fmt: "[{0}] Line {1}: {2}", Vals: D.Name, Vals: D.Range.start.line + 1, Vals: D.Message);
119 if (D.Severity >= DiagnosticsEngine::Error)
120 ++ErrCount;
121 }
122 return ErrCount;
123}
124
125std::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(CheckFactories&: CTFactories);
130 std::vector<std::string> Result;
131 for (const auto &E : CTFactories)
132 if (G.contains(S: E.getKey()))
133 Result.push_back(x: E.getKey().str());
134 llvm::sort(C&: 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.
143class 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
161public:
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(Fmt: "Loading compilation database...");
171 DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
172 CDBOpts.CompileCommandsDir =
173 Config::current().CompileFlags.CDBSearch.FixedCDBPath;
174 BaseCDB =
175 std::make_unique<DirectoryBasedGlobalCompilationDatabase>(args&: CDBOpts);
176 auto Mangler = CommandMangler::detect();
177 Mangler.SystemIncludeExtractor =
178 getSystemIncludeExtractor(QueryDriverGlobs: llvm::ArrayRef(Opts.QueryDriverGlobs));
179 if (Opts.ResourceDir)
180 Mangler.ResourceDir = *Opts.ResourceDir;
181 CDB = std::make_unique<OverlayCDB>(
182 args: BaseCDB.get(), args: std::vector<std::string>{}, args: std::move(Mangler));
183
184 if (auto TrueCmd = CDB->getCompileCommand(File)) {
185 Cmd = std::move(*TrueCmd);
186 log(Fmt: "Compile command {0} is: [{1}] {2}",
187 Vals: Cmd.Heuristic.empty() ? "from CDB" : Cmd.Heuristic, Vals&: Cmd.Directory,
188 Vals: printArgv(Args: Cmd.CommandLine));
189 } else {
190 Cmd = CDB->getFallbackCommand(File);
191 log(Fmt: "Generic fallback command is: [{0}] {1}", Vals&: Cmd.Directory,
192 Vals: printArgv(Args: 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(Fmt: "Imaginary source file contents:\n{0}", Vals&: Inputs.Contents);
211 } else {
212 if (auto Contents = TFS.view(CWD: std::nullopt)->getBufferForFile(Name: File)) {
213 Inputs.Contents = Contents->get()->getBuffer().str();
214 } else {
215 elog(Fmt: "Couldn't read {0}: {1}", Vals&: File, Vals: Contents.getError().message());
216 return false;
217 }
218 }
219 if (Opts.EnableExperimentalModulesSupport) {
220 if (!ModulesManager)
221 ModulesManager.emplace(args&: *CDB);
222 Inputs.ModulesManager = &*ModulesManager;
223 }
224 log(Fmt: "Parsing command...");
225 Invocation =
226 buildCompilerInvocation(Inputs, D&: CaptureInvocationDiags, CC1Args: &CC1Args);
227 auto InvocationDiags = CaptureInvocationDiags.take();
228 ErrCount += showErrors(Diags: InvocationDiags);
229 log(Fmt: "internal (cc1) args are: {0}", Vals: printArgv(Args: CC1Args));
230 if (!Invocation) {
231 elog(Fmt: "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, Content: Inputs.Contents, TFS, FormatFile: false);
238
239 return true;
240 }
241
242 // Build preamble and AST, and index them.
243 bool buildAST() {
244 log(Fmt: "Building preamble...");
245 Preamble = buildPreamble(
246 FileName: File, CI: *Invocation, Inputs, /*StoreInMemory=*/true,
247 PreambleCallback: [&](CapturedASTCtx Ctx,
248 std::shared_ptr<const include_cleaner::PragmaIncludes> PI) {
249 if (!Opts.BuildDynamicSymbolIndex)
250 return;
251 log(Fmt: "Indexing headers...");
252 Index.updatePreamble(Path: File, /*Version=*/"null", AST&: Ctx.getASTContext(),
253 PP&: Ctx.getPreprocessor(), PI: *PI);
254 });
255 if (!Preamble) {
256 elog(Fmt: "Failed to build preamble");
257 return false;
258 }
259 ErrCount += showErrors(Diags: Preamble->Diags);
260
261 log(Fmt: "Building AST...");
262 AST = ParsedAST::build(Filename: File, Inputs, CI: std::move(Invocation),
263 /*InvocationDiags=*/CompilerInvocationDiags: std::vector<Diag>{}, Preamble);
264 if (!AST) {
265 elog(Fmt: "Failed to build AST");
266 return false;
267 }
268 ErrCount +=
269 showErrors(Diags: AST->getDiagnostics().drop_front(N: Preamble->Diags.size()));
270
271 if (Opts.BuildDynamicSymbolIndex) {
272 log(Fmt: "Indexing AST...");
273 Index.updateMain(Path: File, AST&: *AST);
274 }
275
276 if (!CheckTidyTime.empty()) {
277 if (!CLANGD_TIDY_CHECKS) {
278 elog(Fmt: "-{0} requires -DCLANGD_TIDY_CHECKS!", Vals&: CheckTidyTime.ArgStr);
279 return false;
280 }
281 #ifndef NDEBUG
282 elog(Fmt: "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(Fmt: "Timing AST build with individual clang-tidy checks (target accuracy "
300 "{0:P0})",
301 Vals&: 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&: Elapsed, user_time&: UserBegin, sys_time&: System);
309 Run();
310 llvm::sys::Process::GetTimeUsage(elapsed&: Elapsed, user_time&: UserEnd, sys_time&: 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, D&: IgnoreDiags);
326 Duration Val = Time([&] {
327 ParsedAST::build(Filename: File, Inputs, CI: std::move(Invocation), CompilerInvocationDiags: {}, Preamble);
328 });
329 vlog(Fmt: " Measured {0} ==> {1}", Vals&: Checks, Vals&: 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(C&: Measurements);
338 return Measurements[Measurements.size() / 2];
339 };
340 Duration Baseline = MedianTime("-*");
341 log(Fmt: " Baseline = {0}", Vals&: 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(Fmt: " Baseline = {0}", Vals&: Baseline);
352 if (DeltaFraction < -Stability || DeltaFraction > Stability) {
353 elog(Fmt: " Speed unstable, discarding measurement.");
354 continue;
355 }
356 return Change(Median, Baseline);
357 }
358 };
359
360 for (const auto& Check : listTidyChecks(Glob: CheckTidyTime)) {
361 // vlog the check name in case we crash!
362 vlog(Fmt: " Timing {0}", Vals: Check);
363 double Fraction = Measure(Check);
364 log(Fmt: " {0} = {1:P0}", Vals: Check, Vals&: Fraction);
365 }
366 log(Fmt: "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(Fmt: "Building inlay hints");
375 auto Hints = inlayHints(AST&: *AST, RestrictRange: LineRange);
376
377 for (const auto &Hint : Hints) {
378 vlog(Fmt: " {0} {1} [{2}]", Vals: Hint.kind, Vals: Hint.position, Vals: [&] {
379 return llvm::join(R: llvm::map_range(C: Hint.label,
380 F: [&](auto &L) {
381 return llvm::formatv("{{{0}}", L);
382 }),
383 Separator: ", ");
384 }());
385 }
386 }
387
388 void buildSemanticHighlighting(std::optional<Range> LineRange) {
389 log(Fmt: "Building semantic highlighting");
390 auto Highlights =
391 getSemanticHighlightings(AST&: *AST, /*IncludeInactiveRegionTokens=*/true);
392 for (const auto HL : Highlights)
393 if (!LineRange || LineRange->contains(Rng: HL.R))
394 vlog(Fmt: " {0} {1} {2}", Vals: HL.R, Vals: HL.Kind, Vals: 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(Fmt: "Testing features at each token (may be slow in large files)");
401 auto &SM = AST->getSourceManager();
402 auto SpelledTokens = AST->getTokens().spelledTokens(FID: 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(SpellingLoc: Tok.location());
409 unsigned End = Start + Tok.length();
410 Position Pos = offsetToPosition(Code: Inputs.Contents, Offset: 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(Fmt: " {0} {1}", Vals&: Pos, Vals: Tok.text(SM: AST->getSourceManager()));
422 auto Tree = SelectionTree::createRight(AST&: AST->getASTContext(),
423 Tokens: AST->getTokens(), Begin: 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(S: Selection, Filter: Opts.TweakFilter, Modules: Opts.FeatureModules);
430 Selection.FS =
431 &AST->getSourceManager().getFileManager().getVirtualFileSystem();
432 for (const auto &T : Tweaks) {
433 auto Result = T->apply(Sel: Selection);
434 if (!Result) {
435 elog(Fmt: " tweak: {0} ==> FAIL: {1}", Vals: T->id(), Vals: Result.takeError());
436 ++ErrCount;
437 } else {
438 vlog(Fmt: " tweak: {0}", Vals: T->id());
439 }
440 }
441 unsigned Definitions = locateSymbolAt(AST&: *AST, Pos, Index: &Index).size();
442 vlog(Fmt: " definition: {0}", Vals&: Definitions);
443
444 auto Hover = getHover(AST&: *AST, Pos, Style, Index: &Index);
445 vlog(Fmt: " hover: {0}", Vals: Hover.has_value());
446
447 unsigned DocHighlights = findDocumentHighlights(AST&: *AST, Pos).size();
448 vlog(Fmt: " documentHighlight: {0}", Vals&: DocHighlights);
449
450 if (CheckCompletion) {
451 Position EndPos = offsetToPosition(Code: Inputs.Contents, Offset: End);
452 auto CC = codeComplete(FileName: File, Pos: EndPos, Preamble: Preamble.get(), ParseInput: Inputs, Opts: CCOpts);
453 vlog(Fmt: " code completion: {0}",
454 Vals: CC.Completions.empty() ? "<empty>" : CC.Completions[0].Name);
455 }
456 }
457 }
458};
459
460} // namespace
461
462bool 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(Radix: 0, Result&: Begin);
469 if (RangeStr.empty()) {
470 End = Begin;
471 } else {
472 ParseError |= !RangeStr.consume_front(Prefix: "-");
473 ParseError |= RangeStr.consumeInteger(Radix: 0, Result&: End);
474 }
475 if (ParseError || !RangeStr.empty() || Begin <= 0 || End < Begin) {
476 elog(Fmt: "Invalid --check-lines specified. Use Begin-End format, e.g. 3-17");
477 return false;
478 }
479 LineRange = Range{.start: Position{.line: static_cast<int>(Begin - 1), .character: 0},
480 .end: Position{.line: static_cast<int>(End), .character: 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(erasedOnReboot: false, result&: FakeFile);
487 llvm::sys::path::append(path&: FakeFile, a: "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(Fmt: "Testing on source file {0}", Vals&: 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(args: "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 Provider: 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(Fmt: "All checks completed, {0} errors", Vals&: C.ErrCount);
530 return C.ErrCount == 0;
531}
532
533} // namespace clangd
534} // namespace clang
535

Provided by KDAB

Privacy Policy
Update your C++ knowledge – Modern C++11/14/17 Training
Find out more

source code of clang-tools-extra/clangd/tool/Check.cpp