//===--- ClangTidyOptions.cpp - clang-tidy ----------------------*- 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 "ClangTidyOptions.h" #include "ClangTidyModuleRegistry.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/SmallString.h" #include "llvm/Support/Debug.h" #include "llvm/Support/Errc.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MemoryBufferRef.h" #include "llvm/Support/Path.h" #include "llvm/Support/YAMLTraits.h" #include #include #define DEBUG_TYPE "clang-tidy-options" using clang::tidy::ClangTidyOptions; using clang::tidy::FileFilter; using OptionsSource = clang::tidy::ClangTidyOptionsProvider::OptionsSource; LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(FileFilter) LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(FileFilter::LineRange) namespace llvm::yaml { // Map std::pair to a JSON array of size 2. template <> struct SequenceTraits { static size_t size(IO &IO, FileFilter::LineRange &Range) { return Range.first == 0 ? 0 : Range.second == 0 ? 1 : 2; } static unsigned &element(IO &IO, FileFilter::LineRange &Range, size_t Index) { if (Index > 1) IO.setError("Too many elements in line range."); return Index == 0 ? Range.first : Range.second; } }; template <> struct MappingTraits { static void mapping(IO &IO, FileFilter &File) { IO.mapRequired("name", File.Name); IO.mapOptional("lines", File.LineRanges); } static std::string validate(IO &Io, FileFilter &File) { if (File.Name.empty()) return "No file name specified"; for (const FileFilter::LineRange &Range : File.LineRanges) { if (Range.first <= 0 || Range.second <= 0) return "Invalid line range"; } return ""; } }; template <> struct MappingTraits { static void mapping(IO &IO, ClangTidyOptions::StringPair &KeyValue) { IO.mapRequired("key", KeyValue.first); IO.mapRequired("value", KeyValue.second); } }; struct NOptionMap { NOptionMap(IO &) {} NOptionMap(IO &, const ClangTidyOptions::OptionMap &OptionMap) { Options.reserve(OptionMap.size()); for (const auto &KeyValue : OptionMap) Options.emplace_back(std::string(KeyValue.getKey()), KeyValue.getValue().Value); } ClangTidyOptions::OptionMap denormalize(IO &) { ClangTidyOptions::OptionMap Map; for (const auto &KeyValue : Options) Map[KeyValue.first] = ClangTidyOptions::ClangTidyValue(KeyValue.second); return Map; } std::vector Options; }; template <> void yamlize(IO &IO, ClangTidyOptions::OptionMap &Options, bool, EmptyContext &Ctx) { if (IO.outputting()) { IO.beginMapping(); // Only output as a map for (auto &Key : Options) { bool UseDefault; void *SaveInfo; IO.preflightKey(Key.getKey().data(), true, false, UseDefault, SaveInfo); StringRef S = Key.getValue().Value; IO.scalarString(S, needsQuotes(S)); IO.postflightKey(SaveInfo); } IO.endMapping(); } else { // We need custom logic here to support the old method of specifying check // options using a list of maps containing key and value keys. Input &I = reinterpret_cast(IO); if (isa(I.getCurrentNode())) { MappingNormalization NOpts( IO, Options); EmptyContext Ctx; yamlize(IO, NOpts->Options, true, Ctx); } else if (isa(I.getCurrentNode())) { IO.beginMapping(); for (StringRef Key : IO.keys()) { IO.mapRequired(Key.data(), Options[Key].Value); } IO.endMapping(); } else { IO.setError("expected a sequence or map"); } } } struct ChecksVariant { std::optional AsString; std::optional> AsVector; }; template <> void yamlize(IO &IO, ChecksVariant &Checks, bool, EmptyContext &Ctx) { if (!IO.outputting()) { // Special case for reading from YAML // Must support reading from both a string or a list Input &I = reinterpret_cast(IO); if (isa(I.getCurrentNode())) { Checks.AsString = std::string(); yamlize(IO, *Checks.AsString, true, Ctx); } else if (isa(I.getCurrentNode())) { Checks.AsVector = std::vector(); yamlize(IO, *Checks.AsVector, true, Ctx); } else { IO.setError("expected string or sequence"); } } } static void mapChecks(IO &IO, std::optional &Checks) { if (IO.outputting()) { // Output always a string IO.mapOptional("Checks", Checks); } else { // Input as either a string or a list ChecksVariant ChecksAsVariant; IO.mapOptional("Checks", ChecksAsVariant); if (ChecksAsVariant.AsString) Checks = ChecksAsVariant.AsString; else if (ChecksAsVariant.AsVector) Checks = llvm::join(*ChecksAsVariant.AsVector, ","); } } template <> struct MappingTraits { static void mapping(IO &IO, ClangTidyOptions &Options) { bool Ignored = false; mapChecks(IO, Options.Checks); IO.mapOptional("WarningsAsErrors", Options.WarningsAsErrors); IO.mapOptional("HeaderFileExtensions", Options.HeaderFileExtensions); IO.mapOptional("ImplementationFileExtensions", Options.ImplementationFileExtensions); IO.mapOptional("HeaderFilterRegex", Options.HeaderFilterRegex); IO.mapOptional("AnalyzeTemporaryDtors", Ignored); // deprecated IO.mapOptional("FormatStyle", Options.FormatStyle); IO.mapOptional("User", Options.User); IO.mapOptional("CheckOptions", Options.CheckOptions); IO.mapOptional("ExtraArgs", Options.ExtraArgs); IO.mapOptional("ExtraArgsBefore", Options.ExtraArgsBefore); IO.mapOptional("InheritParentConfig", Options.InheritParentConfig); IO.mapOptional("UseColor", Options.UseColor); IO.mapOptional("SystemHeaders", Options.SystemHeaders); } }; } // namespace llvm::yaml namespace clang::tidy { ClangTidyOptions ClangTidyOptions::getDefaults() { ClangTidyOptions Options; Options.Checks = ""; Options.WarningsAsErrors = ""; Options.HeaderFileExtensions = {"", "h", "hh", "hpp", "hxx"}; Options.ImplementationFileExtensions = {"c", "cc", "cpp", "cxx"}; Options.HeaderFilterRegex = ""; Options.SystemHeaders = false; Options.FormatStyle = "none"; Options.User = std::nullopt; for (const ClangTidyModuleRegistry::entry &Module : ClangTidyModuleRegistry::entries()) Options.mergeWith(Module.instantiate()->getModuleOptions(), 0); return Options; } template static void mergeVectors(std::optional &Dest, const std::optional &Src) { if (Src) { if (Dest) Dest->insert(Dest->end(), Src->begin(), Src->end()); else Dest = Src; } } static void mergeCommaSeparatedLists(std::optional &Dest, const std::optional &Src) { if (Src) Dest = (Dest && !Dest->empty() ? *Dest + "," : "") + *Src; } template static void overrideValue(std::optional &Dest, const std::optional &Src) { if (Src) Dest = Src; } ClangTidyOptions &ClangTidyOptions::mergeWith(const ClangTidyOptions &Other, unsigned Order) { mergeCommaSeparatedLists(Checks, Other.Checks); mergeCommaSeparatedLists(WarningsAsErrors, Other.WarningsAsErrors); overrideValue(HeaderFileExtensions, Other.HeaderFileExtensions); overrideValue(ImplementationFileExtensions, Other.ImplementationFileExtensions); overrideValue(HeaderFilterRegex, Other.HeaderFilterRegex); overrideValue(SystemHeaders, Other.SystemHeaders); overrideValue(FormatStyle, Other.FormatStyle); overrideValue(User, Other.User); overrideValue(UseColor, Other.UseColor); mergeVectors(ExtraArgs, Other.ExtraArgs); mergeVectors(ExtraArgsBefore, Other.ExtraArgsBefore); for (const auto &KeyValue : Other.CheckOptions) { CheckOptions.insert_or_assign( KeyValue.getKey(), ClangTidyValue(KeyValue.getValue().Value, KeyValue.getValue().Priority + Order)); } return *this; } ClangTidyOptions ClangTidyOptions::merge(const ClangTidyOptions &Other, unsigned Order) const { ClangTidyOptions Result = *this; Result.mergeWith(Other, Order); return Result; } const char ClangTidyOptionsProvider::OptionsSourceTypeDefaultBinary[] = "clang-tidy binary"; const char ClangTidyOptionsProvider::OptionsSourceTypeCheckCommandLineOption[] = "command-line option '-checks'"; const char ClangTidyOptionsProvider::OptionsSourceTypeConfigCommandLineOption[] = "command-line option '-config'"; ClangTidyOptions ClangTidyOptionsProvider::getOptions(llvm::StringRef FileName) { ClangTidyOptions Result; unsigned Priority = 0; for (auto &Source : getRawOptions(FileName)) Result.mergeWith(Source.first, ++Priority); return Result; } std::vector DefaultOptionsProvider::getRawOptions(llvm::StringRef FileName) { std::vector Result; Result.emplace_back(DefaultOptions, OptionsSourceTypeDefaultBinary); return Result; } ConfigOptionsProvider::ConfigOptionsProvider( ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions, ClangTidyOptions ConfigOptions, ClangTidyOptions OverrideOptions, llvm::IntrusiveRefCntPtr FS) : FileOptionsBaseProvider(std::move(GlobalOptions), std::move(DefaultOptions), std::move(OverrideOptions), std::move(FS)), ConfigOptions(std::move(ConfigOptions)) {} std::vector ConfigOptionsProvider::getRawOptions(llvm::StringRef FileName) { std::vector RawOptions = DefaultOptionsProvider::getRawOptions(FileName); if (ConfigOptions.InheritParentConfig.value_or(false)) { LLVM_DEBUG(llvm::dbgs() << "Getting options for file " << FileName << "...\n"); assert(FS && "FS must be set."); llvm::SmallString<128> AbsoluteFilePath(FileName); if (!FS->makeAbsolute(AbsoluteFilePath)) { addRawFileOptions(AbsoluteFilePath, RawOptions); } } RawOptions.emplace_back(ConfigOptions, OptionsSourceTypeConfigCommandLineOption); RawOptions.emplace_back(OverrideOptions, OptionsSourceTypeCheckCommandLineOption); return RawOptions; } FileOptionsBaseProvider::FileOptionsBaseProvider( ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions, ClangTidyOptions OverrideOptions, llvm::IntrusiveRefCntPtr VFS) : DefaultOptionsProvider(std::move(GlobalOptions), std::move(DefaultOptions)), OverrideOptions(std::move(OverrideOptions)), FS(std::move(VFS)) { if (!FS) FS = llvm::vfs::getRealFileSystem(); ConfigHandlers.emplace_back(".clang-tidy", parseConfiguration); } FileOptionsBaseProvider::FileOptionsBaseProvider( ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions, ClangTidyOptions OverrideOptions, FileOptionsBaseProvider::ConfigFileHandlers ConfigHandlers) : DefaultOptionsProvider(std::move(GlobalOptions), std::move(DefaultOptions)), OverrideOptions(std::move(OverrideOptions)), ConfigHandlers(std::move(ConfigHandlers)) {} void FileOptionsBaseProvider::addRawFileOptions( llvm::StringRef AbsolutePath, std::vector &CurOptions) { 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)) { std::optional Result; auto Iter = CachedOptions.find(CurrentPath); if (Iter != CachedOptions.end()) Result = Iter->second; if (!Result) Result = tryReadConfigFile(CurrentPath); if (Result) { // Store cached value for all intermediate directories. while (Path != CurrentPath) { LLVM_DEBUG(llvm::dbgs() << "Caching configuration for path " << Path << ".\n"); if (!CachedOptions.count(Path)) CachedOptions[Path] = *Result; Path = llvm::sys::path::parent_path(Path); } CachedOptions[Path] = *Result; CurOptions.push_back(*Result); if (!Result->first.InheritParentConfig.value_or(false)) break; } } // Reverse order of file configs because closer configs should have higher // priority. std::reverse(CurOptions.begin() + CurSize, CurOptions.end()); } FileOptionsProvider::FileOptionsProvider( ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions, ClangTidyOptions OverrideOptions, llvm::IntrusiveRefCntPtr VFS) : FileOptionsBaseProvider(std::move(GlobalOptions), std::move(DefaultOptions), std::move(OverrideOptions), std::move(VFS)) {} FileOptionsProvider::FileOptionsProvider( ClangTidyGlobalOptions GlobalOptions, ClangTidyOptions DefaultOptions, ClangTidyOptions OverrideOptions, FileOptionsBaseProvider::ConfigFileHandlers ConfigHandlers) : FileOptionsBaseProvider( std::move(GlobalOptions), std::move(DefaultOptions), std::move(OverrideOptions), std::move(ConfigHandlers)) {} // FIXME: This method has some common logic with clang::format::getStyle(). // Consider pulling out common bits to a findParentFileWithName function or // similar. std::vector FileOptionsProvider::getRawOptions(StringRef FileName) { LLVM_DEBUG(llvm::dbgs() << "Getting options for file " << FileName << "...\n"); assert(FS && "FS must be set."); llvm::SmallString<128> AbsoluteFilePath(FileName); if (FS->makeAbsolute(AbsoluteFilePath)) return {}; std::vector RawOptions = DefaultOptionsProvider::getRawOptions(AbsoluteFilePath.str()); addRawFileOptions(AbsoluteFilePath, RawOptions); OptionsSource CommandLineOptions(OverrideOptions, OptionsSourceTypeCheckCommandLineOption); RawOptions.push_back(CommandLineOptions); return RawOptions; } std::optional FileOptionsBaseProvider::tryReadConfigFile(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 std::nullopt; } for (const ConfigFileHandler &ConfigHandler : ConfigHandlers) { SmallString<128> ConfigFile(Directory); llvm::sys::path::append(ConfigFile, ConfigHandler.first); LLVM_DEBUG(llvm::dbgs() << "Trying " << ConfigFile << "...\n"); llvm::ErrorOr FileStatus = FS->status(ConfigFile); if (!FileStatus || !FileStatus->isRegularFile()) continue; llvm::ErrorOr> Text = FS->getBufferForFile(ConfigFile); if (std::error_code EC = Text.getError()) { llvm::errs() << "Can't read " << ConfigFile << ": " << EC.message() << "\n"; continue; } // Skip empty files, e.g. files opened for writing via shell output // redirection. if ((*Text)->getBuffer().empty()) continue; llvm::ErrorOr ParsedOptions = ConfigHandler.second({(*Text)->getBuffer(), ConfigFile}); if (!ParsedOptions) { if (ParsedOptions.getError()) llvm::errs() << "Error parsing " << ConfigFile << ": " << ParsedOptions.getError().message() << "\n"; continue; } return OptionsSource(*ParsedOptions, std::string(ConfigFile)); } return std::nullopt; } /// Parses -line-filter option and stores it to the \c Options. std::error_code parseLineFilter(StringRef LineFilter, clang::tidy::ClangTidyGlobalOptions &Options) { llvm::yaml::Input Input(LineFilter); Input >> Options.LineFilter; return Input.error(); } llvm::ErrorOr parseConfiguration(llvm::MemoryBufferRef Config) { llvm::yaml::Input Input(Config); ClangTidyOptions Options; Input >> Options; if (Input.error()) return Input.error(); return Options; } static void diagHandlerImpl(const llvm::SMDiagnostic &Diag, void *Ctx) { (*reinterpret_cast(Ctx))(Diag); } llvm::ErrorOr parseConfigurationWithDiags(llvm::MemoryBufferRef Config, DiagCallback Handler) { llvm::yaml::Input Input(Config, nullptr, Handler ? diagHandlerImpl : nullptr, &Handler); ClangTidyOptions Options; Input >> Options; if (Input.error()) return Input.error(); return Options; } std::string configurationAsText(const ClangTidyOptions &Options) { std::string Text; llvm::raw_string_ostream Stream(Text); llvm::yaml::Output Output(Stream); // We use the same mapping method for input and output, so we need a non-const // reference here. ClangTidyOptions NonConstValue = Options; Output << NonConstValue; return Stream.str(); } } // namespace clang::tidy