Commit fcf7cc26 authored by Nathan James's avatar Nathan James
Browse files

[clang-tidy] Added support for validating configuration options

Summary:
Adds support for `ClangTidyCheck::OptionsView` to deteremine:
  - If an option is found in the configuration.
  - If an integer option read from configuration is parsable to an integer.
  - Parse and Serialize enum configuration options directly using a mapping from `llvm::StringRef` to `EnumType`.
  - If an integer or enum option isn't parseable but there is a default value it will issue a warning to stderr that the config value hasn't been used.
  - If an enum option isn't parsable it can provide a hint if the value was a typo.

Reviewers: aaron.ballman, alexfh, gribozavr2

Reviewed By: aaron.ballman

Subscribers: xazax.hun, cfe-commits

Tags: #clang, #clang-tools-extra

Differential Revision: https://reviews.llvm.org/D77085
parent aed2fdb1
Loading
Loading
Loading
Loading
+142 −7
Original line number Diff line number Diff line
@@ -7,10 +7,44 @@
//===----------------------------------------------------------------------===//

#include "ClangTidyCheck.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/raw_ostream.h"

namespace clang {
namespace tidy {

char MissingOptionError::ID;
char UnparseableEnumOptionError::ID;
char UnparseableIntegerOptionError::ID;

std::string MissingOptionError::message() const {
  llvm::SmallString<128> Buffer;
  llvm::raw_svector_ostream Output(Buffer);
  Output << "option not found '" << OptionName << '\'';
  return std::string(Buffer);
}

std::string UnparseableEnumOptionError::message() const {
  llvm::SmallString<128> Buffer;
  llvm::raw_svector_ostream Output(Buffer);
  Output << "invalid configuration value '" << LookupValue << "' for option '"
         << LookupName << '\'';
  if (SuggestedValue)
    Output << "; did you mean '" << *SuggestedValue << "'?";
  return std::string(Buffer);
}

std::string UnparseableIntegerOptionError::message() const {
  llvm::SmallString<128> Buffer;
  llvm::raw_svector_ostream Output(Buffer);
  Output << "invalid configuration value '" << LookupValue << "' for option '"
         << LookupName << "'; expected "
         << (IsBoolean ? "a bool" : "an integer value");
  return std::string(Buffer);
}

ClangTidyCheck::ClangTidyCheck(StringRef CheckName, ClangTidyContext *Context)
    : CheckName(CheckName), Context(Context),
      Options(CheckName, Context->getOptions().CheckOptions) {
@@ -34,17 +68,16 @@ ClangTidyCheck::OptionsView::OptionsView(StringRef CheckName,
                         const ClangTidyOptions::OptionMap &CheckOptions)
    : NamePrefix(CheckName.str() + "."), CheckOptions(CheckOptions) {}

std::string ClangTidyCheck::OptionsView::get(StringRef LocalName,
                                             StringRef Default) const {
llvm::Expected<std::string>
ClangTidyCheck::OptionsView::get(StringRef LocalName) const {
  const auto &Iter = CheckOptions.find(NamePrefix + LocalName.str());
  if (Iter != CheckOptions.end())
    return Iter->second;
  return std::string(Default);
  return llvm::make_error<MissingOptionError>((NamePrefix + LocalName).str());
}

std::string
ClangTidyCheck::OptionsView::getLocalOrGlobal(StringRef LocalName,
                                              StringRef Default) const {
llvm::Expected<std::string>
ClangTidyCheck::OptionsView::getLocalOrGlobal(StringRef LocalName) const {
  auto Iter = CheckOptions.find(NamePrefix + LocalName.str());
  if (Iter != CheckOptions.end())
    return Iter->second;
@@ -52,7 +85,65 @@ ClangTidyCheck::OptionsView::getLocalOrGlobal(StringRef LocalName,
  Iter = CheckOptions.find(LocalName.str());
  if (Iter != CheckOptions.end())
    return Iter->second;
  return std::string(Default);
  return llvm::make_error<MissingOptionError>((NamePrefix + LocalName).str());
}

static llvm::Expected<bool> getAsBool(StringRef Value,
                                      const llvm::Twine &LookupName) {
  if (Value == "true")
    return true;
  if (Value == "false")
    return false;
  bool Result;
  if (!Value.getAsInteger(10, Result))
    return Result;
  return llvm::make_error<UnparseableIntegerOptionError>(LookupName.str(),
                                                         Value.str(), true);
}

template <>
llvm::Expected<bool>
ClangTidyCheck::OptionsView::get<bool>(StringRef LocalName) const {
  llvm::Expected<std::string> ValueOr = get(LocalName);
  if (ValueOr)
    return getAsBool(*ValueOr, NamePrefix + LocalName);
  return ValueOr.takeError();
}

template <>
bool ClangTidyCheck::OptionsView::get<bool>(StringRef LocalName,
                                            bool Default) const {
  llvm::Expected<bool> ValueOr = get<bool>(LocalName);
  if (ValueOr)
    return *ValueOr;
  logErrToStdErr(ValueOr.takeError());
  return Default;
}

template <>
llvm::Expected<bool>
ClangTidyCheck::OptionsView::getLocalOrGlobal<bool>(StringRef LocalName) const {
  llvm::Expected<std::string> ValueOr = get(LocalName);
  bool IsGlobal = false;
  if (!ValueOr) {
    llvm::consumeError(ValueOr.takeError());
    ValueOr = getLocalOrGlobal(LocalName);
    IsGlobal = true;
  }
  if (!ValueOr)
    return ValueOr.takeError();
  return getAsBool(*ValueOr, IsGlobal ? llvm::Twine(LocalName)
                                      : (NamePrefix + LocalName));
}

template <>
bool ClangTidyCheck::OptionsView::getLocalOrGlobal<bool>(StringRef LocalName,
                                                         bool Default) const {
  llvm::Expected<bool> ValueOr = getLocalOrGlobal<bool>(LocalName);
  if (ValueOr)
    return *ValueOr;
  logErrToStdErr(ValueOr.takeError());
  return Default;
}

void ClangTidyCheck::OptionsView::store(ClangTidyOptions::OptionMap &Options,
@@ -67,5 +158,49 @@ void ClangTidyCheck::OptionsView::store(ClangTidyOptions::OptionMap &Options,
  store(Options, LocalName, llvm::itostr(Value));
}

llvm::Expected<int64_t> ClangTidyCheck::OptionsView::getEnumInt(
    StringRef LocalName, ArrayRef<std::pair<StringRef, int64_t>> Mapping,
    bool CheckGlobal, bool IgnoreCase) {
  auto Iter = CheckOptions.find((NamePrefix + LocalName).str());
  if (CheckGlobal && Iter == CheckOptions.end())
    Iter = CheckOptions.find(LocalName.str());
  if (Iter == CheckOptions.end())
    return llvm::make_error<MissingOptionError>((NamePrefix + LocalName).str());

  StringRef Value = Iter->second;
  StringRef Closest;
  unsigned EditDistance = -1;
  for (const auto &NameAndEnum : Mapping) {
    if (IgnoreCase) {
      if (Value.equals_lower(NameAndEnum.first))
        return NameAndEnum.second;
    } else if (Value.equals(NameAndEnum.first)) {
      return NameAndEnum.second;
    } else if (Value.equals_lower(NameAndEnum.first)) {
      Closest = NameAndEnum.first;
      EditDistance = 0;
      continue;
    }
    unsigned Distance = Value.edit_distance(NameAndEnum.first);
    if (Distance < EditDistance) {
      EditDistance = Distance;
      Closest = NameAndEnum.first;
    }
  }
  if (EditDistance < 3)
    return llvm::make_error<UnparseableEnumOptionError>(
        Iter->first, Iter->second, std::string(Closest));
  return llvm::make_error<UnparseableEnumOptionError>(Iter->first,
                                                      Iter->second);
}

void ClangTidyCheck::OptionsView::logErrToStdErr(llvm::Error &&Err) {
  llvm::logAllUnhandledErrors(
      llvm::handleErrors(std::move(Err),
                         [](const MissingOptionError &) -> llvm::Error {
                           return llvm::Error::success();
                         }),
      llvm::errs(), "warning: ");
}
} // namespace tidy
} // namespace clang
+299 −15
Original line number Diff line number Diff line
@@ -14,7 +14,9 @@
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/SourceManager.h"
#include "llvm/ADT/Optional.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Error.h"
#include <memory>
#include <type_traits>
#include <vector>
@@ -25,6 +27,65 @@ class CompilerInstance;

namespace tidy {

template <typename T> class OptionError : public llvm::ErrorInfo<T> {
  std::error_code convertToErrorCode() const override {
    return llvm::inconvertibleErrorCode();
  }

public:
  void log(raw_ostream &OS) const override { OS << this->message(); }
};

class MissingOptionError : public OptionError<MissingOptionError> {
public:
  explicit MissingOptionError(std::string OptionName)
      : OptionName(OptionName) {}

  std::string message() const override;
  static char ID;
private:
  const std::string OptionName;
};

class UnparseableEnumOptionError
    : public OptionError<UnparseableEnumOptionError> {
public:
  explicit UnparseableEnumOptionError(std::string LookupName,
                                      std::string LookupValue)
      : LookupName(LookupName), LookupValue(LookupValue) {}
  explicit UnparseableEnumOptionError(std::string LookupName,
                                      std::string LookupValue,
                                      std::string SuggestedValue)
      : LookupName(LookupName), LookupValue(LookupValue),
        SuggestedValue(SuggestedValue) {}

  std::string message() const override;
  static char ID;

private:
  const std::string LookupName;
  const std::string LookupValue;
  const llvm::Optional<std::string> SuggestedValue;
};

class UnparseableIntegerOptionError
    : public OptionError<UnparseableIntegerOptionError> {
public:
  explicit UnparseableIntegerOptionError(std::string LookupName,
                                         std::string LookupValue,
                                         bool IsBoolean = false)
      : LookupName(LookupName), LookupValue(LookupValue), IsBoolean(IsBoolean) {
  }

  std::string message() const override;
  static char ID;

private:
  const std::string LookupName;
  const std::string LookupValue;
  const bool IsBoolean;
};

/// Base class for all clang-tidy checks.
///
/// To implement a ``ClangTidyCheck``, write a subclass and override some of the
@@ -127,35 +188,111 @@ public:
    OptionsView(StringRef CheckName,
                const ClangTidyOptions::OptionMap &CheckOptions);

    /// Read a named option from the ``Context``.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present, returns
    /// a ``MissingOptionError``.
    llvm::Expected<std::string> get(StringRef LocalName) const;

    /// Read a named option from the ``Context``.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present, returns
    /// \p Default.
    std::string get(StringRef LocalName, StringRef Default) const;
    std::string get(StringRef LocalName, StringRef Default) const {
      if (llvm::Expected<std::string> Val = get(LocalName))
        return *Val;
      else
        llvm::consumeError(Val.takeError());
      return Default.str();
    }

    /// Read a named option from the ``Context``.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either, returns a ``MissingOptionError``.
    llvm::Expected<std::string> getLocalOrGlobal(StringRef LocalName) const;

    /// Read a named option from the ``Context``.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either, returns Default.
    std::string getLocalOrGlobal(StringRef LocalName, StringRef Default) const;
    /// present either, returns \p Default.
    std::string getLocalOrGlobal(StringRef LocalName, StringRef Default) const {
      if (llvm::Expected<std::string> Val = getLocalOrGlobal(LocalName))
        return *Val;
      else
        llvm::consumeError(Val.takeError());
      return Default.str();
    }

    /// Read a named option from the ``Context`` and parse it as an
    /// integral type ``T``.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present, returns
    /// \p Default.
    /// a ``MissingOptionError``. If the corresponding key can't be parsed as
    /// a ``T``, return an ``UnparseableIntegerOptionError``.
    template <typename T>
    std::enable_if_t<std::is_integral<T>::value, llvm::Expected<T>>
    get(StringRef LocalName) const {
      if (llvm::Expected<std::string> Value = get(LocalName)) {
        T Result{};
        if (!StringRef(*Value).getAsInteger(10, Result))
          return Result;
        return llvm::make_error<UnparseableIntegerOptionError>(
            (NamePrefix + LocalName).str(), *Value);
      } else
        return std::move(Value.takeError());
    }

    /// Read a named option from the ``Context`` and parse it as an
    /// integral type ``T``.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present or it can't be
    /// parsed as a ``T``, returns \p Default.
    template <typename T>
    std::enable_if_t<std::is_integral<T>::value, T> get(StringRef LocalName,
                                                        T Default) const {
      std::string Value = get(LocalName, "");
      T Result = Default;
      if (!Value.empty())
        StringRef(Value).getAsInteger(10, Result);
      if (llvm::Expected<T> ValueOr = get<T>(LocalName))
        return *ValueOr;
      else
        logErrToStdErr(ValueOr.takeError());
      return Default;
    }

    /// Read a named option from the ``Context`` and parse it as an
    /// integral type ``T``.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either, returns a ``MissingOptionError``. If the corresponding
    /// key can't be parsed as a ``T``, return an
    /// ``UnparseableIntegerOptionError``.
    template <typename T>
    std::enable_if_t<std::is_integral<T>::value, llvm::Expected<T>>
    getLocalOrGlobal(StringRef LocalName) const {
      llvm::Expected<std::string> ValueOr = get(LocalName);
      bool IsGlobal = false;
      if (!ValueOr) {
        IsGlobal = true;
        llvm::consumeError(ValueOr.takeError());
        ValueOr = getLocalOrGlobal(LocalName);
        if (!ValueOr)
          return std::move(ValueOr.takeError());
      }
      T Result{};
      if (!StringRef(*ValueOr).getAsInteger(10, Result))
        return Result;
      return llvm::make_error<UnparseableIntegerOptionError>(
          (IsGlobal ? LocalName.str() : (NamePrefix + LocalName).str()),
          *ValueOr);
    }

    /// Read a named option from the ``Context`` and parse it as an
@@ -164,15 +301,128 @@ public:
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either, returns Default.
    /// present either or it can't be parsed as a ``T``, returns \p Default.
    template <typename T>
    std::enable_if_t<std::is_integral<T>::value, T>
    getLocalOrGlobal(StringRef LocalName, T Default) const {
      std::string Value = getLocalOrGlobal(LocalName, "");
      T Result = Default;
      if (!Value.empty())
        StringRef(Value).getAsInteger(10, Result);
      return Result;
      if (llvm::Expected<T> ValueOr = getLocalOrGlobal<T>(LocalName))
        return *ValueOr;
      else
        logErrToStdErr(ValueOr.takeError());
      return Default;
    }

    /// Read a named option from the ``Context`` and parse it as a bool.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present, returns
    /// a ``MissingOptionError``. If the corresponding key can't be parsed as
    /// a bool, return an ``UnparseableIntegerOptionError``.
    template <> llvm::Expected<bool> get<bool>(StringRef LocalName) const;

    /// Read a named option from the ``Context`` and parse it as a bool.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present or it can't be
    /// parsed as a bool, returns \p Default.
    template <> bool get<bool>(StringRef LocalName, bool Default) const;

    /// Read a named option from the ``Context`` and parse it as a bool.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either, returns a ``MissingOptionError``. If the corresponding
    /// key can't be parsed as a bool, return an
    /// ``UnparseableIntegerOptionError``.
    template <>
    llvm::Expected<bool> getLocalOrGlobal<bool>(StringRef LocalName) const;

    /// Read a named option from the ``Context`` and parse it as a bool.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either or it can't be parsed as a bool, returns \p Default.
    template <>
    bool getLocalOrGlobal<bool>(StringRef LocalName, bool Default) const;

    /// Read a named option from the ``Context`` and parse it as an
    /// enum type ``T`` using the \p Mapping provided. If \p IgnoreCase is set,
    /// it will search the mapping ignoring the case.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present, returns a
    /// ``MissingOptionError``. If the key can't be parsed as a ``T`` returns a
    /// ``UnparseableEnumOptionError``.
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, llvm::Expected<T>>
    get(StringRef LocalName, ArrayRef<std::pair<StringRef, T>> Mapping,
        bool IgnoreCase = false) {
      if (llvm::Expected<int64_t> ValueOr = getEnumInt(
              LocalName, typeEraseMapping(Mapping), false, IgnoreCase))
        return static_cast<T>(*ValueOr);
      else
        return std::move(ValueOr.takeError());
    }

    /// Read a named option from the ``Context`` and parse it as an
    /// enum type ``T`` using the \p Mapping provided. If \p IgnoreCase is set,
    /// it will search the mapping ignoring the case.
    ///
    /// Reads the option with the check-local name \p LocalName from the
    /// ``CheckOptions``. If the corresponding key is not present or it can't be
    /// parsed as a ``T``, returns \p Default.
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, T>
    get(StringRef LocalName, ArrayRef<std::pair<StringRef, T>> Mapping,
        T Default, bool IgnoreCase = false) {
      if (auto ValueOr = get(LocalName, Mapping, IgnoreCase))
        return *ValueOr;
      else
        logErrToStdErr(ValueOr.takeError());
      return Default;
    }

    /// Read a named option from the ``Context`` and parse it as an
    /// enum type ``T`` using the \p Mapping provided. If \p IgnoreCase is set,
    /// it will search the mapping ignoring the case.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either, returns a ``MissingOptionError``. If the key can't be
    /// parsed as a ``T`` returns a ``UnparseableEnumOptionError``.
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, llvm::Expected<T>>
    getLocalOrGlobal(StringRef LocalName,
                     ArrayRef<std::pair<StringRef, T>> Mapping,
                     bool IgnoreCase = false) {
      if (llvm::Expected<int64_t> ValueOr = getEnumInt(
              LocalName, typeEraseMapping(Mapping), true, IgnoreCase))
        return static_cast<T>(*ValueOr);
      else
        return std::move(ValueOr.takeError());
    }

    /// Read a named option from the ``Context`` and parse it as an
    /// enum type ``T`` using the \p Mapping provided. If \p IgnoreCase is set,
    /// it will search the mapping ignoring the case.
    ///
    /// Reads the option with the check-local name \p LocalName from local or
    /// global ``CheckOptions``. Gets local option first. If local is not
    /// present, falls back to get global option. If global option is not
    /// present either or it can't be parsed as a ``T``, returns \p Default.
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, T>
    getLocalOrGlobal(StringRef LocalName,
                     ArrayRef<std::pair<StringRef, T>> Mapping, T Default,
                     bool IgnoreCase = false) {
      if (auto ValueOr = getLocalOrGlobal(LocalName, Mapping, IgnoreCase))
        return *ValueOr;
      else
        logErrToStdErr(ValueOr.takeError());
      return Default;
    }

    /// Stores an option with the check-local name \p LocalName with
@@ -185,7 +435,41 @@ public:
    void store(ClangTidyOptions::OptionMap &Options, StringRef LocalName,
               int64_t Value) const;

    /// Stores an option with the check-local name \p LocalName as the string
    /// representation of the Enum \p Value using the \p Mapping to \p Options.
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value>
    store(ClangTidyOptions::OptionMap &Options, StringRef LocalName, T Value,
          ArrayRef<std::pair<StringRef, T>> Mapping) {
      auto Iter = llvm::find_if(
          Mapping, [&](const std::pair<StringRef, T> &NameAndEnum) {
            return NameAndEnum.second == Value;
          });
      assert(Iter != Mapping.end() && "Unknown Case Value");
      store(Options, LocalName, Iter->first);
    }

  private:
    using NameAndValue = std::pair<StringRef, int64_t>;

    llvm::Expected<int64_t> getEnumInt(StringRef LocalName,
                                       ArrayRef<NameAndValue> Mapping,
                                       bool CheckGlobal, bool IgnoreCase);

    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, std::vector<NameAndValue>>
    typeEraseMapping(ArrayRef<std::pair<StringRef, T>> Mapping) {
      std::vector<NameAndValue> Result;
      Result.reserve(Mapping.size());
      for (auto &MappedItem : Mapping) {
        Result.emplace_back(MappedItem.first,
                            static_cast<int64_t>(MappedItem.second));
      }
      return Result;
    }

    static void logErrToStdErr(llvm::Error &&Err);

    std::string NamePrefix;
    const ClangTidyOptions::OptionMap &CheckOptions;
  };
+169 −1

File changed.

Preview size limit exceeded, changes collapsed.