diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -77,6 +77,7 @@ SemanticSelection.cpp SourceCode.cpp QueryDriverDatabase.cpp + TidyProvider.cpp TUScheduler.cpp URI.cpp XRefs.cpp diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -41,13 +41,7 @@ namespace clang { namespace clangd { - -/// When set, used by ClangdServer to get clang-tidy options for each particular -/// file. Must be thread-safe. We use this instead of ClangTidyOptionsProvider -/// to allow reading tidy configs from the VFS used for parsing. -using ClangTidyOptionsBuilder = std::function; - +class ClangdTidyProvider; /// Manages a collection of source files and derived data (ASTs, indexes), /// and provides language-aware features such as code completion. /// @@ -121,12 +115,9 @@ /// If set, queried to obtain the configuration to handle each request. config::Provider *ConfigProvider = nullptr; - /// If set, enable clang-tidy in clangd and use to it get clang-tidy - /// configurations for a particular file. - /// Clangd supports only a small subset of ClangTidyOptions, these options - /// (Checks, CheckOptions) are about which clang-tidy checks will be - /// enabled. - ClangTidyOptionsBuilder GetClangTidyOptions; + /// The Options provider to use when running clang-tidy. If null, clang-tidy + /// checks will be disabled. + ClangdTidyProvider *ClangTidyProvider = nullptr; /// If true, force -frecovery-ast flag. /// If false, respect the value in clang. @@ -373,8 +364,8 @@ // Storage for merged views of the various indexes. std::vector> MergedIdx; - // When set, provides clang-tidy options for a specific file. - ClangTidyOptionsBuilder GetClangTidyOptions; + // OptionsPrivder for clang-tidy. + ClangdTidyProvider *ClangTidyProvider = nullptr; // If this is true, suggest include insertion fixes for diagnostic errors that // can be caused by missing includes (e.g. member access in incomplete type). diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -113,40 +113,6 @@ bool TheiaSemanticHighlighting; }; -// Set of clang-tidy checks that don't work well in clangd, either due to -// crashes or false positives. -// Returns a clang-tidy filter string: -check1,-check2. -llvm::StringRef getUnusableTidyChecks() { - static const std::string FalsePositives = - llvm::join_items(", ", - // Check relies on seeing ifndef/define/endif directives, - // clangd doesn't replay those when using a preamble. - "-llvm-header-guard"); - static const std::string CrashingChecks = - llvm::join_items(", ", - // Check can choke on invalid (intermediate) c++ code, - // which is often the case when clangd tries to build an - // AST. - "-bugprone-use-after-move"); - static const std::string UnusableTidyChecks = - llvm::join_items(", ", FalsePositives, CrashingChecks); - return UnusableTidyChecks; -} - -// Returns a clang-tidy options string: check1,check2. -llvm::StringRef getDefaultTidyChecks() { - // These default checks are chosen for: - // - low false-positive rate - // - providing a lot of value - // - being reasonably efficient - static const std::string DefaultChecks = llvm::join_items( - ",", "readability-misleading-indentation", "readability-deleted-default", - "bugprone-integer-division", "bugprone-sizeof-expression", - "bugprone-suspicious-missing-comma", "bugprone-unused-raii", - "bugprone-unused-return-value", "misc-unused-using-decls", - "misc-unused-alias-decls", "misc-definitions-in-headers"); - return DefaultChecks; -} } // namespace ClangdServer::Options ClangdServer::optsForTest() { @@ -177,7 +143,7 @@ ? new FileIndex(Opts.HeavyweightDynamicSymbolIndex, Opts.CollectMainFileRefs) : nullptr), - GetClangTidyOptions(Opts.GetClangTidyOptions), + ClangTidyProvider(Opts.ClangTidyProvider), SuggestMissingIncludes(Opts.SuggestMissingIncludes), BuildRecoveryAST(Opts.BuildRecoveryAST), PreserveRecoveryASTType(Opts.PreserveRecoveryASTType), @@ -235,20 +201,6 @@ llvm::StringRef Version, WantDiagnostics WantDiags, bool ForceRebuild) { ParseOptions Opts; - Opts.ClangTidyOpts = tidy::ClangTidyOptions::getDefaults(); - // FIXME: call tidy options builder on the worker thread, it can do IO. - if (GetClangTidyOptions) - Opts.ClangTidyOpts = - GetClangTidyOptions(*TFS.view(/*CWD=*/llvm::None), File); - if (Opts.ClangTidyOpts.Checks.hasValue()) { - // If the set of checks was configured, make sure clangd incompatible ones - // are disabled. - Opts.ClangTidyOpts.Checks = llvm::join_items( - ", ", *Opts.ClangTidyOpts.Checks, getUnusableTidyChecks()); - } else { - // Otherwise provide a nice set of defaults. - Opts.ClangTidyOpts.Checks = getDefaultTidyChecks().str(); - } Opts.SuggestMissingIncludes = SuggestMissingIncludes; // Compile command is set asynchronously during update, as it can be slow. @@ -259,6 +211,7 @@ Inputs.ForceRebuild = ForceRebuild; Inputs.Opts = std::move(Opts); Inputs.Index = Index; + Inputs.ClangTidyProvider = ClangTidyProvider; Inputs.Opts.BuildRecoveryAST = BuildRecoveryAST; Inputs.Opts.PreserveRecoveryASTType = PreserveRecoveryASTType; bool NewFile = WorkScheduler.update(File, Inputs, WantDiags); diff --git a/clang-tools-extra/clangd/Compiler.h b/clang-tools-extra/clangd/Compiler.h --- a/clang-tools-extra/clangd/Compiler.h +++ b/clang-tools-extra/clangd/Compiler.h @@ -15,7 +15,6 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_COMPILER_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_COMPILER_H -#include "../clang-tidy/ClangTidyOptions.h" #include "GlobalCompilationDatabase.h" #include "index/Index.h" #include "support/ThreadsafeFS.h" @@ -26,6 +25,8 @@ namespace clang { namespace clangd { +class ClangdTidyProvider; + class IgnoreDiagnostics : public DiagnosticConsumer { public: static void log(DiagnosticsEngine::Level DiagLevel, @@ -37,7 +38,6 @@ // Options to run clang e.g. when parsing AST. struct ParseOptions { - tidy::ClangTidyOptions ClangTidyOpts; bool SuggestMissingIncludes = false; bool BuildRecoveryAST = false; bool PreserveRecoveryASTType = false; @@ -56,6 +56,7 @@ // Used to recover from diagnostics (e.g. find missing includes for symbol). const SymbolIndex *Index = nullptr; ParseOptions Opts = ParseOptions(); + ClangdTidyProvider *ClangTidyProvider = nullptr; }; /// Builds compiler invocation that could be used to build AST or preamble. diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp --- a/clang-tools-extra/clangd/ParsedAST.cpp +++ b/clang-tools-extra/clangd/ParsedAST.cpp @@ -17,6 +17,7 @@ #include "IncludeFixer.h" #include "Preamble.h" #include "SourceCode.h" +#include "TidyProvider.h" #include "index/CanonicalIncludes.h" #include "index/Index.h" #include "support/Logger.h" @@ -246,7 +247,7 @@ auto VFS = Inputs.TFS->view(Inputs.CompileCommand.Directory); if (Preamble && Preamble->StatCache) - VFS = Preamble->StatCache->getConsumingFS(std::move(VFS)); + VFS = Preamble->StatCache->getConsumingFS(VFS.get()); assert(CI); // Command-line parsing sets DisableFree to true by default, but we don't want @@ -292,16 +293,22 @@ llvm::Optional CTContext; { trace::Span Tracer("ClangTidyInit"); - dlog("ClangTidy configuration for file {0}: {1}", Filename, - tidy::configurationAsText(Inputs.Opts.ClangTidyOpts)); tidy::ClangTidyCheckFactories CTFactories; for (const auto &E : tidy::ClangTidyModuleRegistry::entries()) E.instantiate()->addCheckFactories(CTFactories); - CTContext.emplace(std::make_unique( - tidy::ClangTidyGlobalOptions(), Inputs.Opts.ClangTidyOpts)); + if (Inputs.ClangTidyProvider) { + CTContext.emplace(Inputs.ClangTidyProvider->getInstance(VFS.get())); + } else { + // This OptionsProvider will have no checks enabled, essentially disabling + // clang-tidy. + CTContext.emplace(std::make_unique( + tidy::ClangTidyGlobalOptions(), tidy::ClangTidyOptions())); + } CTContext->setDiagnosticsEngine(&Clang->getDiagnostics()); CTContext->setASTContext(&Clang->getASTContext()); CTContext->setCurrentFile(Filename); + dlog("ClangTidy configuration for file {0}: {1}", Filename, + tidy::configurationAsText(CTContext->getOptions())); CTChecks = CTFactories.createChecks(CTContext.getPointer()); llvm::erase_if(CTChecks, [&](const auto &Check) { return !Check->isLanguageVersionSupported(CTContext->getLangOpts()); diff --git a/clang-tools-extra/clangd/TidyProvider.h b/clang-tools-extra/clangd/TidyProvider.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/TidyProvider.h @@ -0,0 +1,118 @@ +//===--- TidyProvider.h - create options for running clang-tidy------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_TIDYPROVIDER_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_TIDYPROVIDER_H + +#include "../clang-tidy/ClangTidyOptions.h" +#include "llvm/Support/VirtualFileSystem.h" +#include + +namespace clang { +namespace clangd { + +/// The base class of all clang-tidy config providers for clangd. The option +/// getters take a pointer to a Filesystem that should be used for any file IO. +class ClangdTidyProvider { +public: + using OptionsSource = tidy::ClangTidyOptionsProvider::OptionsSource; + + /// Returns a provider than can be passed directly to the constructor of \ref + /// ClangTidyContext which delegates its option sourcing to this class. + virtual std::unique_ptr + getInstance(llvm::vfs::FileSystem *FS); + + virtual const tidy::ClangTidyGlobalOptions & + getGlobalOptions(llvm::vfs::FileSystem *) = 0; + + virtual std::vector + getRawOptions(llvm::vfs::FileSystem *FS, llvm::StringRef FileName) = 0; + + virtual ~ClangdTidyProvider() = default; +}; + +/// A Provider that will mimic the behaviour of tidy::FileOptionsProvider but +/// it caches options retrieved in a thread safe manner. To be completely thread +/// safe it must be passed a thread safe filesystem when reading files. +class FileTidyProvider : public ClangdTidyProvider { +public: + FileTidyProvider(tidy::ClangTidyGlobalOptions GlobalOptions, + tidy::ClangTidyOptions DefaultOptions, + tidy::ClangTidyOptions OverrideOptions) + : GlobalOpts(std::move(GlobalOptions)), + DefaultOpts(std::move(DefaultOptions)), + OverrideOpts(std::move(OverrideOptions)) {} + const tidy::ClangTidyGlobalOptions & + getGlobalOptions(llvm::vfs::FileSystem * /*unused*/) override; + + std::vector getRawOptions(llvm::vfs::FileSystem *FS, + llvm::StringRef FileName) override; + +protected: + /// Loads .clang-tidy config files into \p CurOptions from the filesystem \p + /// FS found in parent directories of \p FileName. + void addFileOptions(llvm::vfs::FileSystem *FS, llvm::StringRef FileName, + std::vector &CurOptions); + +private: + /// Tries to read and parse a configuration file located in \p Directory. + llvm::Optional tryReadConfigFile(llvm::vfs::FileSystem *FS, + llvm::StringRef Directory); + + /// Trys to get a OptionsSource from \p Directory, returns the OptionsSource + /// and a flag indicating if the value was read from the cache. + llvm::Optional> + tryGetConfig(llvm::vfs::FileSystem *FS, llvm::StringRef Directory); + + // Using a shared mutex lets different threads read the cache at the same + // time, while preventing any thread updating the cache. + std::shared_timed_mutex CacheGuard; + llvm::StringMap CachedOptions; + + const tidy::ClangTidyGlobalOptions GlobalOpts; + const tidy::ClangTidyOptions DefaultOpts; + const tidy::ClangTidyOptions OverrideOpts; +}; + +/// A FileTidyProvider that will adjust its checks for clangd. +/// Either by assigning a nice default set of checks if none are specified or +/// removing checks known to cause issues in clang-tidy. +class CheckAdjustingTidyProvider : public FileTidyProvider { +public: + using FileTidyProvider::FileTidyProvider; + std::vector getRawOptions(llvm::vfs::FileSystem *FS, + llvm::StringRef FileName) override; + +protected: + /// Appends an OptionsSource that will set default checks if no checks exists, + /// or disable checks known to cause issue when ran in clangd. + void adjustChecks(std::vector &RawOptions); +}; + +/// A ClangdTidyProvider that will use clangd::config when building its options. +class ConfigTidyProvider : public CheckAdjustingTidyProvider { +public: + using CheckAdjustingTidyProvider::CheckAdjustingTidyProvider; + + std::vector getRawOptions(llvm::vfs::FileSystem *FS, + llvm::StringRef FileName) override; +}; + +// Set of clang-tidy checks that don't work well in clangd, either due to +// crashes or false positives. +// Returns a clang-tidy filter string: -check1,-check2. +llvm::StringRef getUnusableTidyChecks(); + +// Set of nice checks to enable by default if no checks are specified. +// Returns a clang-tidy options string: check1,check2. +llvm::StringRef getDefaultTidyChecks(); + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_TIDYPROVIDER_H diff --git a/clang-tools-extra/clangd/TidyProvider.cpp b/clang-tools-extra/clangd/TidyProvider.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/TidyProvider.cpp @@ -0,0 +1,292 @@ +//===--- TidyProvider.cpp - create options for running clang-tidy----------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "TidyProvider.h" +#include "Config.h" +#include "clang/Basic/LLVM.h" +#include "llvm/Support/VirtualFileSystem.h" + +namespace clang { +namespace clangd { + +namespace { + +class ProxyFSTidyOptionsProvider : public tidy::ClangTidyOptionsProvider { +public: + ProxyFSTidyOptionsProvider(ClangdTidyProvider &Provider, + llvm::vfs::FileSystem *FS) + : Provider(Provider), TFSView(FS) {} + + const tidy::ClangTidyGlobalOptions &getGlobalOptions() override { + return Provider.getGlobalOptions(TFSView.get()); + } + std::vector getRawOptions(llvm::StringRef FileName) override { + return Provider.getRawOptions(TFSView.get(), FileName); + } + +private: + ClangdTidyProvider &Provider; + llvm::IntrusiveRefCntPtr TFSView; +}; + +} // namespace + +StringRef getUnusableTidyChecks() { + static const std::string FalsePositives = + llvm::join_items(", ", + // Check relies on seeing ifndef/define/endif directives, + // clangd doesn't replay those when using a preamble. + "-llvm-header-guard"); + static const std::string CrashingChecks = + llvm::join_items(", ", + // Check can choke on invalid (intermediate) c++ code, + // which is often the case when clangd tries to build an + // AST. + "-bugprone-use-after-move"); + static const std::string UnusableTidyChecks = + llvm::join_items(", ", FalsePositives, CrashingChecks); + return UnusableTidyChecks; +} + +StringRef getDefaultTidyChecks() { + // These default checks are chosen for: + // - low false-positive rate + // - providing a lot of value + // - being reasonably efficient + static const std::string DefaultChecks = llvm::join_items( + ",", "readability-misleading-indentation", "readability-deleted-default", + "bugprone-integer-division", "bugprone-sizeof-expression", + "bugprone-suspicious-missing-comma", "bugprone-unused-raii", + "bugprone-unused-return-value", "misc-unused-using-decls", + "misc-unused-alias-decls", "misc-definitions-in-headers"); + return DefaultChecks; +} + +std::unique_ptr +ClangdTidyProvider::getInstance(llvm::vfs::FileSystem *FS) { + return std::make_unique(*this, FS); +} + +const tidy::ClangTidyGlobalOptions & +FileTidyProvider::getGlobalOptions(llvm::vfs::FileSystem * /*unused*/) { + return GlobalOpts; +} + +std::vector +FileTidyProvider::getRawOptions(llvm::vfs::FileSystem *FS, + llvm::StringRef FileName) { + std::vector RawOptions; + RawOptions.emplace_back(this->DefaultOpts, "clangd: default options"); + addFileOptions(FS, FileName, RawOptions); + + RawOptions.emplace_back(OverrideOpts, "command-line: -clang-tidy-checks"); + + return RawOptions; +} + +void FileTidyProvider::addFileOptions(llvm::vfs::FileSystem *FS, + StringRef FileName, + std::vector &CurOptions) { + + llvm::SmallString<128> AbsolutePath(FileName); + + if (FS->makeAbsolute(AbsolutePath)) + return; + + auto CurSize = CurOptions.size(); + + // Look for a suitable configuration file in all parent directories of the + // file. Start with the immediate parent directory and move up. + StringRef Path = llvm::sys::path::parent_path(AbsolutePath); + for (StringRef CurrentPath = Path; !CurrentPath.empty(); + CurrentPath = llvm::sys::path::parent_path(CurrentPath)) { + auto SourceAndFromCache = tryGetConfig(FS, CurrentPath); + + if (!SourceAndFromCache) + continue; // No config found for this directory + + OptionsSource &OptionsAndSource = SourceAndFromCache->first; + + if (!SourceAndFromCache->second) { + // Require exclusive access while we update the CachedOptions. + std::unique_lock ExclusiveGuard(CacheGuard); + CachedOptions.insert(std::make_pair(Path, OptionsAndSource)); + } + + if (Path != CurrentPath) { + // Defer locking until we are sure we need to update the cache. + std::unique_lock ExclusiveGuard( + CacheGuard, std::defer_lock_t{}); + + // Acquire this lock now as it's needed to read the cache. + // If we ever need to update the cache, this will be released. + std::shared_lock SharedGuard(CacheGuard); + + // Store cached value for all intermediate directories. + while (Path != CurrentPath) { + if (CachedOptions.count(Path) == 0) { + // Release the shared access and gain exclusive access if we don't + // already have it. + if (!ExclusiveGuard.owns_lock()) { + assert(SharedGuard.owns_lock()); + SharedGuard.unlock(); + ExclusiveGuard.lock(); + } + CachedOptions.insert(std::make_pair(Path, OptionsAndSource)); + } + Path = llvm::sys::path::parent_path(Path); + } + } + + CurOptions.push_back(std::move(OptionsAndSource)); + + if (!CurOptions.back().first.InheritParentConfig.getValueOr(false)) + break; + // We need to inherit from the directory where the options were found. + // dir1/ + // .clang-tidy + // dir2/ + // .clang-tidy + // dir3/ + // dir4/ + // + // If search at dir4/FileName and dir2/.clang-tidy is in the cache, the + // cache will also contain an entry for dir4 that is a copy of dir2. + // Likewise so will dir3. If we don't adjust the search path we'll look for + // the parent config in dir3 which will be a copy of whats in dir4. Where as + // if we adjust the search path to start from dir2 parent we'll search in + // dir1 and find the config we actually need. + AbsolutePath.assign(CurOptions.back().second); + if (FS->makeAbsolute(AbsolutePath)) + break; + Path = llvm::sys::path::parent_path(AbsolutePath); + CurrentPath = Path; + } + // Reverse order of file configs because closer configs should have higher + // priority. + std::reverse(CurOptions.begin() + CurSize, CurOptions.end()); +} + +llvm::Optional> +FileTidyProvider::tryGetConfig(llvm::vfs::FileSystem *FS, + llvm::StringRef Directory) { + { + // Only require shared control when reading. This lets multiple threads + // query the cache at the same time so long as no one is updating it. + std::shared_lock Guard(CacheGuard); + auto Iter = CachedOptions.find(Directory); + if (Iter != CachedOptions.end()) + return std::make_pair(Iter->getValue(), true); + } + + if (auto Res = tryReadConfigFile(FS, Directory)) + return std::make_pair(std::move(*Res), false); + return llvm::None; +} + +llvm::Optional +FileTidyProvider::tryReadConfigFile(llvm::vfs::FileSystem *FS, + llvm::StringRef Directory) { + assert(!Directory.empty()); + + llvm::ErrorOr DirectoryStatus = FS->status(Directory); + + if (!DirectoryStatus || !DirectoryStatus->isDirectory()) { + llvm::errs() << "Error reading configuration from " << Directory + << ": directory doesn't exist.\n"; + return llvm::None; + } + + SmallString<128> ConfigFile(Directory); + llvm::sys::path::append(ConfigFile, ".clang-tidy"); + + llvm::ErrorOr FileStatus = FS->status(ConfigFile); + + if (!FileStatus || !FileStatus->isRegularFile()) + return llvm::None; + + llvm::ErrorOr> Text = + FS->getBufferForFile(ConfigFile); + if (std::error_code EC = Text.getError()) { + llvm::errs() << "Can't read " << ConfigFile << ": " << EC.message() << "\n"; + return llvm::None; + } + + // Skip empty files, e.g. files opened for writing via shell output + // redirection. + if ((*Text)->getBuffer().empty()) + return llvm::None; + llvm::ErrorOr ParsedOptions = + tidy::parseConfiguration((*Text)->getBuffer()); + if (!ParsedOptions) { + if (ParsedOptions.getError()) + llvm::errs() << "Error parsing " << ConfigFile << ": " + << ParsedOptions.getError().message() << "\n"; + return llvm::None; + } + return OptionsSource(std::move(*ParsedOptions), std::string(ConfigFile)); +} + +std::vector +CheckAdjustingTidyProvider::getRawOptions(llvm::vfs::FileSystem *FS, + llvm::StringRef FileName) { + std::vector Opts = + FileTidyProvider::getRawOptions(FS, FileName); + adjustChecks(Opts); + return Opts; +} + +void CheckAdjustingTidyProvider::adjustChecks( + std::vector &RawOptions) { + if (llvm::all_of(RawOptions, [](OptionsSource &Source) { + return !Source.first.Checks.hasValue() || Source.first.Checks->empty(); + })) { + tidy::ClangTidyOptions DefaultChecks; + DefaultChecks.Checks.emplace(getDefaultTidyChecks()); + RawOptions.emplace_back(DefaultChecks, "clangd: default checks"); + } else { + tidy::ClangTidyOptions RemoveIncompatiableChecks; + RemoveIncompatiableChecks.Checks.emplace(getUnusableTidyChecks()); + RawOptions.emplace_back(RemoveIncompatiableChecks, + "clangd: incompatible checks"); + } +} + +std::vector +ConfigTidyProvider::getRawOptions(llvm::vfs::FileSystem *FS, + llvm::StringRef FileName) { + + const auto &CurTidyConfig = Config::current().ClangTidy; + + // If the config doesn't contain any clang-tidy options, fall back to the + // CheckAdjustingTidyProvider. + if (CurTidyConfig.Checks.empty() && CurTidyConfig.CheckOptions.empty()) + return CheckAdjustingTidyProvider::getRawOptions(FS, FileName); + + std::vector OptionSources = + FileTidyProvider::getRawOptions(FS, FileName); + + tidy::ClangTidyOptions ConfigOpts; + + if (!CurTidyConfig.Checks.empty()) + ConfigOpts.Checks.emplace(CurTidyConfig.Checks); + + for (const auto &CheckOption : CurTidyConfig.CheckOptions) + ConfigOpts.CheckOptions.insert_or_assign( + CheckOption.getKey(), + tidy::ClangTidyOptions::ClangTidyValue(CheckOption.getValue())); + + OptionSources.insert(OptionSources.end() - 1, + OptionsSource(ConfigOpts, "clangd-config")); + + adjustChecks(OptionSources); + return OptionSources; +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/tool/Check.cpp b/clang-tools-extra/clangd/tool/Check.cpp --- a/clang-tools-extra/clangd/tool/Check.cpp +++ b/clang-tools-extra/clangd/tool/Check.cpp @@ -133,8 +133,6 @@ return false; } } - Inputs.Opts.ClangTidyOpts = - Opts.GetClangTidyOptions(*TFS.view(llvm::None), File); log("Parsing command..."); Invocation = buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args); diff --git a/clang-tools-extra/clangd/tool/ClangdMain.cpp b/clang-tools-extra/clangd/tool/ClangdMain.cpp --- a/clang-tools-extra/clangd/tool/ClangdMain.cpp +++ b/clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -11,6 +11,7 @@ #include "Features.inc" #include "PathMapping.h" #include "Protocol.h" +#include "TidyProvider.h" #include "Transport.h" #include "index/Background.h" #include "index/Serialization.h" @@ -803,9 +804,7 @@ } // Create an empty clang-tidy option. - std::mutex ClangTidyOptMu; - std::unique_ptr - ClangTidyOptProvider; /*GUARDED_BY(ClangTidyOptMu)*/ + std::unique_ptr ClangTidyOptProvider; if (EnableClangTidy) { auto EmptyDefaults = tidy::ClangTidyOptions::getDefaults(); EmptyDefaults.Checks.reset(); // So we can tell if checks were ever set. @@ -817,21 +816,15 @@ tidy::ClangTidyOptions OverrideClangTidyOptions; if (!ClangTidyChecks.empty()) OverrideClangTidyOptions.Checks = ClangTidyChecks; - ClangTidyOptProvider = std::make_unique( - tidy::ClangTidyGlobalOptions(), - /* Default */ EmptyDefaults, - /* Override */ OverrideClangTidyOptions, TFS.view(/*CWD=*/llvm::None)); - Opts.GetClangTidyOptions = [&](llvm::vfs::FileSystem &, - llvm::StringRef File) { - // This function must be thread-safe and tidy option providers are not. - tidy::ClangTidyOptions Opts; - { - std::lock_guard Lock(ClangTidyOptMu); - // FIXME: use the FS provided to the function. - Opts = ClangTidyOptProvider->getOptions(File); - } - return Opts; - }; + if (EnableConfig) + ClangTidyOptProvider = std::make_unique( + tidy::ClangTidyGlobalOptions(), std::move(EmptyDefaults), + std::move(OverrideClangTidyOptions)); + else + ClangTidyOptProvider = std::make_unique( + tidy::ClangTidyGlobalOptions(), std::move(EmptyDefaults), + std::move(OverrideClangTidyOptions)); + Opts.ClangTidyProvider = ClangTidyOptProvider.get(); } Opts.AsyncPreambleBuilds = AsyncPreamble; Opts.SuggestMissingIncludes = SuggestMissingIncludes; diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -87,6 +87,7 @@ TUSchedulerTests.cpp TestFS.cpp TestIndex.cpp + TestTidyProvider.cpp TestTU.cpp TestWorkspace.cpp TypeHierarchyTests.cpp diff --git a/clang-tools-extra/clangd/unittests/ClangdTests.cpp b/clang-tools-extra/clangd/unittests/ClangdTests.cpp --- a/clang-tools-extra/clangd/unittests/ClangdTests.cpp +++ b/clang-tools-extra/clangd/unittests/ClangdTests.cpp @@ -16,6 +16,7 @@ #include "SyncAPI.h" #include "TestFS.h" #include "TestTU.h" +#include "TidyProvider.h" #include "URI.h" #include "support/MemoryTree.h" #include "support/Path.h" @@ -1214,17 +1215,20 @@ bool HadDiagsInLastCallback = false; } DiagConsumer; + // We need to use the actual options provider here as we're interested in + // making sure those providers correctly disable problematic checks. + // As an added bonus it demonstrates the provider is capable of handling the + // ThreadsafeFS. + MockFS FS; + FS.Files[testPath(".clang-tidy")] = R"( + Checks: -*,bugprone-use-after-move,llvm-header-guard + )"; MockCompilationDatabase CDB; + CheckAdjustingTidyProvider Provider({}, {}, {}); CDB.ExtraClangFlags = {"-xc++"}; auto Opts = ClangdServer::optsForTest(); - Opts.GetClangTidyOptions = [](llvm::vfs::FileSystem &, llvm::StringRef) { - auto Opts = tidy::ClangTidyOptions::getDefaults(); - // These checks don't work well in clangd, even if configured they shouldn't - // run. - Opts.Checks = "bugprone-use-after-move,llvm-header-guard"; - return Opts; - }; + Opts.ClangTidyProvider = &Provider; ClangdServer Server(CDB, FS, Opts, &DiagConsumer); const char *SourceContents = R"cpp( struct Foo { Foo(); Foo(Foo&); Foo(Foo&&); }; diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp --- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp +++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp @@ -14,6 +14,7 @@ #include "TestFS.h" #include "TestIndex.h" #include "TestTU.h" +#include "TestTidyProvider.h" #include "index/MemIndex.h" #include "support/Path.h" #include "clang/Basic/Diagnostic.h" @@ -129,7 +130,8 @@ } )cpp"); auto TU = TestTU::withCode(Test.code()); - TU.ClangTidyChecks = "-*,google-explicit-constructor"; + TU.ClangTidyProvider = + std::make_unique("-*,google-explicit-constructor"); EXPECT_THAT( TU.build().getDiagnostics(), ElementsAre( @@ -201,8 +203,9 @@ auto TU = TestTU::withCode(Test.code()); // Enable alias clang-tidy checks, these check emits the same diagnostics // (except the check name). - TU.ClangTidyChecks = "-*, readability-uppercase-literal-suffix, " - "hicpp-uppercase-literal-suffix"; + TU.ClangTidyProvider = std::make_unique( + "-*, readability-uppercase-literal-suffix, " + "hicpp-uppercase-literal-suffix"); // Verify that we filter out the duplicated diagnostic message. EXPECT_THAT( TU.build().getDiagnostics(), @@ -245,9 +248,9 @@ )cpp"); auto TU = TestTU::withCode(Test.code()); TU.HeaderFilename = "assert.h"; // Suppress "not found" error. - TU.ClangTidyChecks = + TU.ClangTidyProvider = std::make_unique( "-*, bugprone-sizeof-expression, bugprone-macro-repeated-side-effects, " - "modernize-deprecated-headers, modernize-use-trailing-return-type"; + "modernize-deprecated-headers, modernize-use-trailing-return-type"); EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre( @@ -289,7 +292,8 @@ auto TU = TestTU::withCode(Test.code()); TU.ExtraArgs = {"-isystem."}; TU.AdditionalFiles["a.h"] = TU.AdditionalFiles["b.h"] = ""; - TU.ClangTidyChecks = "-*, llvm-include-order"; + TU.ClangTidyProvider = + std::make_unique("-*, llvm-include-order"); EXPECT_THAT( TU.build().getDiagnostics(), Contains(AllOf(Diag(Test.range(), "#includes are not sorted properly"), @@ -361,7 +365,8 @@ } )cpp"); TestTU TU = TestTU::withCode(Main.code()); - TU.ClangTidyChecks = "modernize-loop-convert"; + TU.ClangTidyProvider = + std::make_unique("modernize-loop-convert"); EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( @@ -384,7 +389,8 @@ } )cpp"); TestTU TU = TestTU::withCode(Main.code()); - TU.ClangTidyChecks = "bugprone-integer-division"; + TU.ClangTidyProvider = + std::make_unique("bugprone-integer-division"); EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( @@ -401,8 +407,8 @@ } )cpp"); TestTU TU = TestTU::withCode(Main.code()); - TU.ClangTidyChecks = "bugprone-integer-division"; - TU.ClangTidyWarningsAsErrors = "bugprone-integer-division"; + TU.ClangTidyProvider = std::make_unique( + "bugprone-integer-division", "bugprone-integer-division"); EXPECT_THAT( TU.build().getDiagnostics(), UnorderedElementsAre(::testing::AllOf( @@ -452,8 +458,8 @@ } )cpp"); TestTU TU = TestTU::withCode(Main.code()); - TU.ClangTidyChecks = "bugprone-integer-division"; - TU.ClangTidyWarningsAsErrors = "bugprone-integer-division"; + TU.ClangTidyProvider = std::make_unique( + "bugprone-integer-division", "bugprone-integer-division"); EXPECT_THAT(TU.build().getDiagnostics(), UnorderedElementsAre()); } @@ -468,7 +474,8 @@ } )cpp"); TestTU TU = TestTU::withCode(Main.code()); - TU.ClangTidyChecks = "-*,bugprone-bad-signal-to-kill-thread"; + TU.ClangTidyProvider = std::make_unique( + "-*,bugprone-bad-signal-to-kill-thread"); EXPECT_THAT(TU.build().getDiagnostics(), UnorderedElementsAre()); // no-crash } diff --git a/clang-tools-extra/clangd/unittests/ParsedASTTests.cpp b/clang-tools-extra/clangd/unittests/ParsedASTTests.cpp --- a/clang-tools-extra/clangd/unittests/ParsedASTTests.cpp +++ b/clang-tools-extra/clangd/unittests/ParsedASTTests.cpp @@ -24,6 +24,7 @@ #include "SourceCode.h" #include "TestFS.h" #include "TestTU.h" +#include "TestTidyProvider.h" #include "clang/AST/DeclTemplate.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" @@ -37,6 +38,7 @@ #include "gmock/gmock-matchers.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include namespace clang { namespace clangd { @@ -250,7 +252,8 @@ TestTU TU; // this check runs the preprocessor, we need to make sure it does not break // our recording logic. - TU.ClangTidyChecks = "modernize-use-trailing-return-type"; + TU.ClangTidyProvider = std::make_unique( + "modernize-use-trailing-return-type"); TU.Code = "inline int foo() {}"; auto AST = TU.build(); @@ -406,7 +409,8 @@ "replay-preamble-module", ""); TestTU TU; // This check records inclusion directives replayed by clangd. - TU.ClangTidyChecks = "replay-preamble-check"; + TU.ClangTidyProvider = + std::make_unique("replay-preamble-check"); llvm::Annotations Test(R"cpp( $hash^#$include[[import]] $filebegin^"$filerange[[bar.h]]" $hash^#$include[[include_next]] $filebegin^"$filerange[[baz.h]]" diff --git a/clang-tools-extra/clangd/unittests/TestTU.h b/clang-tools-extra/clangd/unittests/TestTU.h --- a/clang-tools-extra/clangd/unittests/TestTU.h +++ b/clang-tools-extra/clangd/unittests/TestTU.h @@ -17,6 +17,7 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTTU_H #define LLVM_CLANG_TOOLS_EXTRA_UNITTESTS_CLANGD_TESTTU_H +#include "../TidyProvider.h" #include "Compiler.h" #include "ParsedAST.h" #include "TestFS.h" @@ -58,8 +59,7 @@ // Extra arguments for the compiler invocation. std::vector ExtraArgs; - llvm::Optional ClangTidyChecks; - llvm::Optional ClangTidyWarningsAsErrors; + std::unique_ptr ClangTidyProvider; // Index to use when building AST. const SymbolIndex *ExternalIndex = nullptr; diff --git a/clang-tools-extra/clangd/unittests/TestTU.cpp b/clang-tools-extra/clangd/unittests/TestTU.cpp --- a/clang-tools-extra/clangd/unittests/TestTU.cpp +++ b/clang-tools-extra/clangd/unittests/TestTU.cpp @@ -59,8 +59,7 @@ FS.OverlayRealFileSystemForModules = true; Inputs.TFS = &FS; Inputs.Opts = ParseOptions(); - Inputs.Opts.ClangTidyOpts.Checks = ClangTidyChecks; - Inputs.Opts.ClangTidyOpts.WarningsAsErrors = ClangTidyWarningsAsErrors; + Inputs.ClangTidyProvider = ClangTidyProvider.get(); Inputs.Index = ExternalIndex; if (Inputs.Index) Inputs.Opts.SuggestMissingIncludes = true; diff --git a/clang-tools-extra/clangd/unittests/TestTidyProvider.h b/clang-tools-extra/clangd/unittests/TestTidyProvider.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/TestTidyProvider.h @@ -0,0 +1,34 @@ +//===-- TestTidyProvider.h --------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "../TidyProvider.h" +#include "llvm/Support/VirtualFileSystem.h" + +namespace clang { +namespace clangd { + +class TestClangTidyProvider : public ClangdTidyProvider { +public: + /// Convienence method for creating a provider that just uses \p Checks and \p + /// WarningsAsErrors + TestClangTidyProvider(llvm::StringRef Checks, + llvm::StringRef WarningsAsErrors = {}); + TestClangTidyProvider(const tidy::ClangTidyGlobalOptions &GlobalOptions, + const tidy::ClangTidyOptions &Options); + std::vector getRawOptions(llvm::vfs::FileSystem * /*unused*/, + llvm::StringRef FileName) override; + const tidy::ClangTidyGlobalOptions & + getGlobalOptions(llvm::vfs::FileSystem * /*unused*/) override; + +private: + tidy::ClangTidyGlobalOptions GlobalOpts; + tidy::ClangTidyOptions Opts; +}; + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/TestTidyProvider.cpp b/clang-tools-extra/clangd/unittests/TestTidyProvider.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/TestTidyProvider.cpp @@ -0,0 +1,50 @@ +//===-- TestTidyProvider.cpp ------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "TestTidyProvider.h" + +namespace clang { + +using namespace tidy; + +namespace clangd { + +TestClangTidyProvider::TestClangTidyProvider(llvm::StringRef Checks, + llvm::StringRef WarningsAsErrors) { + if (Checks.empty()) + Opts.Checks.emplace(getDefaultTidyChecks()); + else + Opts.Checks.emplace((Checks + "," + getUnusableTidyChecks()).str()); + if (!WarningsAsErrors.empty()) + Opts.WarningsAsErrors.emplace(WarningsAsErrors); +} + +TestClangTidyProvider::TestClangTidyProvider( + const ClangTidyGlobalOptions &GlobalOptions, + const ClangTidyOptions &Options) + : GlobalOpts(GlobalOptions), Opts(Options) { + if (!Opts.Checks.hasValue() || Opts.Checks->empty()) + Opts.Checks = getDefaultTidyChecks().str(); + else + Opts.Checks->append(("," + getUnusableTidyChecks().str())); +} + +std::vector +TestClangTidyProvider::getRawOptions(llvm::vfs::FileSystem * /*unused*/, + llvm::StringRef FileName) { + std::vector RawOpts; + RawOpts.emplace_back(Opts, "mock-options"); + return RawOpts; +} + +const ClangTidyGlobalOptions & +TestClangTidyProvider::getGlobalOptions(llvm::vfs::FileSystem * /*unused*/) { + return GlobalOpts; +} +} // namespace clangd +} // namespace clang