Carlos Galvez 5b37cddff8 [clang-tidy] Introduce HeaderFileExtensions and ImplementationFileExtensions options
Re-introduce the patch that was reverted previously.
In the first attempt, the checks would not be able to
read from the global option, since getLocalOrGlobal
only works with string types. Additional logic is needed
in order to support both use cases in the transition
period. All that logic will be removed when the local
options are fully removed.

We have a number of checks designed to analyze problems
in header files only, for example:

bugprone-suspicious-include
google-build-namespaces
llvm-header-guard
misc-definitions-in-header
...

All these checks duplicate the same logic and options
to determine whether a location is placed in the main
source file or in the header. More checks are coming
up with similar requirements.

Thus, to remove duplication, let's move this option
to the top-level configuration of clang-tidy (since
it's something all checks should share).

Add a deprecation notice for all checks that use the
local option, prompting to update to the global option.

Differential Revision: https://reviews.llvm.org/D142655
2023-02-19 13:44:11 +00:00

453 lines
18 KiB
C++

#include "ClangTidyOptions.h"
#include "ClangTidyCheck.h"
#include "ClangTidyDiagnosticConsumer.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/ScopedPrinter.h"
#include "llvm/Testing/Annotations/Annotations.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <optional>
namespace clang {
namespace tidy {
enum class Colours { Red, Orange, Yellow, Green, Blue, Indigo, Violet };
template <> struct OptionEnumMapping<Colours> {
static llvm::ArrayRef<std::pair<Colours, StringRef>> getEnumMapping() {
static constexpr std::pair<Colours, StringRef> Mapping[] = {
{Colours::Red, "Red"}, {Colours::Orange, "Orange"},
{Colours::Yellow, "Yellow"}, {Colours::Green, "Green"},
{Colours::Blue, "Blue"}, {Colours::Indigo, "Indigo"},
{Colours::Violet, "Violet"}};
return ArrayRef(Mapping);
}
};
namespace test {
TEST(ParseLineFilter, EmptyFilter) {
ClangTidyGlobalOptions Options;
EXPECT_FALSE(parseLineFilter("", Options));
EXPECT_TRUE(Options.LineFilter.empty());
EXPECT_FALSE(parseLineFilter("[]", Options));
EXPECT_TRUE(Options.LineFilter.empty());
}
TEST(ParseLineFilter, InvalidFilter) {
ClangTidyGlobalOptions Options;
EXPECT_TRUE(!!parseLineFilter("asdf", Options));
EXPECT_TRUE(Options.LineFilter.empty());
EXPECT_TRUE(!!parseLineFilter("[{}]", Options));
EXPECT_TRUE(!!parseLineFilter("[{\"name\":\"\"}]", Options));
EXPECT_TRUE(
!!parseLineFilter("[{\"name\":\"test\",\"lines\":[[1]]}]", Options));
EXPECT_TRUE(
!!parseLineFilter("[{\"name\":\"test\",\"lines\":[[1,2,3]]}]", Options));
EXPECT_TRUE(
!!parseLineFilter("[{\"name\":\"test\",\"lines\":[[1,-1]]}]", Options));
}
TEST(ParseLineFilter, ValidFilter) {
ClangTidyGlobalOptions Options;
std::error_code Error = parseLineFilter(
"[{\"name\":\"file1.cpp\",\"lines\":[[3,15],[20,30],[42,42]]},"
"{\"name\":\"file2.h\"},"
"{\"name\":\"file3.cc\",\"lines\":[[100,1000]]}]",
Options);
EXPECT_FALSE(Error);
EXPECT_EQ(3u, Options.LineFilter.size());
EXPECT_EQ("file1.cpp", Options.LineFilter[0].Name);
EXPECT_EQ(3u, Options.LineFilter[0].LineRanges.size());
EXPECT_EQ(3u, Options.LineFilter[0].LineRanges[0].first);
EXPECT_EQ(15u, Options.LineFilter[0].LineRanges[0].second);
EXPECT_EQ(20u, Options.LineFilter[0].LineRanges[1].first);
EXPECT_EQ(30u, Options.LineFilter[0].LineRanges[1].second);
EXPECT_EQ(42u, Options.LineFilter[0].LineRanges[2].first);
EXPECT_EQ(42u, Options.LineFilter[0].LineRanges[2].second);
EXPECT_EQ("file2.h", Options.LineFilter[1].Name);
EXPECT_EQ(0u, Options.LineFilter[1].LineRanges.size());
EXPECT_EQ("file3.cc", Options.LineFilter[2].Name);
EXPECT_EQ(1u, Options.LineFilter[2].LineRanges.size());
EXPECT_EQ(100u, Options.LineFilter[2].LineRanges[0].first);
EXPECT_EQ(1000u, Options.LineFilter[2].LineRanges[0].second);
}
TEST(ParseConfiguration, ValidConfiguration) {
llvm::ErrorOr<ClangTidyOptions> Options =
parseConfiguration(llvm::MemoryBufferRef(
"Checks: \"-*,misc-*\"\n"
"HeaderFileExtensions: [\"\",\"h\",\"hh\",\"hpp\",\"hxx\"]\n"
"ImplementationFileExtensions: [\"c\",\"cc\",\"cpp\",\"cxx\"]\n"
"HeaderFilterRegex: \".*\"\n"
"AnalyzeTemporaryDtors: true\n"
"User: some.user",
"Options"));
EXPECT_TRUE(!!Options);
EXPECT_EQ("-*,misc-*", *Options->Checks);
EXPECT_EQ(std::vector<std::string>({"", "h", "hh", "hpp", "hxx"}),
*Options->HeaderFileExtensions);
EXPECT_EQ(std::vector<std::string>({"c", "cc", "cpp", "cxx"}),
*Options->ImplementationFileExtensions);
EXPECT_EQ(".*", *Options->HeaderFilterRegex);
EXPECT_EQ("some.user", *Options->User);
}
TEST(ParseConfiguration, ChecksSeparatedByNewlines) {
auto MemoryBuffer = llvm::MemoryBufferRef("Checks: |\n"
" -*,misc-*\n"
" llvm-*\n"
" -clang-*,\n"
" google-*",
"Options");
auto Options = parseConfiguration(MemoryBuffer);
EXPECT_TRUE(!!Options);
EXPECT_EQ("-*,misc-*\nllvm-*\n-clang-*,\ngoogle-*\n", *Options->Checks);
}
TEST(ParseConfiguration, MergeConfigurations) {
llvm::ErrorOr<ClangTidyOptions> Options1 =
parseConfiguration(llvm::MemoryBufferRef(R"(
Checks: "check1,check2"
HeaderFileExtensions: ["h","hh"]
ImplementationFileExtensions: ["c","cc"]
HeaderFilterRegex: "filter1"
AnalyzeTemporaryDtors: true
User: user1
ExtraArgs: ['arg1', 'arg2']
ExtraArgsBefore: ['arg-before1', 'arg-before2']
UseColor: false
)",
"Options1"));
ASSERT_TRUE(!!Options1);
llvm::ErrorOr<ClangTidyOptions> Options2 =
parseConfiguration(llvm::MemoryBufferRef(R"(
Checks: "check3,check4"
HeaderFileExtensions: ["hpp","hxx"]
ImplementationFileExtensions: ["cpp","cxx"]
HeaderFilterRegex: "filter2"
AnalyzeTemporaryDtors: false
User: user2
ExtraArgs: ['arg3', 'arg4']
ExtraArgsBefore: ['arg-before3', 'arg-before4']
UseColor: true
)",
"Options2"));
ASSERT_TRUE(!!Options2);
ClangTidyOptions Options = Options1->merge(*Options2, 0);
EXPECT_EQ("check1,check2,check3,check4", *Options.Checks);
EXPECT_EQ(std::vector<std::string>({"hpp", "hxx"}),
*Options.HeaderFileExtensions);
EXPECT_EQ(std::vector<std::string>({"cpp", "cxx"}),
*Options.ImplementationFileExtensions);
EXPECT_EQ("filter2", *Options.HeaderFilterRegex);
EXPECT_EQ("user2", *Options.User);
ASSERT_TRUE(Options.ExtraArgs.has_value());
EXPECT_EQ("arg1,arg2,arg3,arg4", llvm::join(Options.ExtraArgs->begin(),
Options.ExtraArgs->end(), ","));
ASSERT_TRUE(Options.ExtraArgsBefore.has_value());
EXPECT_EQ("arg-before1,arg-before2,arg-before3,arg-before4",
llvm::join(Options.ExtraArgsBefore->begin(),
Options.ExtraArgsBefore->end(), ","));
ASSERT_TRUE(Options.UseColor.has_value());
EXPECT_TRUE(*Options.UseColor);
}
namespace {
class DiagCollecter {
public:
struct Diag {
private:
static size_t posToOffset(const llvm::SMLoc Loc,
const llvm::SourceMgr *Src) {
return Loc.getPointer() -
Src->getMemoryBuffer(Src->FindBufferContainingLoc(Loc))
->getBufferStart();
}
public:
Diag(const llvm::SMDiagnostic &D)
: Message(D.getMessage()), Kind(D.getKind()),
Pos(posToOffset(D.getLoc(), D.getSourceMgr())) {
if (!D.getRanges().empty()) {
// Ranges are stored as column numbers on the line that has the error.
unsigned Offset = Pos - D.getColumnNo();
Range.emplace();
Range->Begin = Offset + D.getRanges().front().first,
Range->End = Offset + D.getRanges().front().second;
}
}
std::string Message;
llvm::SourceMgr::DiagKind Kind;
size_t Pos;
std::optional<llvm::Annotations::Range> Range;
friend void PrintTo(const Diag &D, std::ostream *OS) {
*OS << (D.Kind == llvm::SourceMgr::DK_Error ? "error: " : "warning: ")
<< D.Message << "@" << llvm::to_string(D.Pos);
if (D.Range)
*OS << ":[" << D.Range->Begin << ", " << D.Range->End << ")";
}
};
DiagCollecter() = default;
DiagCollecter(const DiagCollecter &) = delete;
std::function<void(const llvm::SMDiagnostic &)>
getCallback(bool Clear = true) & {
if (Clear)
Diags.clear();
return [&](const llvm::SMDiagnostic &Diag) { Diags.emplace_back(Diag); };
}
std::function<void(const llvm::SMDiagnostic &)>
getCallback(bool Clear = true) && = delete;
llvm::ArrayRef<Diag> getDiags() const { return Diags; }
private:
std::vector<Diag> Diags;
};
MATCHER_P(DiagMessage, M, "") { return arg.Message == M; }
MATCHER_P(DiagKind, K, "") { return arg.Kind == K; }
MATCHER_P(DiagPos, P, "") { return arg.Pos == P; }
MATCHER_P(DiagRange, P, "") { return arg.Range && *arg.Range == P; }
} // namespace
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::UnorderedElementsAre;
TEST(ParseConfiguration, CollectDiags) {
DiagCollecter Collector;
auto ParseWithDiags = [&](llvm::StringRef Buffer) {
return parseConfigurationWithDiags(llvm::MemoryBufferRef(Buffer, "Options"),
Collector.getCallback());
};
llvm::Annotations Options(R"(
[[Check]]: llvm-include-order
)");
llvm::ErrorOr<ClangTidyOptions> ParsedOpt = ParseWithDiags(Options.code());
EXPECT_TRUE(!ParsedOpt);
EXPECT_THAT(Collector.getDiags(),
testing::ElementsAre(AllOf(DiagMessage("unknown key 'Check'"),
DiagKind(llvm::SourceMgr::DK_Error),
DiagPos(Options.range().Begin),
DiagRange(Options.range()))));
Options = llvm::Annotations(R"(
UseColor: [[NotABool]]
)");
ParsedOpt = ParseWithDiags(Options.code());
EXPECT_TRUE(!ParsedOpt);
EXPECT_THAT(Collector.getDiags(),
testing::ElementsAre(AllOf(DiagMessage("invalid boolean"),
DiagKind(llvm::SourceMgr::DK_Error),
DiagPos(Options.range().Begin),
DiagRange(Options.range()))));
}
namespace {
class TestCheck : public ClangTidyCheck {
public:
TestCheck(ClangTidyContext *Context) : ClangTidyCheck("test", Context) {}
template <typename... Args> auto getLocal(Args &&... Arguments) {
return Options.get(std::forward<Args>(Arguments)...);
}
template <typename... Args> auto getGlobal(Args &&... Arguments) {
return Options.getLocalOrGlobal(std::forward<Args>(Arguments)...);
}
template <typename IntType = int, typename... Args>
auto getIntLocal(Args &&... Arguments) {
return Options.get<IntType>(std::forward<Args>(Arguments)...);
}
template <typename IntType = int, typename... Args>
auto getIntGlobal(Args &&... Arguments) {
return Options.getLocalOrGlobal<IntType>(std::forward<Args>(Arguments)...);
}
};
#define CHECK_VAL(Value, Expected) \
do { \
auto Item = Value; \
ASSERT_TRUE(!!Item); \
EXPECT_EQ(*Item, Expected); \
} while (false)
MATCHER_P(ToolDiagMessage, M, "") { return arg.Message.Message == M; }
MATCHER_P(ToolDiagLevel, L, "") { return arg.DiagLevel == L; }
} // namespace
} // namespace test
static constexpr auto Warning = tooling::Diagnostic::Warning;
static constexpr auto Error = tooling::Diagnostic::Error;
static void PrintTo(const ClangTidyError &Err, ::std::ostream *OS) {
*OS << (Err.DiagLevel == Error ? "error: " : "warning: ")
<< Err.Message.Message;
}
namespace test {
TEST(CheckOptionsValidation, MissingOptions) {
ClangTidyOptions Options;
ClangTidyContext Context(std::make_unique<DefaultOptionsProvider>(
ClangTidyGlobalOptions(), Options));
ClangTidyDiagnosticConsumer DiagConsumer(Context);
DiagnosticsEngine DE(new DiagnosticIDs(), new DiagnosticOptions,
&DiagConsumer, false);
Context.setDiagnosticsEngine(&DE);
TestCheck TestCheck(&Context);
EXPECT_FALSE(TestCheck.getLocal("Opt"));
EXPECT_EQ(TestCheck.getLocal("Opt", "Unknown"), "Unknown");
// Missing options aren't errors.
EXPECT_TRUE(DiagConsumer.take().empty());
}
TEST(CheckOptionsValidation, ValidIntOptions) {
ClangTidyOptions Options;
auto &CheckOptions = Options.CheckOptions;
CheckOptions["test.IntExpected"] = "1";
CheckOptions["test.IntInvalid1"] = "1WithMore";
CheckOptions["test.IntInvalid2"] = "NoInt";
CheckOptions["GlobalIntExpected"] = "1";
CheckOptions["GlobalIntInvalid"] = "NoInt";
CheckOptions["test.DefaultedIntInvalid"] = "NoInt";
CheckOptions["test.BoolITrueValue"] = "1";
CheckOptions["test.BoolIFalseValue"] = "0";
CheckOptions["test.BoolTrueValue"] = "true";
CheckOptions["test.BoolFalseValue"] = "false";
CheckOptions["test.BoolTrueShort"] = "Y";
CheckOptions["test.BoolFalseShort"] = "N";
CheckOptions["test.BoolUnparseable"] = "Nothing";
ClangTidyContext Context(std::make_unique<DefaultOptionsProvider>(
ClangTidyGlobalOptions(), Options));
ClangTidyDiagnosticConsumer DiagConsumer(Context);
DiagnosticsEngine DE(new DiagnosticIDs(), new DiagnosticOptions,
&DiagConsumer, false);
Context.setDiagnosticsEngine(&DE);
TestCheck TestCheck(&Context);
CHECK_VAL(TestCheck.getIntLocal("IntExpected"), 1);
CHECK_VAL(TestCheck.getIntGlobal("GlobalIntExpected"), 1);
EXPECT_FALSE(TestCheck.getIntLocal("IntInvalid1").has_value());
EXPECT_FALSE(TestCheck.getIntLocal("IntInvalid2").has_value());
EXPECT_FALSE(TestCheck.getIntGlobal("GlobalIntInvalid").has_value());
ASSERT_EQ(TestCheck.getIntLocal("DefaultedIntInvalid", 1), 1);
CHECK_VAL(TestCheck.getIntLocal<bool>("BoolITrueValue"), true);
CHECK_VAL(TestCheck.getIntLocal<bool>("BoolIFalseValue"), false);
CHECK_VAL(TestCheck.getIntLocal<bool>("BoolTrueValue"), true);
CHECK_VAL(TestCheck.getIntLocal<bool>("BoolFalseValue"), false);
CHECK_VAL(TestCheck.getIntLocal<bool>("BoolTrueShort"), true);
CHECK_VAL(TestCheck.getIntLocal<bool>("BoolFalseShort"), false);
EXPECT_FALSE(TestCheck.getIntLocal<bool>("BoolUnparseable"));
EXPECT_THAT(
DiagConsumer.take(),
UnorderedElementsAre(
AllOf(ToolDiagMessage(
"invalid configuration value '1WithMore' for option "
"'test.IntInvalid1'; expected an integer"),
ToolDiagLevel(Warning)),
AllOf(
ToolDiagMessage("invalid configuration value 'NoInt' for option "
"'test.IntInvalid2'; expected an integer"),
ToolDiagLevel(Warning)),
AllOf(
ToolDiagMessage("invalid configuration value 'NoInt' for option "
"'GlobalIntInvalid'; expected an integer"),
ToolDiagLevel(Warning)),
AllOf(ToolDiagMessage(
"invalid configuration value 'NoInt' for option "
"'test.DefaultedIntInvalid'; expected an integer"),
ToolDiagLevel(Warning)),
AllOf(ToolDiagMessage(
"invalid configuration value 'Nothing' for option "
"'test.BoolUnparseable'; expected a bool"),
ToolDiagLevel(Warning))));
}
TEST(ValidConfiguration, ValidEnumOptions) {
ClangTidyOptions Options;
auto &CheckOptions = Options.CheckOptions;
CheckOptions["test.Valid"] = "Red";
CheckOptions["test.Invalid"] = "Scarlet";
CheckOptions["test.ValidWrongCase"] = "rED";
CheckOptions["test.NearMiss"] = "Oragne";
CheckOptions["GlobalValid"] = "Violet";
CheckOptions["GlobalInvalid"] = "Purple";
CheckOptions["GlobalValidWrongCase"] = "vIOLET";
CheckOptions["GlobalNearMiss"] = "Yelow";
ClangTidyContext Context(std::make_unique<DefaultOptionsProvider>(
ClangTidyGlobalOptions(), Options));
ClangTidyDiagnosticConsumer DiagConsumer(Context);
DiagnosticsEngine DE(new DiagnosticIDs(), new DiagnosticOptions,
&DiagConsumer, false);
Context.setDiagnosticsEngine(&DE);
TestCheck TestCheck(&Context);
CHECK_VAL(TestCheck.getIntLocal<Colours>("Valid"), Colours::Red);
CHECK_VAL(TestCheck.getIntGlobal<Colours>("GlobalValid"), Colours::Violet);
CHECK_VAL(
TestCheck.getIntLocal<Colours>("ValidWrongCase", /*IgnoreCase*/ true),
Colours::Red);
CHECK_VAL(TestCheck.getIntGlobal<Colours>("GlobalValidWrongCase",
/*IgnoreCase*/ true),
Colours::Violet);
EXPECT_FALSE(TestCheck.getIntLocal<Colours>("ValidWrongCase").has_value());
EXPECT_FALSE(TestCheck.getIntLocal<Colours>("NearMiss").has_value());
EXPECT_FALSE(TestCheck.getIntGlobal<Colours>("GlobalInvalid").has_value());
EXPECT_FALSE(
TestCheck.getIntGlobal<Colours>("GlobalValidWrongCase").has_value());
EXPECT_FALSE(TestCheck.getIntGlobal<Colours>("GlobalNearMiss").has_value());
EXPECT_FALSE(TestCheck.getIntLocal<Colours>("Invalid").has_value());
EXPECT_THAT(
DiagConsumer.take(),
UnorderedElementsAre(
AllOf(ToolDiagMessage("invalid configuration value "
"'Scarlet' for option 'test.Invalid'"),
ToolDiagLevel(Warning)),
AllOf(ToolDiagMessage("invalid configuration value 'rED' for option "
"'test.ValidWrongCase'; did you mean 'Red'?"),
ToolDiagLevel(Warning)),
AllOf(
ToolDiagMessage("invalid configuration value 'Oragne' for option "
"'test.NearMiss'; did you mean 'Orange'?"),
ToolDiagLevel(Warning)),
AllOf(ToolDiagMessage("invalid configuration value "
"'Purple' for option 'GlobalInvalid'"),
ToolDiagLevel(Warning)),
AllOf(
ToolDiagMessage("invalid configuration value 'vIOLET' for option "
"'GlobalValidWrongCase'; did you mean 'Violet'?"),
ToolDiagLevel(Warning)),
AllOf(
ToolDiagMessage("invalid configuration value 'Yelow' for option "
"'GlobalNearMiss'; did you mean 'Yellow'?"),
ToolDiagLevel(Warning))));
}
#undef CHECK_VAL
} // namespace test
} // namespace tidy
} // namespace clang