| //===-- ConfigCompileTests.cpp --------------------------------------------===// |
| // |
| // 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 "Config.h" |
| #include "ConfigFragment.h" |
| #include "ConfigTesting.h" |
| #include "Feature.h" |
| #include "TestFS.h" |
| #include "clang/Basic/DiagnosticSema.h" |
| #include "llvm/ADT/None.h" |
| #include "llvm/ADT/Optional.h" |
| #include "llvm/ADT/StringRef.h" |
| #include "llvm/Support/Path.h" |
| #include "llvm/Support/SourceMgr.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include <string> |
| |
| namespace clang { |
| namespace clangd { |
| namespace config { |
| namespace { |
| using ::testing::AllOf; |
| using ::testing::Contains; |
| using ::testing::ElementsAre; |
| using ::testing::IsEmpty; |
| using ::testing::SizeIs; |
| using ::testing::StartsWith; |
| using ::testing::UnorderedElementsAre; |
| |
| class ConfigCompileTests : public ::testing::Test { |
| protected: |
| CapturedDiags Diags; |
| Config Conf; |
| Fragment Frag; |
| Params Parm; |
| |
| bool compileAndApply() { |
| Conf = Config(); |
| Diags.Diagnostics.clear(); |
| auto Compiled = std::move(Frag).compile(Diags.callback()); |
| return Compiled(Parm, Conf); |
| } |
| }; |
| |
| TEST_F(ConfigCompileTests, Condition) { |
| // No condition. |
| Frag = {}; |
| Frag.CompileFlags.Add.emplace_back("X"); |
| EXPECT_TRUE(compileAndApply()) << "Empty config"; |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(1)); |
| |
| // Regex with no file. |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back("fo*"); |
| EXPECT_FALSE(compileAndApply()); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(0)); |
| |
| // Following tests have a file path set. |
| Parm.Path = "bar"; |
| |
| // Non-matching regex. |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back("fo*"); |
| EXPECT_FALSE(compileAndApply()); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| |
| // Matching regex. |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back("fo*"); |
| Frag.If.PathMatch.emplace_back("ba*r"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| |
| // Excluded regex. |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back("b.*"); |
| Frag.If.PathExclude.emplace_back(".*r"); |
| EXPECT_FALSE(compileAndApply()) << "Included but also excluded"; |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| |
| // Invalid regex. |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back("**]@theu"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_THAT(Diags.Diagnostics, SizeIs(1)); |
| EXPECT_THAT(Diags.Diagnostics.front().Message, StartsWith("Invalid regex")); |
| |
| // Valid regex and unknown key. |
| Frag = {}; |
| Frag.If.HasUnrecognizedCondition = true; |
| Frag.If.PathMatch.emplace_back("ba*r"); |
| EXPECT_FALSE(compileAndApply()); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| |
| // Only matches case-insensitively. |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back("B.*R"); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| #ifdef CLANGD_PATH_CASE_INSENSITIVE |
| EXPECT_TRUE(compileAndApply()); |
| #else |
| EXPECT_FALSE(compileAndApply()); |
| #endif |
| |
| Frag = {}; |
| Frag.If.PathExclude.emplace_back("B.*R"); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| #ifdef CLANGD_PATH_CASE_INSENSITIVE |
| EXPECT_FALSE(compileAndApply()); |
| #else |
| EXPECT_TRUE(compileAndApply()); |
| #endif |
| } |
| |
| TEST_F(ConfigCompileTests, CompileCommands) { |
| Frag.CompileFlags.Add.emplace_back("-foo"); |
| Frag.CompileFlags.Remove.emplace_back("--include-directory="); |
| std::vector<std::string> Argv = {"clang", "-I", "bar/", "--", "a.cc"}; |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(2)); |
| for (auto &Edit : Conf.CompileFlags.Edits) |
| Edit(Argv); |
| EXPECT_THAT(Argv, ElementsAre("clang", "-foo", "--", "a.cc")); |
| } |
| |
| TEST_F(ConfigCompileTests, CompilationDatabase) { |
| Frag.CompileFlags.CompilationDatabase.emplace("None"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, |
| Config::CDBSearchSpec::NoCDBSearch); |
| |
| Frag.CompileFlags.CompilationDatabase.emplace("Ancestors"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, |
| Config::CDBSearchSpec::Ancestors); |
| |
| // Relative path not allowed without directory set. |
| Frag.CompileFlags.CompilationDatabase.emplace("Something"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, |
| Config::CDBSearchSpec::Ancestors) |
| << "default value"; |
| EXPECT_THAT(Diags.Diagnostics, |
| ElementsAre(DiagMessage( |
| "CompilationDatabase must be an absolute path, because this " |
| "fragment is not associated with any directory."))); |
| |
| // Relative path allowed if directory is set. |
| Frag.Source.Directory = testRoot(); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, |
| Config::CDBSearchSpec::FixedDir); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.FixedCDBPath, testPath("Something")); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| |
| // Absolute path allowed. |
| Frag.Source.Directory.clear(); |
| Frag.CompileFlags.CompilationDatabase.emplace(testPath("Something2")); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, |
| Config::CDBSearchSpec::FixedDir); |
| EXPECT_EQ(Conf.CompileFlags.CDBSearch.FixedCDBPath, testPath("Something2")); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| } |
| |
| TEST_F(ConfigCompileTests, Index) { |
| Frag.Index.Background.emplace("Skip"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.Index.Background, Config::BackgroundPolicy::Skip); |
| |
| Frag = {}; |
| Frag.Index.Background.emplace("Foo"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.Index.Background, Config::BackgroundPolicy::Build) |
| << "by default"; |
| EXPECT_THAT( |
| Diags.Diagnostics, |
| ElementsAre(DiagMessage( |
| "Invalid Background value 'Foo'. Valid values are Build, Skip."))); |
| } |
| |
| TEST_F(ConfigCompileTests, PathSpecMatch) { |
| auto BarPath = llvm::sys::path::convert_to_slash(testPath("foo/bar.h")); |
| Parm.Path = BarPath; |
| |
| struct { |
| std::string Directory; |
| std::string PathSpec; |
| bool ShouldMatch; |
| } Cases[] = { |
| { |
| // Absolute path matches. |
| "", |
| llvm::sys::path::convert_to_slash(testPath("foo/bar.h")), |
| true, |
| }, |
| { |
| // Absolute path fails. |
| "", |
| llvm::sys::path::convert_to_slash(testPath("bar/bar.h")), |
| false, |
| }, |
| { |
| // Relative should fail to match as /foo/bar.h doesn't reside under |
| // /baz/. |
| testPath("baz"), |
| "bar\\.h", |
| false, |
| }, |
| { |
| // Relative should pass with /foo as directory. |
| testPath("foo"), |
| "bar\\.h", |
| true, |
| }, |
| }; |
| |
| // PathMatch |
| for (const auto &Case : Cases) { |
| Frag = {}; |
| Frag.If.PathMatch.emplace_back(Case.PathSpec); |
| Frag.Source.Directory = Case.Directory; |
| EXPECT_EQ(compileAndApply(), Case.ShouldMatch); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| } |
| |
| // PathEclude |
| for (const auto &Case : Cases) { |
| SCOPED_TRACE(Case.Directory); |
| SCOPED_TRACE(Case.PathSpec); |
| Frag = {}; |
| Frag.If.PathExclude.emplace_back(Case.PathSpec); |
| Frag.Source.Directory = Case.Directory; |
| EXPECT_NE(compileAndApply(), Case.ShouldMatch); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| } |
| } |
| |
| TEST_F(ConfigCompileTests, DiagnosticsIncludeCleaner) { |
| // Defaults to None. |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, |
| Config::UnusedIncludesPolicy::None); |
| |
| Frag = {}; |
| Frag.Diagnostics.UnusedIncludes.emplace("None"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, |
| Config::UnusedIncludesPolicy::None); |
| |
| Frag = {}; |
| Frag.Diagnostics.UnusedIncludes.emplace("Strict"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, |
| Config::UnusedIncludesPolicy::Strict); |
| } |
| |
| TEST_F(ConfigCompileTests, DiagnosticSuppression) { |
| Frag.Diagnostics.Suppress.emplace_back("bugprone-use-after-move"); |
| Frag.Diagnostics.Suppress.emplace_back("unreachable-code"); |
| Frag.Diagnostics.Suppress.emplace_back("-Wunused-variable"); |
| Frag.Diagnostics.Suppress.emplace_back("typecheck_bool_condition"); |
| Frag.Diagnostics.Suppress.emplace_back("err_unexpected_friend"); |
| Frag.Diagnostics.Suppress.emplace_back("warn_alloca"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_THAT(Conf.Diagnostics.Suppress.keys(), |
| UnorderedElementsAre("bugprone-use-after-move", |
| "unreachable-code", "unused-variable", |
| "typecheck_bool_condition", |
| "unexpected_friend", "warn_alloca")); |
| EXPECT_TRUE(isBuiltinDiagnosticSuppressed(diag::warn_unreachable, |
| Conf.Diagnostics.Suppress)); |
| // Subcategory not respected/suppressed. |
| EXPECT_FALSE(isBuiltinDiagnosticSuppressed(diag::warn_unreachable_break, |
| Conf.Diagnostics.Suppress)); |
| EXPECT_TRUE(isBuiltinDiagnosticSuppressed(diag::warn_unused_variable, |
| Conf.Diagnostics.Suppress)); |
| EXPECT_TRUE(isBuiltinDiagnosticSuppressed(diag::err_typecheck_bool_condition, |
| Conf.Diagnostics.Suppress)); |
| EXPECT_TRUE(isBuiltinDiagnosticSuppressed(diag::err_unexpected_friend, |
| Conf.Diagnostics.Suppress)); |
| EXPECT_TRUE(isBuiltinDiagnosticSuppressed(diag::warn_alloca, |
| Conf.Diagnostics.Suppress)); |
| |
| Frag.Diagnostics.Suppress.emplace_back("*"); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_TRUE(Conf.Diagnostics.SuppressAll); |
| EXPECT_THAT(Conf.Diagnostics.Suppress, IsEmpty()); |
| } |
| |
| TEST_F(ConfigCompileTests, Tidy) { |
| auto &Tidy = Frag.Diagnostics.ClangTidy; |
| Tidy.Add.emplace_back("bugprone-use-after-move"); |
| Tidy.Add.emplace_back("llvm-*"); |
| Tidy.Remove.emplace_back("llvm-include-order"); |
| Tidy.Remove.emplace_back("readability-*"); |
| Tidy.CheckOptions.emplace_back( |
| std::make_pair(std::string("StrictMode"), std::string("true"))); |
| Tidy.CheckOptions.emplace_back(std::make_pair( |
| std::string("example-check.ExampleOption"), std::string("0"))); |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_EQ(Conf.Diagnostics.ClangTidy.CheckOptions.size(), 2U); |
| EXPECT_EQ(Conf.Diagnostics.ClangTidy.CheckOptions.lookup("StrictMode"), |
| "true"); |
| EXPECT_EQ(Conf.Diagnostics.ClangTidy.CheckOptions.lookup( |
| "example-check.ExampleOption"), |
| "0"); |
| #if CLANGD_TIDY_CHECKS |
| EXPECT_EQ( |
| Conf.Diagnostics.ClangTidy.Checks, |
| "bugprone-use-after-move,llvm-*,-llvm-include-order,-readability-*"); |
| EXPECT_THAT(Diags.Diagnostics, IsEmpty()); |
| #else // !CLANGD_TIDY_CHECKS |
| EXPECT_EQ(Conf.Diagnostics.ClangTidy.Checks, "llvm-*,-readability-*"); |
| EXPECT_THAT( |
| Diags.Diagnostics, |
| ElementsAre( |
| DiagMessage( |
| "clang-tidy check 'bugprone-use-after-move' was not found"), |
| DiagMessage("clang-tidy check 'llvm-include-order' was not found"))); |
| #endif |
| } |
| |
| TEST_F(ConfigCompileTests, TidyBadChecks) { |
| auto &Tidy = Frag.Diagnostics.ClangTidy; |
| Tidy.Add.emplace_back("unknown-check"); |
| Tidy.Remove.emplace_back("*"); |
| Tidy.Remove.emplace_back("llvm-includeorder"); |
| EXPECT_TRUE(compileAndApply()); |
| // Ensure bad checks are stripped from the glob. |
| EXPECT_EQ(Conf.Diagnostics.ClangTidy.Checks, "-*"); |
| EXPECT_THAT( |
| Diags.Diagnostics, |
| ElementsAre( |
| AllOf(DiagMessage("clang-tidy check 'unknown-check' was not found"), |
| DiagKind(llvm::SourceMgr::DK_Warning)), |
| AllOf( |
| DiagMessage("clang-tidy check 'llvm-includeorder' was not found"), |
| DiagKind(llvm::SourceMgr::DK_Warning)))); |
| } |
| |
| TEST_F(ConfigCompileTests, ExternalServerNeedsTrusted) { |
| Fragment::IndexBlock::ExternalBlock External; |
| External.Server.emplace("xxx"); |
| Frag.Index.External = std::move(External); |
| compileAndApply(); |
| EXPECT_THAT( |
| Diags.Diagnostics, |
| ElementsAre(DiagMessage( |
| "Remote index may not be specified by untrusted configuration. " |
| "Copy this into user config to use it."))); |
| EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| } |
| |
| TEST_F(ConfigCompileTests, ExternalBlockWarnOnMultipleSource) { |
| Frag.Source.Trusted = true; |
| Fragment::IndexBlock::ExternalBlock External; |
| External.File.emplace(""); |
| External.Server.emplace(""); |
| Frag.Index.External = std::move(External); |
| compileAndApply(); |
| #ifdef CLANGD_ENABLE_REMOTE |
| EXPECT_THAT( |
| Diags.Diagnostics, |
| Contains( |
| AllOf(DiagMessage("Exactly one of File, Server or None must be set."), |
| DiagKind(llvm::SourceMgr::DK_Error)))); |
| #else |
| ASSERT_TRUE(Conf.Index.External.hasValue()); |
| EXPECT_EQ(Conf.Index.External->Kind, Config::ExternalIndexSpec::File); |
| #endif |
| } |
| |
| TEST_F(ConfigCompileTests, ExternalBlockDisableWithNone) { |
| compileAndApply(); |
| EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| |
| Fragment::IndexBlock::ExternalBlock External; |
| External.IsNone = true; |
| Frag.Index.External = std::move(External); |
| compileAndApply(); |
| EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| } |
| |
| TEST_F(ConfigCompileTests, ExternalBlockErrOnNoSource) { |
| Frag.Index.External.emplace(Fragment::IndexBlock::ExternalBlock{}); |
| compileAndApply(); |
| EXPECT_THAT( |
| Diags.Diagnostics, |
| Contains( |
| AllOf(DiagMessage("Exactly one of File, Server or None must be set."), |
| DiagKind(llvm::SourceMgr::DK_Error)))); |
| } |
| |
| TEST_F(ConfigCompileTests, ExternalBlockDisablesBackgroundIndex) { |
| auto BazPath = testPath("foo/bar/baz.h", llvm::sys::path::Style::posix); |
| Parm.Path = BazPath; |
| Frag.Index.Background.emplace("Build"); |
| Fragment::IndexBlock::ExternalBlock External; |
| External.File.emplace(testPath("foo")); |
| External.MountPoint.emplace( |
| testPath("foo/bar", llvm::sys::path::Style::posix)); |
| Frag.Index.External = std::move(External); |
| compileAndApply(); |
| EXPECT_EQ(Conf.Index.Background, Config::BackgroundPolicy::Skip); |
| } |
| |
| TEST_F(ConfigCompileTests, ExternalBlockMountPoint) { |
| auto GetFrag = [](llvm::StringRef Directory, |
| llvm::Optional<const char *> MountPoint) { |
| Fragment Frag; |
| Frag.Source.Directory = Directory.str(); |
| Fragment::IndexBlock::ExternalBlock External; |
| External.File.emplace(testPath("foo")); |
| if (MountPoint) |
| External.MountPoint.emplace(*MountPoint); |
| Frag.Index.External = std::move(External); |
| return Frag; |
| }; |
| |
| auto BarPath = testPath("foo/bar.h", llvm::sys::path::Style::posix); |
| BarPath = llvm::sys::path::convert_to_slash(BarPath); |
| Parm.Path = BarPath; |
| // Non-absolute MountPoint without a directory raises an error. |
| Frag = GetFrag("", "foo"); |
| compileAndApply(); |
| ASSERT_THAT( |
| Diags.Diagnostics, |
| ElementsAre( |
| AllOf(DiagMessage("MountPoint must be an absolute path, because this " |
| "fragment is not associated with any directory."), |
| DiagKind(llvm::SourceMgr::DK_Error)))); |
| EXPECT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| |
| auto FooPath = testPath("foo/", llvm::sys::path::Style::posix); |
| FooPath = llvm::sys::path::convert_to_slash(FooPath); |
| // Ok when relative. |
| Frag = GetFrag(testRoot(), "foo/"); |
| compileAndApply(); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File); |
| EXPECT_THAT(Conf.Index.External.MountPoint, FooPath); |
| |
| // None defaults to ".". |
| Frag = GetFrag(FooPath, llvm::None); |
| compileAndApply(); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File); |
| EXPECT_THAT(Conf.Index.External.MountPoint, FooPath); |
| |
| // Without a file, external index is empty. |
| Parm.Path = ""; |
| Frag = GetFrag("", FooPath.c_str()); |
| compileAndApply(); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| |
| // File outside MountPoint, no index. |
| auto BazPath = testPath("bar/baz.h", llvm::sys::path::Style::posix); |
| BazPath = llvm::sys::path::convert_to_slash(BazPath); |
| Parm.Path = BazPath; |
| Frag = GetFrag("", FooPath.c_str()); |
| compileAndApply(); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| |
| // File under MountPoint, index should be set. |
| BazPath = testPath("foo/baz.h", llvm::sys::path::Style::posix); |
| BazPath = llvm::sys::path::convert_to_slash(BazPath); |
| Parm.Path = BazPath; |
| Frag = GetFrag("", FooPath.c_str()); |
| compileAndApply(); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File); |
| EXPECT_THAT(Conf.Index.External.MountPoint, FooPath); |
| |
| // Only matches case-insensitively. |
| BazPath = testPath("fOo/baz.h", llvm::sys::path::Style::posix); |
| BazPath = llvm::sys::path::convert_to_slash(BazPath); |
| Parm.Path = BazPath; |
| |
| FooPath = testPath("FOO/", llvm::sys::path::Style::posix); |
| FooPath = llvm::sys::path::convert_to_slash(FooPath); |
| Frag = GetFrag("", FooPath.c_str()); |
| compileAndApply(); |
| ASSERT_THAT(Diags.Diagnostics, IsEmpty()); |
| #ifdef CLANGD_PATH_CASE_INSENSITIVE |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::File); |
| EXPECT_THAT(Conf.Index.External.MountPoint, FooPath); |
| #else |
| ASSERT_EQ(Conf.Index.External.Kind, Config::ExternalIndexSpec::None); |
| #endif |
| } |
| |
| TEST_F(ConfigCompileTests, AllScopes) { |
| // Defaults to true. |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_TRUE(Conf.Completion.AllScopes); |
| |
| Frag = {}; |
| Frag.Completion.AllScopes = false; |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_FALSE(Conf.Completion.AllScopes); |
| |
| Frag = {}; |
| Frag.Completion.AllScopes = true; |
| EXPECT_TRUE(compileAndApply()); |
| EXPECT_TRUE(Conf.Completion.AllScopes); |
| } |
| } // namespace |
| } // namespace config |
| } // namespace clangd |
| } // namespace clang |