mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-16 23:06:34 +00:00
535 lines
19 KiB
C++
535 lines
19 KiB
C++
//===--- Check.cpp - clangd self-diagnostics ------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// Many basic problems can occur processing a file in clangd, e.g.:
|
|
// - system includes are not found
|
|
// - crash when indexing its AST
|
|
// clangd --check provides a simplified, isolated way to reproduce these,
|
|
// with no editor, LSP, threads, background indexing etc to contend with.
|
|
//
|
|
// One important use case is gathering information for bug reports.
|
|
// Another is reproducing crashes, and checking which setting prevent them.
|
|
//
|
|
// It simulates opening a file (determining compile command, parsing, indexing)
|
|
// and then running features at many locations.
|
|
//
|
|
// Currently it adds some basic logging of progress and results.
|
|
// We should consider extending it to also recognize common symptoms and
|
|
// recommend solutions (e.g. standard library installation issues).
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "../clang-tidy/ClangTidyModule.h"
|
|
#include "../clang-tidy/ClangTidyModuleRegistry.h"
|
|
#include "../clang-tidy/ClangTidyOptions.h"
|
|
#include "../clang-tidy/GlobList.h"
|
|
#include "ClangdLSPServer.h"
|
|
#include "ClangdServer.h"
|
|
#include "CodeComplete.h"
|
|
#include "CompileCommands.h"
|
|
#include "Compiler.h"
|
|
#include "Config.h"
|
|
#include "ConfigFragment.h"
|
|
#include "ConfigProvider.h"
|
|
#include "Diagnostics.h"
|
|
#include "Feature.h"
|
|
#include "GlobalCompilationDatabase.h"
|
|
#include "Hover.h"
|
|
#include "InlayHints.h"
|
|
#include "ParsedAST.h"
|
|
#include "Preamble.h"
|
|
#include "Protocol.h"
|
|
#include "Selection.h"
|
|
#include "SemanticHighlighting.h"
|
|
#include "SourceCode.h"
|
|
#include "TidyProvider.h"
|
|
#include "XRefs.h"
|
|
#include "clang-include-cleaner/Record.h"
|
|
#include "index/FileIndex.h"
|
|
#include "refactor/Tweak.h"
|
|
#include "support/Context.h"
|
|
#include "support/Logger.h"
|
|
#include "support/ThreadsafeFS.h"
|
|
#include "support/Trace.h"
|
|
#include "clang/AST/ASTContext.h"
|
|
#include "clang/Basic/Diagnostic.h"
|
|
#include "clang/Basic/LLVM.h"
|
|
#include "clang/Format/Format.h"
|
|
#include "clang/Frontend/CompilerInvocation.h"
|
|
#include "clang/Tooling/CompilationDatabase.h"
|
|
#include "llvm/ADT/ArrayRef.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/SmallString.h"
|
|
#include "llvm/Support/Chrono.h"
|
|
#include "llvm/Support/CommandLine.h"
|
|
#include "llvm/Support/Path.h"
|
|
#include "llvm/Support/Process.h"
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace clang {
|
|
namespace clangd {
|
|
namespace {
|
|
|
|
// These will never be shown in --help, ClangdMain doesn't list the category.
|
|
llvm::cl::opt<std::string> CheckTidyTime{
|
|
"check-tidy-time",
|
|
llvm::cl::desc("Print the overhead of checks matching this glob"),
|
|
llvm::cl::init("")};
|
|
llvm::cl::opt<std::string> CheckFileLines{
|
|
"check-lines",
|
|
llvm::cl::desc(
|
|
"Limits the range of tokens in -check file on which "
|
|
"various features are tested. Example --check-lines=3-7 restricts "
|
|
"testing to lines 3 to 7 (inclusive) or --check-lines=5 to restrict "
|
|
"to one line. Default is testing entire file."),
|
|
llvm::cl::init("")};
|
|
llvm::cl::opt<bool> CheckLocations{
|
|
"check-locations",
|
|
llvm::cl::desc(
|
|
"Runs certain features (e.g. hover) at each point in the file. "
|
|
"Somewhat slow."),
|
|
llvm::cl::init(true)};
|
|
llvm::cl::opt<bool> CheckCompletion{
|
|
"check-completion",
|
|
llvm::cl::desc("Run code-completion at each point (slow)"),
|
|
llvm::cl::init(false)};
|
|
llvm::cl::opt<bool> CheckWarnings{
|
|
"check-warnings",
|
|
llvm::cl::desc("Print warnings as well as errors"),
|
|
llvm::cl::init(false)};
|
|
|
|
// Print the diagnostics meeting severity threshold, and return count of errors.
|
|
unsigned showErrors(llvm::ArrayRef<Diag> Diags) {
|
|
unsigned ErrCount = 0;
|
|
for (const auto &D : Diags) {
|
|
if (D.Severity >= DiagnosticsEngine::Error || CheckWarnings)
|
|
elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message);
|
|
if (D.Severity >= DiagnosticsEngine::Error)
|
|
++ErrCount;
|
|
}
|
|
return ErrCount;
|
|
}
|
|
|
|
std::vector<std::string> listTidyChecks(llvm::StringRef Glob) {
|
|
tidy::GlobList G(Glob);
|
|
tidy::ClangTidyCheckFactories CTFactories;
|
|
for (const auto &E : tidy::ClangTidyModuleRegistry::entries())
|
|
E.instantiate()->addCheckFactories(CTFactories);
|
|
std::vector<std::string> Result;
|
|
for (const auto &E : CTFactories)
|
|
if (G.contains(E.getKey()))
|
|
Result.push_back(E.getKey().str());
|
|
llvm::sort(Result);
|
|
return Result;
|
|
}
|
|
|
|
// This class is just a linear pipeline whose functions get called in sequence.
|
|
// Each exercises part of clangd's logic on our test file and logs results.
|
|
// Later steps depend on state built in earlier ones (such as the AST).
|
|
// Many steps can fatally fail (return false), then subsequent ones cannot run.
|
|
// Nonfatal failures are logged and tracked in ErrCount.
|
|
class Checker {
|
|
// from constructor
|
|
std::string File;
|
|
ClangdLSPServer::Options Opts;
|
|
// from buildCommand
|
|
tooling::CompileCommand Cmd;
|
|
std::unique_ptr<GlobalCompilationDatabase> BaseCDB;
|
|
std::unique_ptr<GlobalCompilationDatabase> CDB;
|
|
// from buildInvocation
|
|
ParseInputs Inputs;
|
|
std::unique_ptr<CompilerInvocation> Invocation;
|
|
format::FormatStyle Style;
|
|
std::optional<ModulesBuilder> ModulesManager;
|
|
// from buildAST
|
|
std::shared_ptr<const PreambleData> Preamble;
|
|
std::optional<ParsedAST> AST;
|
|
FileIndex Index;
|
|
|
|
public:
|
|
// Number of non-fatal errors seen.
|
|
unsigned ErrCount = 0;
|
|
|
|
Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts)
|
|
: File(File), Opts(Opts), Index(/*SupportContainedRefs=*/true) {}
|
|
|
|
// Read compilation database and choose a compile command for the file.
|
|
bool buildCommand(const ThreadsafeFS &TFS) {
|
|
log("Loading compilation database...");
|
|
DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
|
|
CDBOpts.CompileCommandsDir =
|
|
Config::current().CompileFlags.CDBSearch.FixedCDBPath;
|
|
BaseCDB =
|
|
std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts);
|
|
auto Mangler = CommandMangler::detect();
|
|
Mangler.SystemIncludeExtractor =
|
|
getSystemIncludeExtractor(llvm::ArrayRef(Opts.QueryDriverGlobs));
|
|
if (Opts.ResourceDir)
|
|
Mangler.ResourceDir = *Opts.ResourceDir;
|
|
CDB = std::make_unique<OverlayCDB>(
|
|
BaseCDB.get(), std::vector<std::string>{}, std::move(Mangler));
|
|
|
|
if (auto TrueCmd = CDB->getCompileCommand(File)) {
|
|
Cmd = std::move(*TrueCmd);
|
|
log("Compile command {0} is: [{1}] {2}",
|
|
Cmd.Heuristic.empty() ? "from CDB" : Cmd.Heuristic, Cmd.Directory,
|
|
printArgv(Cmd.CommandLine));
|
|
} else {
|
|
Cmd = CDB->getFallbackCommand(File);
|
|
log("Generic fallback command is: [{0}] {1}", Cmd.Directory,
|
|
printArgv(Cmd.CommandLine));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Prepare inputs and build CompilerInvocation (parsed compile command).
|
|
bool buildInvocation(const ThreadsafeFS &TFS,
|
|
std::optional<std::string> Contents) {
|
|
StoreDiags CaptureInvocationDiags;
|
|
std::vector<std::string> CC1Args;
|
|
Inputs.CompileCommand = Cmd;
|
|
Inputs.TFS = &TFS;
|
|
Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
|
|
Inputs.Opts.PreambleParseForwardingFunctions =
|
|
Opts.PreambleParseForwardingFunctions;
|
|
if (Contents) {
|
|
Inputs.Contents = *Contents;
|
|
log("Imaginary source file contents:\n{0}", Inputs.Contents);
|
|
} else {
|
|
if (auto Contents = TFS.view(std::nullopt)->getBufferForFile(File)) {
|
|
Inputs.Contents = Contents->get()->getBuffer().str();
|
|
} else {
|
|
elog("Couldn't read {0}: {1}", File, Contents.getError().message());
|
|
return false;
|
|
}
|
|
}
|
|
if (Opts.EnableExperimentalModulesSupport) {
|
|
if (!ModulesManager)
|
|
ModulesManager.emplace(*CDB);
|
|
Inputs.ModulesManager = &*ModulesManager;
|
|
}
|
|
log("Parsing command...");
|
|
Invocation =
|
|
buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args);
|
|
auto InvocationDiags = CaptureInvocationDiags.take();
|
|
ErrCount += showErrors(InvocationDiags);
|
|
log("internal (cc1) args are: {0}", printArgv(CC1Args));
|
|
if (!Invocation) {
|
|
elog("Failed to parse command line");
|
|
return false;
|
|
}
|
|
|
|
// FIXME: Check that resource-dir/built-in-headers exist?
|
|
|
|
Style = getFormatStyleForFile(File, Inputs.Contents, TFS, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Build preamble and AST, and index them.
|
|
bool buildAST() {
|
|
log("Building preamble...");
|
|
Preamble = buildPreamble(
|
|
File, *Invocation, Inputs, /*StoreInMemory=*/true,
|
|
[&](CapturedASTCtx Ctx,
|
|
std::shared_ptr<const include_cleaner::PragmaIncludes> PI) {
|
|
if (!Opts.BuildDynamicSymbolIndex)
|
|
return;
|
|
log("Indexing headers...");
|
|
Index.updatePreamble(File, /*Version=*/"null", Ctx.getASTContext(),
|
|
Ctx.getPreprocessor(), *PI);
|
|
});
|
|
if (!Preamble) {
|
|
elog("Failed to build preamble");
|
|
return false;
|
|
}
|
|
ErrCount += showErrors(Preamble->Diags);
|
|
|
|
log("Building AST...");
|
|
AST = ParsedAST::build(File, Inputs, std::move(Invocation),
|
|
/*InvocationDiags=*/std::vector<Diag>{}, Preamble);
|
|
if (!AST) {
|
|
elog("Failed to build AST");
|
|
return false;
|
|
}
|
|
ErrCount +=
|
|
showErrors(AST->getDiagnostics().drop_front(Preamble->Diags.size()));
|
|
|
|
if (Opts.BuildDynamicSymbolIndex) {
|
|
log("Indexing AST...");
|
|
Index.updateMain(File, *AST);
|
|
}
|
|
|
|
if (!CheckTidyTime.empty()) {
|
|
if (!CLANGD_TIDY_CHECKS) {
|
|
elog("-{0} requires -DCLANGD_TIDY_CHECKS!", CheckTidyTime.ArgStr);
|
|
return false;
|
|
}
|
|
#ifndef NDEBUG
|
|
elog("Timing clang-tidy checks in asserts-mode is not representative!");
|
|
#endif
|
|
checkTidyTimes();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// For each check foo, we want to build with checks=-* and checks=-*,foo.
|
|
// (We do a full build rather than just AST matchers to meausre PPCallbacks).
|
|
//
|
|
// However, performance has both random noise and systematic changes, such as
|
|
// step-function slowdowns due to CPU scaling.
|
|
// We take the median of 5 measurements, and after every check discard the
|
|
// measurement if the baseline changed by >3%.
|
|
void checkTidyTimes() {
|
|
double Stability = 0.03;
|
|
log("Timing AST build with individual clang-tidy checks (target accuracy "
|
|
"{0:P0})",
|
|
Stability);
|
|
|
|
using Duration = std::chrono::nanoseconds;
|
|
// Measure time elapsed by a block of code. Currently: user CPU time.
|
|
auto Time = [&](auto &&Run) -> Duration {
|
|
llvm::sys::TimePoint<> Elapsed;
|
|
std::chrono::nanoseconds UserBegin, UserEnd, System;
|
|
llvm::sys::Process::GetTimeUsage(Elapsed, UserBegin, System);
|
|
Run();
|
|
llvm::sys::Process::GetTimeUsage(Elapsed, UserEnd, System);
|
|
return UserEnd - UserBegin;
|
|
};
|
|
auto Change = [&](Duration Exp, Duration Base) -> double {
|
|
return (double)(Exp.count() - Base.count()) / Base.count();
|
|
};
|
|
// Build ParsedAST with a fixed check glob, and return the time taken.
|
|
auto Build = [&](llvm::StringRef Checks) -> Duration {
|
|
TidyProvider CTProvider = [&](tidy::ClangTidyOptions &Opts,
|
|
llvm::StringRef) {
|
|
Opts.Checks = Checks.str();
|
|
};
|
|
Inputs.ClangTidyProvider = CTProvider;
|
|
// Sigh, can't reuse the CompilerInvocation.
|
|
IgnoringDiagConsumer IgnoreDiags;
|
|
auto Invocation = buildCompilerInvocation(Inputs, IgnoreDiags);
|
|
Duration Val = Time([&] {
|
|
ParsedAST::build(File, Inputs, std::move(Invocation), {}, Preamble);
|
|
});
|
|
vlog(" Measured {0} ==> {1}", Checks, Val);
|
|
return Val;
|
|
};
|
|
// Measure several times, return the median.
|
|
auto MedianTime = [&](llvm::StringRef Checks) -> Duration {
|
|
std::array<Duration, 5> Measurements;
|
|
for (auto &M : Measurements)
|
|
M = Build(Checks);
|
|
llvm::sort(Measurements);
|
|
return Measurements[Measurements.size() / 2];
|
|
};
|
|
Duration Baseline = MedianTime("-*");
|
|
log(" Baseline = {0}", Baseline);
|
|
// Attempt to time a check, may update Baseline if it is unstable.
|
|
auto Measure = [&](llvm::StringRef Check) -> double {
|
|
for (;;) {
|
|
Duration Median = MedianTime(("-*," + Check).str());
|
|
Duration NewBase = MedianTime("-*");
|
|
|
|
// Value only usable if baseline is fairly consistent before/after.
|
|
double DeltaFraction = Change(NewBase, Baseline);
|
|
Baseline = NewBase;
|
|
vlog(" Baseline = {0}", Baseline);
|
|
if (DeltaFraction < -Stability || DeltaFraction > Stability) {
|
|
elog(" Speed unstable, discarding measurement.");
|
|
continue;
|
|
}
|
|
return Change(Median, Baseline);
|
|
}
|
|
};
|
|
|
|
for (const auto& Check : listTidyChecks(CheckTidyTime)) {
|
|
// vlog the check name in case we crash!
|
|
vlog(" Timing {0}", Check);
|
|
double Fraction = Measure(Check);
|
|
log(" {0} = {1:P0}", Check, Fraction);
|
|
}
|
|
log("Finished individual clang-tidy checks");
|
|
|
|
// Restore old options.
|
|
Inputs.ClangTidyProvider = Opts.ClangTidyProvider;
|
|
}
|
|
|
|
// Build Inlay Hints for the entire AST or the specified range
|
|
void buildInlayHints(std::optional<Range> LineRange) {
|
|
log("Building inlay hints");
|
|
auto Hints = inlayHints(*AST, LineRange);
|
|
|
|
for (const auto &Hint : Hints) {
|
|
vlog(" {0} {1} [{2}]", Hint.kind, Hint.position, [&] {
|
|
return llvm::join(llvm::map_range(Hint.label,
|
|
[&](auto &L) {
|
|
return llvm::formatv("{{{0}}", L);
|
|
}),
|
|
", ");
|
|
}());
|
|
}
|
|
}
|
|
|
|
void buildSemanticHighlighting(std::optional<Range> LineRange) {
|
|
log("Building semantic highlighting");
|
|
auto Highlights =
|
|
getSemanticHighlightings(*AST, /*IncludeInactiveRegionTokens=*/true);
|
|
for (const auto HL : Highlights)
|
|
if (!LineRange || LineRange->contains(HL.R))
|
|
vlog(" {0} {1} {2}", HL.R, HL.Kind, HL.Modifiers);
|
|
}
|
|
|
|
// Run AST-based features at each token in the file.
|
|
void testLocationFeatures(std::optional<Range> LineRange) {
|
|
trace::Span Trace("testLocationFeatures");
|
|
log("Testing features at each token (may be slow in large files)");
|
|
auto &SM = AST->getSourceManager();
|
|
auto SpelledTokens = AST->getTokens().spelledTokens(SM.getMainFileID());
|
|
|
|
CodeCompleteOptions CCOpts = Opts.CodeComplete;
|
|
CCOpts.Index = &Index;
|
|
|
|
for (const auto &Tok : SpelledTokens) {
|
|
unsigned Start = AST->getSourceManager().getFileOffset(Tok.location());
|
|
unsigned End = Start + Tok.length();
|
|
Position Pos = offsetToPosition(Inputs.Contents, Start);
|
|
|
|
if (LineRange && !LineRange->contains(Pos))
|
|
continue;
|
|
|
|
trace::Span Trace("Token");
|
|
SPAN_ATTACH(Trace, "pos", Pos);
|
|
SPAN_ATTACH(Trace, "text", Tok.text(AST->getSourceManager()));
|
|
|
|
// FIXME: dumping the tokens may leak sensitive code into bug reports.
|
|
// Add an option to turn this off, once we decide how options work.
|
|
vlog(" {0} {1}", Pos, Tok.text(AST->getSourceManager()));
|
|
auto Tree = SelectionTree::createRight(AST->getASTContext(),
|
|
AST->getTokens(), Start, End);
|
|
Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree),
|
|
nullptr);
|
|
// FS is only populated when applying a tweak, not during prepare as
|
|
// prepare should not do any I/O to be fast.
|
|
auto Tweaks =
|
|
prepareTweaks(Selection, Opts.TweakFilter, Opts.FeatureModules);
|
|
Selection.FS =
|
|
&AST->getSourceManager().getFileManager().getVirtualFileSystem();
|
|
for (const auto &T : Tweaks) {
|
|
auto Result = T->apply(Selection);
|
|
if (!Result) {
|
|
elog(" tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError());
|
|
++ErrCount;
|
|
} else {
|
|
vlog(" tweak: {0}", T->id());
|
|
}
|
|
}
|
|
unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size();
|
|
vlog(" definition: {0}", Definitions);
|
|
|
|
auto Hover = getHover(*AST, Pos, Style, &Index);
|
|
vlog(" hover: {0}", Hover.has_value());
|
|
|
|
unsigned DocHighlights = findDocumentHighlights(*AST, Pos).size();
|
|
vlog(" documentHighlight: {0}", DocHighlights);
|
|
|
|
if (CheckCompletion) {
|
|
Position EndPos = offsetToPosition(Inputs.Contents, End);
|
|
auto CC = codeComplete(File, EndPos, Preamble.get(), Inputs, CCOpts);
|
|
vlog(" code completion: {0}",
|
|
CC.Completions.empty() ? "<empty>" : CC.Completions[0].Name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
bool check(llvm::StringRef File, const ThreadsafeFS &TFS,
|
|
const ClangdLSPServer::Options &Opts) {
|
|
std::optional<Range> LineRange;
|
|
if (!CheckFileLines.empty()) {
|
|
uint32_t Begin = 0, End = std::numeric_limits<uint32_t>::max();
|
|
StringRef RangeStr(CheckFileLines);
|
|
bool ParseError = RangeStr.consumeInteger(0, Begin);
|
|
if (RangeStr.empty()) {
|
|
End = Begin;
|
|
} else {
|
|
ParseError |= !RangeStr.consume_front("-");
|
|
ParseError |= RangeStr.consumeInteger(0, End);
|
|
}
|
|
if (ParseError || !RangeStr.empty() || Begin <= 0 || End < Begin) {
|
|
elog("Invalid --check-lines specified. Use Begin-End format, e.g. 3-17");
|
|
return false;
|
|
}
|
|
LineRange = Range{Position{static_cast<int>(Begin - 1), 0},
|
|
Position{static_cast<int>(End), 0}};
|
|
}
|
|
|
|
llvm::SmallString<0> FakeFile;
|
|
std::optional<std::string> Contents;
|
|
if (File.empty()) {
|
|
llvm::sys::path::system_temp_directory(false, FakeFile);
|
|
llvm::sys::path::append(FakeFile, "test.cc");
|
|
File = FakeFile;
|
|
Contents = R"cpp(
|
|
#include <stddef.h>
|
|
#include <string>
|
|
|
|
size_t N = 50;
|
|
auto xxx = std::string(N, 'x');
|
|
)cpp";
|
|
}
|
|
log("Testing on source file {0}", File);
|
|
|
|
class OverrideConfigProvider : public config::Provider {
|
|
std::vector<config::CompiledFragment>
|
|
getFragments(const config::Params &,
|
|
config::DiagnosticCallback Diag) const override {
|
|
config::Fragment F;
|
|
// If we're timing clang-tidy checks, implicitly disabling the slow ones
|
|
// is counterproductive!
|
|
if (CheckTidyTime.getNumOccurrences())
|
|
F.Diagnostics.ClangTidy.FastCheckFilter.emplace("None");
|
|
return {std::move(F).compile(Diag)};
|
|
}
|
|
} OverrideConfig;
|
|
auto ConfigProvider =
|
|
config::Provider::combine({Opts.ConfigProvider, &OverrideConfig});
|
|
|
|
auto ContextProvider = ClangdServer::createConfiguredContextProvider(
|
|
ConfigProvider.get(), nullptr);
|
|
WithContext Ctx(ContextProvider(
|
|
FakeFile.empty()
|
|
? File
|
|
: /*Don't turn on local configs for an arbitrary temp path.*/ ""));
|
|
Checker C(File, Opts);
|
|
if (!C.buildCommand(TFS) || !C.buildInvocation(TFS, Contents) ||
|
|
!C.buildAST())
|
|
return false;
|
|
C.buildInlayHints(LineRange);
|
|
C.buildSemanticHighlighting(LineRange);
|
|
if (CheckLocations)
|
|
C.testLocationFeatures(LineRange);
|
|
|
|
log("All checks completed, {0} errors", C.ErrCount);
|
|
return C.ErrCount == 0;
|
|
}
|
|
|
|
} // namespace clangd
|
|
} // namespace clang
|