diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -37,6 +37,7 @@ CompileCommands.cpp Compiler.cpp Config.cpp + ConfigCompile.cpp ConfigYAML.cpp Diagnostics.cpp DraftStore.cpp diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -0,0 +1,140 @@ +//===--- ConfigCompile.cpp - Translating Fragments into Config ------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// Fragments are applied to Configs in two steps: +// +// 1. (When the fragment is first loaded) +// FragmentCompiler::compile() traverses the Fragment and creates +// function objects that know how to apply the configuration. +// 2. (Every time a config is required) +// CompiledFragment::apply() executes these functions to populate the Config. +// +// Work could be split between these steps in different ways. We try to +// do as much work as possible in the first step. For example, regexes are +// compiled in stage 1 and captured by the apply function. This is because: +// +// - it's more efficient, as the work done in stage 1 must only be done once +// - problems can be reported in stage 1, in stage 2 we must silently recover +// +//===----------------------------------------------------------------------===// + +#include "Config.h" +#include "ConfigProvider.h" +#include "support/Logger.h" +#include "support/Trace.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Regex.h" +#include "llvm/Support/SMLoc.h" +#include "llvm/Support/SourceMgr.h" + +namespace clang { +namespace clangd { +namespace config { +namespace { + +// Wrapper around condition compile() functions to reduce arg-passing. +struct FragmentCompiler { + std::vector> &Conditions; + std::vector> &Apply; + DiagnosticCallback Diagnostic; + llvm::SourceMgr *SourceMgr; + + std::string RegexError = ""; + llvm::Optional compileRegex(const Located &Text) { + std::string Anchored = "^(" + *Text + ")$"; + llvm::Regex Result(Anchored); + if (!Result.isValid(RegexError)) { + diag(Error, "Invalid regex " + Anchored + ": " + RegexError, Text.Range); + return llvm::None; + } + return Result; + } + + void compile(Fragment &&F) { + compile(std::move(F.Condition)); + compile(std::move(F.CompileFlags)); + } + + void compile(Fragment::ConditionBlock &&F) { + if (F.HasUnrecognizedCondition) + Conditions.push_back([&](const Params &) { return false; }); + + auto PathMatch = std::make_unique>(); + for (auto &Entry : F.PathMatch) { + if (auto RE = compileRegex(Entry)) + PathMatch->push_back(std::move(*RE)); + } + if (!PathMatch->empty()) { + Conditions.push_back([PathMatch(std::move(PathMatch))](const Params &P) { + if (P.Path.empty()) + return false; + return llvm::any_of(*PathMatch, [&](const llvm::Regex &RE) { + return RE.match(P.Path); + }); + }); + } + } + + void compile(Fragment::CompileFlagsBlock &&F) { + if (!F.Add.empty()) { + std::vector Add; + for (auto &A : F.Add) + Add.push_back(std::move(*A)); + Apply.push_back([Add(std::move(Add))](Config &C) { + C.CompileFlags.Edits.push_back([Add](std::vector &Args) { + Args.insert(Args.end(), Add.begin(), Add.end()); + }); + }); + } + } + + constexpr static auto Error = llvm::SourceMgr::DK_Error; + void diag(llvm::SourceMgr::DiagKind Kind, llvm::StringRef Message, + llvm::SMRange Range) { + if (Range.isValid() && SourceMgr != nullptr) + Diagnostic(SourceMgr->GetMessage(Range.Start, Kind, Message, Range)); + else + Diagnostic(llvm::SMDiagnostic("", Kind, Message)); + } +}; + +} // namespace + +CompiledFragment::CompiledFragment(Fragment F, DiagnosticCallback D) { + llvm::StringRef SourceFile = ""; + std::pair LineCol = {0, 0}; + if (auto *SM = F.Source.Manager.get()) { + unsigned BufID = SM->getMainFileID(); + LineCol = SM->getLineAndColumn(F.Source.Location, BufID); + SourceFile = SM->getBufferInfo(BufID).Buffer->getBufferIdentifier(); + } + vlog("config::Fragment: compiling {0}:{1} -> {2}", SourceFile, LineCol.first, + this); + trace::Span Tracer("ConfigCompile"); + SPAN_ATTACH(Tracer, "ConfigFile", SourceFile); + + FragmentCompiler{Conditions, Apply, D, F.Source.Manager.get()}.compile( + std::move(F)); +} + +bool CompiledFragment::apply(const Params &P, Config &C) const { + for (auto &C : Conditions) { + if (!C(P)) { + dlog("config::Fragment {0}: condition not met", this); + return false; + } + } + dlog("config::Fragment {0}: applying {1} rules", this, Apply.size()); + for (auto &A : Apply) + A(C); + return true; +} + +} // namespace config +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/ConfigProvider.h b/clang-tools-extra/clangd/ConfigProvider.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/ConfigProvider.h @@ -0,0 +1,63 @@ +//===--- ConfigProvider.h - Loading of user configuration --------*- 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 +// +//===----------------------------------------------------------------------===// +// +// Various clangd features have configurable behaviour (or can be disabled). +// The configuration system allows users to control this: +// - in a user config file, a project config file, via LSP, or via flags +// - specifying different settings for different files +// This file defines the structures used for this, that produce a Config. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CONFIG_PROVIDER_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CONFIG_PROVIDER_H + +#include "ConfigFragment.h" +#include "llvm/ADT/FunctionExtras.h" +#include "llvm/Support/SMLoc.h" +#include +#include + +namespace clang { +namespace clangd { +struct Config; +namespace config { + +/// Describes the context used to evaluate configuration fragments. +struct Params { + /// Absolute path to file we're targeting. Unix slashes. + /// Empty if not configuring a particular file. + llvm::StringRef Path; +}; + +/// A chunk of configuration that has been fully analyzed and is ready to apply. +/// +/// Fragments are compiled by Providers when first loaded, and cached for reuse. +/// Like a compiled program, this is good for performance and also encourages +/// errors to be reported early and only once. +class CompiledFragment { +public: + /// Analyzes and consumes a fragment, possibly yielding more diagnostics. + /// This always produces a usable compiled fragment (errors are recovered). + explicit CompiledFragment(Fragment, DiagnosticCallback); + + /// Updates the configuration to reflect settings from the fragment. + /// Returns true if the condition was met and the settings were used. + bool apply(const Params &, Config &) const; + +private: + // FIXME: remove mutable once unique_function is const-compatible. + mutable std::vector> Conditions; + mutable std::vector> Apply; +}; + +} // namespace config +} // namespace clangd +} // namespace clang + +#endif diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -41,6 +41,7 @@ CollectMacrosTests.cpp CompileCommandsTests.cpp CompilerTests.cpp + ConfigCompileTests.cpp ConfigYAMLTests.cpp DexTests.cpp DiagnosticsTests.cpp diff --git a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp @@ -0,0 +1,88 @@ +//===-- 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 "ConfigProvider.h" +#include "ConfigTesting.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace config { +namespace { +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::SizeIs; +using ::testing::StartsWith; + +class ConfigCompileTests : public ::testing::Test { +protected: + CapturedDiags Diags; + Config Conf; + Fragment Frag; + Params Parm; + + bool compile() { + Conf = Config(); + Diags.Diagnostics.clear(); + CompiledFragment CF(Frag, Diags.callback()); + return CF.apply(Parm, Conf); + } +}; + +TEST_F(ConfigCompileTests, Condition) { + // No condition. + Frag.CompileFlags.Add.emplace_back("X"); + EXPECT_TRUE(compile()) << "Empty config"; + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(1)); + + // Regex with no file. + Frag.Condition.PathMatch.emplace_back("fo*"); + EXPECT_FALSE(compile()); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(0)); + + // Non-matching regex. + Parm.Path = "bar"; + EXPECT_FALSE(compile()); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + + // Matching regex. + Frag.Condition.PathMatch.emplace_back("ba*r"); + EXPECT_TRUE(compile()); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + + // Invalid regex. + Frag.Condition.PathMatch.emplace_back("**]@theu"); + EXPECT_TRUE(compile()); + EXPECT_THAT(Diags.Diagnostics, SizeIs(1)); + EXPECT_THAT(Diags.Diagnostics.front().Message, StartsWith("Invalid regex")); + EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(1)); + Frag.Condition.PathMatch.pop_back(); + + // Valid regex and unknown key. + Frag.Condition.HasUnrecognizedCondition = true; + EXPECT_FALSE(compile()); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); +} + +TEST_F(ConfigCompileTests, CompileCommands) { + Frag.CompileFlags.Add.emplace_back("-foo"); + std::vector Argv = {"clang", "a.cc"}; + EXPECT_TRUE(compile()); + EXPECT_THAT(Conf.CompileFlags.Edits, SizeIs(1)); + Conf.CompileFlags.Edits.front()(Argv); + EXPECT_THAT(Argv, ElementsAre("clang", "a.cc", "-foo")); +} + +} // namespace +} // namespace config +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/ConfigTesting.h b/clang-tools-extra/clangd/unittests/ConfigTesting.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/ConfigTesting.h @@ -0,0 +1,77 @@ +//===-- ConfigTesting.h - Helpers for configuration tests -------*- 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_UNITTESTS_CONFIGTESTING_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_UNITTESTS_CONFIGTESTING_H + +#include "Protocol.h" +#include "llvm/Support/ScopedPrinter.h" +#include "llvm/Support/SourceMgr.h" +#include "gmock/gmock.h" +#include + +namespace clang { +namespace clangd { +namespace config { + +// Provides a DiagnosticsCallback that records diganostics. +// Unlike just pushing them into a vector, underlying storage need not survive. +struct CapturedDiags { + std::function callback() { + return [this](const llvm::SMDiagnostic &D) { + Diagnostics.emplace_back(); + Diag &Out = Diagnostics.back(); + Out.Message = D.getMessage().str(); + Out.Kind = D.getKind(); + Out.Pos.line = D.getLineNo() - 1; + Out.Pos.character = D.getColumnNo(); // Zero-based - bug in SourceMgr? + if (!D.getRanges().empty()) { + const auto &R = D.getRanges().front(); + Out.Range.emplace(); + Out.Range->start.line = Out.Range->end.line = Out.Pos.line; + Out.Range->start.character = R.first; + Out.Range->end.character = R.second; + } + }; + } + struct Diag { + std::string Message; + llvm::SourceMgr::DiagKind Kind; + Position Pos; + llvm::Optional 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); + } + }; + std::vector Diagnostics; +}; + +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, R, "") { return arg.Range == R; } + +inline Position toPosition(llvm::SMLoc L, const llvm::SourceMgr &SM) { + auto LineCol = SM.getLineAndColumn(L); + Position P; + P.line = LineCol.first - 1; + P.character = LineCol.second - 1; + return P; +} + +inline Range toRange(llvm::SMRange R, const llvm::SourceMgr &SM) { + return Range{toPosition(R.Start, SM), toPosition(R.End, SM)}; +} + +} // namespace config +} // namespace clangd +} // namespace clang + +#endif diff --git a/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp b/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp --- a/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp +++ b/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp @@ -8,14 +8,13 @@ #include "Annotations.h" #include "ConfigFragment.h" -#include "Matchers.h" +#include "ConfigTesting.h" #include "Protocol.h" #include "llvm/Support/SMLoc.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/SourceMgr.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "gtest/internal/gtest-internal.h" namespace clang { namespace clangd { @@ -36,55 +35,6 @@ return false; } -Position toPosition(llvm::SMLoc L, const llvm::SourceMgr &SM) { - auto LineCol = SM.getLineAndColumn(L); - Position P; - P.line = LineCol.first - 1; - P.character = LineCol.second - 1; - return P; -} - -Range toRange(llvm::SMRange R, const llvm::SourceMgr &SM) { - return Range{toPosition(R.Start, SM), toPosition(R.End, SM)}; -} - -struct CapturedDiags { - std::function callback() { - return [this](const llvm::SMDiagnostic &D) { - Diagnostics.emplace_back(); - Diag &Out = Diagnostics.back(); - Out.Message = D.getMessage().str(); - Out.Kind = D.getKind(); - Out.Pos.line = D.getLineNo() - 1; - Out.Pos.character = D.getColumnNo(); // Zero-based - bug in SourceMgr? - if (!D.getRanges().empty()) { - const auto &R = D.getRanges().front(); - Out.Range.emplace(); - Out.Range->start.line = Out.Range->end.line = Out.Pos.line; - Out.Range->start.character = R.first; - Out.Range->end.character = R.second; - } - }; - } - struct Diag { - std::string Message; - llvm::SourceMgr::DiagKind Kind; - Position Pos; - llvm::Optional 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); - } - }; - std::vector Diagnostics; -}; - -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, R, "") { return arg.Range == R; } - TEST(ParseYAML, SyntacticForms) { CapturedDiags Diags; const char *YAML = R"yaml(