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