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 @@ -85,6 +85,7 @@ Hover.cpp IncludeCleaner.cpp IncludeFixer.cpp + IncludeRenamer.cpp InlayHints.cpp JSONTransport.cpp PathMapping.cpp diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -404,6 +404,7 @@ CodeCompleteOpts.MainFileSignals = IP->Signals; CodeCompleteOpts.AllScopes = Config::current().Completion.AllScopes; + CodeCompleteOpts.IncludeRenamer = Config::current().IncludeRules; // FIXME(ibiryukov): even if Preamble is non-null, we may want to check // both the old and the new version in case only one of them matches. CodeCompleteResult Result = clangd::codeComplete( diff --git a/clang-tools-extra/clangd/CodeComplete.h b/clang-tools-extra/clangd/CodeComplete.h --- a/clang-tools-extra/clangd/CodeComplete.h +++ b/clang-tools-extra/clangd/CodeComplete.h @@ -17,6 +17,8 @@ #include "ASTSignals.h" #include "Compiler.h" +#include "Config.h" +#include "IncludeRenamer.h" #include "Protocol.h" #include "Quality.h" #include "index/Index.h" @@ -31,6 +33,7 @@ #include "llvm/ADT/StringRef.h" #include #include +#include #include namespace clang { @@ -148,6 +151,8 @@ /// Semantics: E.g. For Base = 1.3, if the Prediction score reduces by 2.6 /// points then completion score reduces by 50% or 1.3^(-2.6). float DecisionForestBase = 1.3f; + + std::shared_ptr IncludeRenamer; }; // Semi-structured representation of a code-complete suggestion for our C++ API. diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp --- a/clang-tools-extra/clangd/CodeComplete.cpp +++ b/clang-tools-extra/clangd/CodeComplete.cpp @@ -21,6 +21,7 @@ #include "AST.h" #include "CodeCompletionStrings.h" #include "Compiler.h" +#include "Config.h" #include "ExpectedTypes.h" #include "FileDistance.h" #include "FuzzyMatch.h" @@ -385,10 +386,15 @@ for (const auto &Inc : C.RankedIncludeHeaders) { if (auto ToInclude = Inserted(Inc)) { CodeCompletion::IncludeCandidate Include; - Include.Header = ToInclude->first; - if (ToInclude->second && ShouldInsert) - Include.Insertion = Includes.insert(ToInclude->first); - Completion.Includes.push_back(std::move(Include)); + if (Optional MappedInclude = + Opts.IncludeRenamer + ? Opts.IncludeRenamer->processInclude(ToInclude->first) + : ToInclude->first) { + Include.Header = *MappedInclude; + if (ToInclude->second && ShouldInsert) + Include.Insertion = Includes.insert(*MappedInclude); + Completion.Includes.push_back(std::move(Include)); + } } else log("Failed to generate include insertion edits for adding header " "(FileURI='{0}', IncludeHeader='{1}') into {2}: {3}", diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h --- a/clang-tools-extra/clangd/Config.h +++ b/clang-tools-extra/clangd/Config.h @@ -24,11 +24,13 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CONFIG_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CONFIG_H +#include "IncludeRenamer.h" #include "support/Context.h" #include "llvm/ADT/FunctionExtras.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" +#include "llvm/Support/Regex.h" #include #include #include @@ -140,6 +142,10 @@ bool DeducedTypes = true; bool Designators = true; } InlayHints; + + /// Rules for configuring include insertions. + std::shared_ptr IncludeRules = + std::make_shared(); }; } // namespace clangd diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp --- a/clang-tools-extra/clangd/ConfigCompile.cpp +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -29,6 +29,7 @@ #include "ConfigProvider.h" #include "Diagnostics.h" #include "Feature.h" +#include "IncludeRenamer.h" #include "TidyProvider.h" #include "support/Logger.h" #include "support/Path.h" @@ -198,6 +199,7 @@ compile(std::move(F.Hover)); compile(std::move(F.InlayHints)); compile(std::move(F.Style)); + compile(std::move(F.IncludeRules)); } void compile(Fragment::IfBlock &&F) { @@ -588,6 +590,55 @@ }); } + void compile(std::vector> &&F) { +#ifdef CLANGD_PATH_CASE_INSENSITIVE + static llvm::Regex::RegexFlags Flags = llvm::Regex::IgnoreCase; +#else + static llvm::Regex::RegexFlags Flags = llvm::Regex::NoFlags; +#endif + std::vector Rules; + for (auto &Item : F) { + std::shared_ptr Regex; + if (!Item->PathMatch) { + diag(Error, "Include block must contain 'PathMatch'", Item.Range); + } else { + Regex = std::make_shared(**Item->PathMatch, Flags); + // Don't anchor these regexes + std::string RegexError; + if (!Regex->isValid(RegexError)) { + diag(Error, + llvm::formatv("Invalid regular expression '{0}': {1}", + **Item->PathMatch, RegexError) + .str(), + Item->PathMatch->Range); + Regex.reset(); + } + } + llvm::Optional Type; + if (Item->Type) { + if (auto Value = compileEnum("Type", *Item->Type) + .map("Quoted", IncludeRule::Type::Quoted) + .map("Angled", IncludeRule::Type::Angled) + .map("Disabled", IncludeRule::Type::Disabled) + .value()) { + Type = Value; + } + } + if (Regex) { + llvm::Optional Replace; + if (Item->Replace) + Replace = std::move(**Item->Replace); + Rules.emplace_back(std::move(Regex), std::move(Type), + std::move(Replace)); + } + } + if (!Rules.empty()) { + Out.Apply.push_back([Rules(std::move(Rules))](const Params &, Config &C) { + C.IncludeRules->addRules(Rules); + }); + } + } + constexpr static llvm::SourceMgr::DiagKind Error = llvm::SourceMgr::DK_Error; constexpr static llvm::SourceMgr::DiagKind Warning = llvm::SourceMgr::DK_Warning; diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -307,6 +307,37 @@ llvm::Optional> Designators; }; InlayHintsBlock InlayHints; + + struct IncludeRuleBlock { + /// A regex that must be matched for this rule to take effect. + /// Only partial matching is needed so: + /// {PathMatch: '^foo/'} will match all includes under the foo directory + llvm::Optional> PathMatch; + + /// What type of include we should generate. + /// + /// Valid values are: + /// - Quoted: insert a "double_quoted" include. + /// - Angled: insert an include. + /// - Disabled: don't emit an include for this item + llvm::Optional> Type; + + /// A regex replacement string to transform the matched path. + /// For example with this config: + /// PathMatch: '^foo/' + /// Replace: 'my_foo/' + /// + /// The include: `` would be changed to: + /// `` + /// Note only the first regex match is replaced, therefor given a regex + /// 'foo/' and a replace 'my_foo', the include: `` would be + /// changed to: `` + llvm::Optional> Replace; + }; + + /// A list of @c IncludeRuleBlock`s. This list is traversed in order and stops + /// being processed once the first rule matches. + std::vector> IncludeRules; }; } // namespace config diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp --- a/clang-tools-extra/clangd/ConfigYAML.cpp +++ b/clang-tools-extra/clangd/ConfigYAML.cpp @@ -68,6 +68,7 @@ Dict.handle("Completion", [&](Node &N) { parse(F.Completion, N); }); Dict.handle("Hover", [&](Node &N) { parse(F.Hover, N); }); Dict.handle("InlayHints", [&](Node &N) { parse(F.InlayHints, N); }); + Dict.handle("IncludeRules", [&](Node &N) { parse(F.IncludeRules, N); }); Dict.parse(N); return !(N.failed() || HadError); } @@ -251,6 +252,58 @@ Dict.parse(N); } + llvm::Optional> + parseIncludeRule(Node &N) { + Fragment::IncludeRuleBlock Result; + DictParser Dict("IncludeRule", this); + Dict.handle("PathMatch", [&](Node &N) { + if (auto Value = scalarValue(N, "PathMatch")) + Result.PathMatch = *Value; + }); + Dict.handle("Type", [&](Node &N) { + if (auto Value = scalarValue(N, "Type")) + Result.Type = *Value; + }); + Dict.handle("Replace", [&](Node &N) { + if (auto Value = scalarValue(N, "Replace")) + Result.Replace = *Value; + }); + Dict.parse(N); + if (this->HadError) + return llvm::None; + return Located(std::move(Result), + N.getSourceRange()); + } + + void parse(std::vector> &F, Node &N) { + if (auto *S = llvm::dyn_cast(&N)) { + llvm::Optional Result; + DictParser Dict("IncludeRule", this); + Dict.handle("PathMatch", [&](Node &N) { + if (auto Value = scalarValue(N, "PathMatch")) + Result->PathMatch = *Value; + }); + Dict.handle("Type", [&](Node &N) { + if (auto Value = scalarValue(N, "Type")) + Result->Type = *Value; + }); + Dict.handle("Replace", [&](Node &N) { + if (auto Value = scalarValue(N, "Replace")) + Result->Replace = *Value; + }); + for (auto &Child : *S) { + Result.emplace(); + Dict.parse(Child); + if (!this->HadError) + F.emplace_back(std::move(*Result), Child.getSourceRange()); + Result.emplace(); + } + } else { + N.skip(); + error("Expected list of Input Rules", N); + } + } + // Helper for parsing mapping nodes (dictionaries). // We don't use YamlIO as we want to control over unknown keys. class DictParser { diff --git a/clang-tools-extra/clangd/IncludeFixer.h b/clang-tools-extra/clangd/IncludeFixer.h --- a/clang-tools-extra/clangd/IncludeFixer.h +++ b/clang-tools-extra/clangd/IncludeFixer.h @@ -11,6 +11,7 @@ #include "Diagnostics.h" #include "Headers.h" +#include "IncludeRenamer.h" #include "index/Index.h" #include "index/Symbol.h" #include "clang/AST/Type.h" @@ -33,9 +34,10 @@ class IncludeFixer { public: IncludeFixer(llvm::StringRef File, std::shared_ptr Inserter, - const SymbolIndex &Index, unsigned IndexRequestLimit) + const SymbolIndex &Index, unsigned IndexRequestLimit, + std::shared_ptr Renamer) : File(File), Inserter(std::move(Inserter)), Index(Index), - IndexRequestLimit(IndexRequestLimit) {} + IndexRequestLimit(IndexRequestLimit), Renamer(Renamer) {} /// Returns include insertions that can potentially recover the diagnostic. /// If Info is a note and fixes are returned, they should *replace* the note. @@ -92,6 +94,8 @@ llvm::Optional fuzzyFindCached(const FuzzyFindRequest &Req) const; llvm::Optional lookupCached(const SymbolID &ID) const; + + std::shared_ptr Renamer; }; } // namespace clangd diff --git a/clang-tools-extra/clangd/IncludeFixer.cpp b/clang-tools-extra/clangd/IncludeFixer.cpp --- a/clang-tools-extra/clangd/IncludeFixer.cpp +++ b/clang-tools-extra/clangd/IncludeFixer.cpp @@ -319,11 +319,13 @@ for (const auto &Inc : getRankedIncludes(Sym)) { if (auto ToInclude = Inserted(Sym, Inc)) { if (ToInclude->second) { - if (!InsertedHeaders.try_emplace(ToInclude->first).second) - continue; - if (auto Fix = - insertHeader(ToInclude->first, (Sym.Scope + Sym.Name).str())) - Fixes.push_back(std::move(*Fix)); + if (auto MappedInclude = Renamer->processInclude(ToInclude->first)) { + if (!InsertedHeaders.try_emplace(*MappedInclude).second) + continue; + if (auto Fix = + insertHeader(*MappedInclude, (Sym.Scope + Sym.Name).str())) + Fixes.push_back(std::move(*Fix)); + } } } else { vlog("Failed to calculate include insertion for {0} into {1}: {2}", Inc, diff --git a/clang-tools-extra/clangd/IncludeRenamer.h b/clang-tools-extra/clangd/IncludeRenamer.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/IncludeRenamer.h @@ -0,0 +1,57 @@ +//===--- IncludeRenamed.h-----------------------------------------*- 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_INCLUDERENAMED_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_INCLUDERENAMED_H + +#include "clang/Basic/LLVM.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Regex.h" +#include +#include +#include +namespace clang::clangd { + +class IncludeRule { + friend class IncludeRenamer; + +public: + enum class Type { Angled, Quoted, Disabled }; + + IncludeRule(std::shared_ptr Match, Optional IncludeType, + Optional Replace) + : Match(std::move(Match)), IncludeType(IncludeType), + Replace(std::move(Replace)) {} + +private: + std::shared_ptr Match; + Optional IncludeType; + Optional Replace; +}; + +class IncludeRenamer { +public: + Optional processInclude(StringRef Include) const; + + void addRule(IncludeRule &&Rule); + + void addRules(ArrayRef Rules); + + bool empty() const; + + size_t size() const; + +private: + mutable std::mutex Guard; + std::vector Rules; +}; + +} // namespace clang::clangd + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_INCLUDERENAMED_H \ No newline at end of file diff --git a/clang-tools-extra/clangd/IncludeRenamer.cpp b/clang-tools-extra/clangd/IncludeRenamer.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/IncludeRenamer.cpp @@ -0,0 +1,80 @@ +//===--- IncludeRenamed.cpp---------------------------------------*- 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 "IncludeRenamer.h" +#include "support/Logger.h" +#include "llvm/ADT/None.h" +#include + +namespace clang::clangd { + +llvm::Optional +IncludeRenamer::processInclude(llvm::StringRef Include) const { + std::lock_guard Lock(Guard); + if (Include.empty()) + return llvm::None; + if (Rules.empty()) + return Include.str(); + llvm::StringRef Hdr = Include; + bool IsQuote = Hdr.consume_front("\""); + if (IsQuote ? Hdr.consume_back("\"") + : (Hdr.consume_front("<") && Hdr.consume_back(">"))) { + for (auto &Rule : Rules) { + if (Rule.Match->match(Hdr)) { + if (Rule.IncludeType == IncludeRule::Type::Disabled) + return llvm::None; + bool NewQuotes = (Rule.IncludeType) + ? (*Rule.IncludeType == IncludeRule::Type::Quoted) + : IsQuote; + std::string NewHdr = NewQuotes ? "\"" : "<"; + std::string Err; + if (Rule.Replace) { + NewHdr += Rule.Match->sub(*Rule.Replace, Hdr, &Err); + if (!Err.empty()) { + log("Failed to regex replace include '{0}' with error: " + "'{1}')", + Hdr, Err); + break; + } + } else { + NewHdr += Hdr; + } + NewHdr += NewQuotes ? '"' : '>'; + return std::move(NewHdr); + } + } + } + return Include.str(); +} + +void IncludeRenamer::addRule(IncludeRule &&Rule) { + assert(Rule.Match && Rule.Match->isValid()); + std::lock_guard Lock(Guard); + Rules.push_back(std::move(Rule)); +} + +void IncludeRenamer::addRules(ArrayRef Rules) { + assert(llvm::all_of(Rules, [](const IncludeRule &R) { + return R.Match && R.Match->isValid(); + })); + std::lock_guard Lock(Guard); + // Insert from the beginning as higher priority configs are loaded later. + this->Rules.insert(this->Rules.begin(), Rules.begin(), Rules.end()); +} + +bool IncludeRenamer::empty() const { + std::lock_guard Lock(Guard); + return Rules.empty(); +} + +size_t IncludeRenamer::size() const { + std::lock_guard Lock(Guard); + return Rules.size(); +} + +} // namespace clang::clangd diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp --- a/clang-tools-extra/clangd/ParsedAST.cpp +++ b/clang-tools-extra/clangd/ParsedAST.cpp @@ -563,7 +563,7 @@ Inserter->addExisting(Inc); } FixIncludes.emplace(Filename, Inserter, *Inputs.Index, - /*IndexRequestLimit=*/5); + /*IndexRequestLimit=*/5, Cfg.IncludeRules); ASTDiags.contributeFixes([&FixIncludes](DiagnosticsEngine::Level DiagLevl, const clang::Diagnostic &Info) { return FixIncludes->fix(DiagLevl, Info); diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp --- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp @@ -11,6 +11,7 @@ #include "ClangdServer.h" #include "CodeComplete.h" #include "Compiler.h" +#include "IncludeRenamer.h" #include "Matchers.h" #include "Protocol.h" #include "Quality.h" @@ -2651,6 +2652,22 @@ EXPECT_EQ(Results[0].Includes.size(), 2u); } +TEST(CompletionTest, InsertIncludeRules) { + std::string DeclFile = URI::create(testPath("foo")).toString(); + Symbol Sym = func("Func"); + Sym.CanonicalDeclaration.FileURI = DeclFile.c_str(); + Sym.IncludeHeaders.emplace_back("\"foo.h\"", 2); + + CodeCompleteOptions Opts; + auto Renamer = std::make_shared(); + Renamer->addRule({std::make_shared("foo"), + IncludeRule::Type::Angled, std::string("my_foo")}); + Opts.IncludeRenamer = Renamer; + auto Results = completions("Fun^", {Sym}, Opts).Completions; + assert(!Results.empty()); + EXPECT_THAT(Results[0], AllOf(insertInclude(""))); +} + TEST(CompletionTest, NoInsertIncludeIfOnePresent) { Annotations Test(R"cpp( #include "foo.h" diff --git a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp --- a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp +++ b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp @@ -544,6 +544,71 @@ EXPECT_TRUE(compileAndApply()); EXPECT_THAT(Conf.Style.FullyQualifiedNamespaces, ElementsAre("foo", "bar")); } + +TEST_F(ConfigCompileTests, IncludeRules) { + + auto Add = [&](llvm::StringRef PathMatch = "", llvm::StringRef Type = "", + llvm::StringRef Replace = "") { + Fragment::IncludeRuleBlock IRB; + if (!PathMatch.empty()) + IRB.PathMatch.emplace(PathMatch.str()); + if (!Type.empty()) + IRB.Type.emplace(Type.str()); + if (!Replace.empty()) + IRB.Replace.emplace(Replace.str()); + Frag.IncludeRules.push_back(std::move(IRB)); + }; + + { + Frag = {}; + Add(); + EXPECT_TRUE(compileAndApply()); + EXPECT_THAT( + Diags.Diagnostics, + Contains(AllOf(diagMessage("Include block must contain 'PathMatch'"), + diagKind(llvm::SourceMgr::DK_Error)))); + EXPECT_TRUE(Conf.IncludeRules->empty()); + } + { + Frag = {}; + Add("InvalidRegex("); + EXPECT_TRUE(compileAndApply()); + EXPECT_THAT( + Diags.Diagnostics, + Contains(AllOf(diagMessage("Invalid regular expression " + "'InvalidRegex(': parentheses not balanced"), + diagKind(llvm::SourceMgr::DK_Error)))); + EXPECT_TRUE(Conf.IncludeRules->empty()); + } + { + Frag = {}; + Add("^library-10/"); + EXPECT_TRUE(compileAndApply()); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + EXPECT_EQ(Conf.IncludeRules->size(), 1U); + } + { + Frag = {}; + Add("^library-10/", "Invalid"); + EXPECT_TRUE(compileAndApply()); + EXPECT_THAT(Diags.Diagnostics, + Contains(AllOf( + diagMessage("Invalid Type value 'Invalid'. " + "Valid values are Quoted, Angled, Disabled."), + diagKind(llvm::SourceMgr::DK_Warning)))); + EXPECT_EQ(Conf.IncludeRules->size(), 1U); + } + + { + Frag = {}; + Add("^library-10/", "Quoted"); + Add("^library-12/", "Angled"); + Add("^library-13/", "", "library/"); + EXPECT_TRUE(compileAndApply()); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + EXPECT_EQ(Conf.IncludeRules->size(), 3U); + } +} } // namespace } // namespace config } // namespace clangd 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 @@ -274,6 +274,31 @@ EXPECT_THAT(Results[0].Style.FullyQualifiedNamespaces, ElementsAre(val("foo"), val("bar"))); } + +TEST(ParseYAML, IncludeRules) { + CapturedDiags Diags; + Annotations YAML(R"yaml( +IncludeRules: + - PathMatch: 'Library/' + Type: Angled + - PathMatch: 'Library2/' + Type: Quoted + Replace: 'Libary/' +--- +IncludeRules: + $listinput^PathMatch: 'Library/' + Type: Angled + Replace: 'Library2/' +)yaml"); + auto Results = + Fragment::parseYAML(YAML.code(), "config.yaml", Diags.callback()); + ASSERT_EQ(Results.size(), 1u); + EXPECT_EQ(Results[0].IncludeRules.size(), 2U); + EXPECT_THAT(Diags.Diagnostics, + ElementsAre(AllOf(diagMessage("Expected list of Input Rules"), + diagKind(llvm::SourceMgr::DK_Error), + diagPos(YAML.point("listinput"))))); +} } // namespace } // namespace config } // namespace clangd diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp --- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp +++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp @@ -11,6 +11,7 @@ #include "Diagnostics.h" #include "Feature.h" #include "FeatureModule.h" +#include "IncludeRenamer.h" #include "ParsedAST.h" #include "Protocol.h" #include "TestFS.h" @@ -1294,6 +1295,35 @@ "Include \"b.h\" for symbol na::nb::X"))))); } +TEST(IncludeFixerTest, InsertIncludeRules) { + Annotations Test(R"cpp(// error-ok +$insert[[]]namespace na { +namespace nb { +void foo() { + $unqualified[[X]] x; +} +} +} + )cpp"); + auto TU = TestTU::withCode(Test.code()); + auto Index = buildIndexWithSymbol( + {SymbolWithHeader{"na::X", "unittest:///foo.h", "\"foo.h\""}, + SymbolWithHeader{"na::nb::X", "unittest:///bar.h", "\"bar.h\""}}); + TU.ExternalIndex = Index.get(); + Config Cfg; + Cfg.IncludeRules->addRule({std::make_shared("^foo"), + IncludeRule::Type::Angled, std::string("my_foo")}); + Cfg.IncludeRules->addRule({std::make_shared("^bar"), + IncludeRule::Type::Disabled, None}); + WithContextValue Ctx(Config::Key, std::move(Cfg)); + EXPECT_THAT(*TU.build().getDiagnostics(), + UnorderedElementsAre(AllOf( + Diag(Test.range("unqualified"), "unknown type name 'X'"), + diagName("unknown_typename"), + withFix(Fix(Test.range("insert"), "#include \n", + "Include for symbol na::X"))))); +} + TEST(IncludeFixerTest, NoCrashMemberAccess) { Annotations Test(R"cpp(// error-ok struct X { int xyz; };