brintos

brintos / llvm-project-archived public Read only

0
0
Text · 19.1 KiB · df8d075 Raw
535 lines · cpp
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-exception6//7//===----------------------------------------------------------------------===//8//9// Many basic problems can occur processing a file in clangd, e.g.:10//  - system includes are not found11//  - crash when indexing its AST12// 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 and23// 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("")};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("")};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(true)};104llvm::cl::opt<bool> CheckCompletion{105    "check-completion",106    llvm::cl::desc("Run code-completion at each point (slow)"),107    llvm::cl::init(false)};108llvm::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.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("[{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 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(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.143class Checker {144  // from constructor145  std::string File;146  ClangdLSPServer::Options Opts;147  // from buildCommand148  tooling::CompileCommand Cmd;149  std::unique_ptr<GlobalCompilationDatabase> BaseCDB;150  std::unique_ptr<GlobalCompilationDatabase> CDB;151  // from buildInvocation152  ParseInputs Inputs;153  std::unique_ptr<CompilerInvocation> Invocation;154  format::FormatStyle Style;155  std::optional<ModulesBuilder> ModulesManager;156  // from buildAST157  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("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 NDEBUG282      elog("Timing clang-tidy checks in asserts-mode is not representative!");283      #endif284      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 as294  // step-function slowdowns due to CPU scaling.295  // We take the median of 5 measurements, and after every check discard the296  // 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 range373  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 as427      // 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} // namespace461 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(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 ones505      // 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          ? File519          : /*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 clangd534} // namespace clang535