diff --git a/clang-tools-extra/clangd/TidyProvider.cpp b/clang-tools-extra/clangd/TidyProvider.cpp --- a/clang-tools-extra/clangd/TidyProvider.cpp +++ b/clang-tools-extra/clangd/TidyProvider.cpp @@ -8,7 +8,9 @@ #include "TidyProvider.h" #include "Config.h" +#include "support/FileCache.h" #include "support/Logger.h" +#include "support/ThreadsafeFS.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" @@ -19,53 +21,115 @@ namespace clang { namespace clangd { +namespace { -static void mergeCheckList(llvm::Optional &Checks, - llvm::StringRef List) { - if (List.empty()) - return; - if (!Checks || Checks->empty()) { - Checks.emplace(List); - return; +// Access to config from a .clang-tidy file, caching IO and parsing. +class DotClangTidyCache : private FileCache { + // We cache and expose shared_ptr to avoid copying the value on every lookup + // when we're ultimately just going to pass it to mergeWith. + mutable std::shared_ptr Value; + +public: + DotClangTidyCache(PathRef Path) : FileCache(Path) {} + + std::shared_ptr + get(const ThreadsafeFS &TFS, + std::chrono::steady_clock::time_point FreshTime) const { + std::shared_ptr Result; + read( + TFS, FreshTime, + [this](llvm::Optional Data) { + Value.reset(); + if (Data && !Data->empty()) { + if (auto Parsed = tidy::parseConfiguration(*Data)) + Value = std::make_shared( + std::move(*Parsed)); + else + elog("Error parsing clang-tidy configuration in {0}: {1}", path(), + Parsed.getError().message()); + } + }, + [&]() { Result = Value; }); + return Result; } - *Checks = llvm::join_items(",", *Checks, List); -} +}; -static llvm::Optional -tryReadConfigFile(llvm::vfs::FileSystem *FS, llvm::StringRef Directory) { - assert(!Directory.empty()); - // We guaranteed that child directories of Directory exist, so this assert - // should hopefully never fail. - assert(FS->exists(Directory)); +// Access to combined config from .clang-tidy files governing a source file. +// Each config file is cached and the caches are shared for affected sources. +// +// FIXME: largely duplicates config::Provider::fromAncestorRelativeYAMLFiles. +// Potentially useful for compile_commands.json too. Extract? +class DotClangTidyTree { + const ThreadsafeFS &FS; + std::string RelPath; + std::chrono::steady_clock::duration MaxStaleness; - llvm::SmallString<128> ConfigFile(Directory); - llvm::sys::path::append(ConfigFile, ".clang-tidy"); + mutable std::mutex Mu; + // Keys are the ancestor directory, not the actual config path within it. + // We only insert into this map, so pointers to values are stable forever. + // Mutex guards the map itself, not the values (which are threadsafe). + mutable llvm::StringMap Cache; - llvm::ErrorOr FileStatus = FS->status(ConfigFile); +public: + DotClangTidyTree(const ThreadsafeFS &FS) + : FS(FS), RelPath(".clang-tidy"), MaxStaleness(std::chrono::seconds(5)) {} - if (!FileStatus || !FileStatus->isRegularFile()) - return llvm::None; + void apply(tidy::ClangTidyOptions &Result, PathRef AbsPath) { + namespace path = llvm::sys::path; + assert(path::is_absolute(AbsPath)); + + // Compute absolute paths to all ancestors (substrings of P.Path). + // Ensure cache entries for each ancestor exist in the map. + llvm::StringRef Parent = path::parent_path(AbsPath); + llvm::SmallVector Caches; + { + std::lock_guard Lock(Mu); + for (auto I = path::begin(Parent, path::Style::posix), + E = path::end(Parent); + I != E; ++I) { + assert(I->end() >= Parent.begin() && I->end() <= Parent.end() && + "Canonical path components should be substrings"); + llvm::StringRef Ancestor(Parent.begin(), I->end() - Parent.begin()); + auto It = Cache.find(Ancestor); - llvm::ErrorOr> Text = - FS->getBufferForFile(ConfigFile); - if (std::error_code EC = Text.getError()) { - elog("Can't read '{0}': {1}", ConfigFile, EC.message()); - return llvm::None; + // Assemble the actual config file path only if needed. + if (It == Cache.end()) { + llvm::SmallString<256> ConfigPath = Ancestor; + path::append(ConfigPath, RelPath); + It = Cache.try_emplace(Ancestor, ConfigPath.str()).first; + } + Caches.push_back(&It->second); + } + } + // Finally query each individual file. + // This will take a (per-file) lock for each file that actually exists. + std::chrono::steady_clock::time_point FreshTime = + std::chrono::steady_clock::now() - MaxStaleness; + llvm::SmallVector, 4> + OptionStack; + for (const DotClangTidyCache *Cache : Caches) + if (auto Config = Cache->get(FS, FreshTime)) { + OptionStack.push_back(std::move(Config)); + if (!OptionStack.back()->InheritParentConfig.getValueOr(false)) + break; + } + unsigned Order = 1u; + for (auto &Option : llvm::reverse(OptionStack)) + Result.mergeWith(*Option, Order++); } +}; - // 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()) - elog("Error parsing clang-tidy configuration in '{0}': {1}", ConfigFile, - ParsedOptions.getError().message()); - return llvm::None; +} // namespace + +static void mergeCheckList(llvm::Optional &Checks, + llvm::StringRef List) { + if (List.empty()) + return; + if (!Checks || Checks->empty()) { + Checks.emplace(List); + return; } - return std::move(*ParsedOptions); + *Checks = llvm::join_items(",", *Checks, List); } TidyProviderRef provideEnvironment() { @@ -176,41 +240,9 @@ } TidyProvider provideClangTidyFiles(ThreadsafeFS &TFS) { - return [&TFS](tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) { - llvm::SmallVector OptionStack; - auto FS(TFS.view(llvm::None)); - llvm::SmallString<256> AbsolutePath(Filename); - - assert(llvm::sys::path::is_absolute(AbsolutePath)); - - llvm::sys::path::remove_dots(AbsolutePath, true); - llvm::StringRef Directory = llvm::sys::path::parent_path(AbsolutePath); - { - auto Status = FS->status(Directory); - - if (!Status || !Status->isDirectory()) { - elog("Error reading configuration from {0}: directory doesn't exist", - Directory); - return; - } - } - - // FIXME: Store options in a cache that validates itself against changes - // during the clangd session. - for (llvm::StringRef CurrentDirectory = Directory; - !CurrentDirectory.empty(); - CurrentDirectory = llvm::sys::path::parent_path(CurrentDirectory)) { - auto ConfigFile = tryReadConfigFile(FS.get(), CurrentDirectory); - if (!ConfigFile) - continue; - OptionStack.push_back(std::move(*ConfigFile)); - // Should we search for a parent config to merge - if (!OptionStack.back().InheritParentConfig.getValueOr(false)) - break; - } - unsigned Order = 1U; - for (auto &Option : llvm::reverse(OptionStack)) - Opts.mergeWith(Option, Order++); + return [Tree = std::make_unique(TFS)]( + tidy::ClangTidyOptions &Opts, llvm::StringRef Filename) { + Tree->apply(Opts, Filename); }; }