https://github.com/dmaclach updated https://github.com/llvm/llvm-project/pull/212852
>From 4c545d19338d660eb6101207ee701dac2440afab Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Wed, 29 Jul 2026 12:29:08 -0700 Subject: [PATCH 1/8] [include cleaner] Use tooling::HeaderIncludes for include insertions and deletions Instead of relying on magic UINT_MAX replacements for clang-format to resolve, this change uses tooling::HeaderIncludes to calculate precise offsets and replacement text for adding and removing headers. --- .../include-cleaner/lib/Analysis.cpp | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp index 922a690dfc47c..2cbde460b4ba5 100644 --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -20,8 +20,10 @@ #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/Core/Replacement.h" +#include "clang/Tooling/Inclusions/HeaderIncludes.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" #include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/STLFunctionalExtras.h" @@ -31,7 +33,6 @@ #include "llvm/Support/Error.h" #include "llvm/Support/ErrorHandling.h" #include <cassert> -#include <climits> #include <string> #include <utility> @@ -167,12 +168,30 @@ std::string fixIncludes(const AnalysisResults &Results, const format::FormatStyle &Style) { assert(Style.isCpp() && "Only C++ style supports include insertions!"); tooling::Replacements R; - // Encode insertions/deletions in the magic way clang-format understands. - for (const Include *I : Results.Unused) - cantFail(R.add(tooling::Replacement(FileName, UINT_MAX, 1, I->quote()))); - for (auto &[Spelled, _] : Results.Missing) - cantFail(R.add( - tooling::Replacement(FileName, UINT_MAX, 0, "#include " + Spelled))); + tooling::HeaderIncludes HeaderIncludes(FileName, Code, Style.IncludeStyle); + + for (const Include *I : Results.Unused) { + auto Deletion = HeaderIncludes.remove(I->Spelled, I->Angled); + for (const auto &Del : Deletion) { + cantFail(R.add(Del)); + } + } + + llvm::DenseMap<unsigned, std::string> InsertionsByOffset; + for (auto &[Spelled, _] : Results.Missing) { + auto Insertion = HeaderIncludes.insert(StringRef{Spelled}.trim("\"<>"), + Spelled.starts_with('<'), + tooling::IncludeDirective::Include); + if (Insertion) { + InsertionsByOffset[Insertion->getOffset()] += + Insertion->getReplacementText(); + } + } + + for (const auto &Entry : InsertionsByOffset) { + cantFail( + R.add(tooling::Replacement(FileName, Entry.first, 0, Entry.second))); + } // "cleanup" actually turns the UINT_MAX replacements into concrete edits. auto Positioned = cantFail(format::cleanupAroundReplacements(Code, R, Style)); return cantFail(tooling::applyAllReplacements(Code, Positioned)); >From ca539d937294de60b727ac57dddf217f7191668f Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Wed, 29 Jul 2026 12:44:58 -0700 Subject: [PATCH 2/8] Removed comment that I missed. --- clang-tools-extra/include-cleaner/lib/Analysis.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp index 2cbde460b4ba5..de194fbb3366a 100644 --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -192,7 +192,6 @@ std::string fixIncludes(const AnalysisResults &Results, cantFail( R.add(tooling::Replacement(FileName, Entry.first, 0, Entry.second))); } - // "cleanup" actually turns the UINT_MAX replacements into concrete edits. auto Positioned = cantFail(format::cleanupAroundReplacements(Code, R, Style)); return cantFail(tooling::applyAllReplacements(Code, Positioned)); } >From f176cfb128843a51397ed20ad24841f61ea090c3 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Wed, 29 Jul 2026 13:15:54 -0700 Subject: [PATCH 3/8] Replaced c++20 starts_with with a non c++20 replacement. --- clang-tools-extra/include-cleaner/lib/Analysis.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp index de194fbb3366a..609105566ce22 100644 --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -179,9 +179,9 @@ std::string fixIncludes(const AnalysisResults &Results, llvm::DenseMap<unsigned, std::string> InsertionsByOffset; for (auto &[Spelled, _] : Results.Missing) { - auto Insertion = HeaderIncludes.insert(StringRef{Spelled}.trim("\"<>"), - Spelled.starts_with('<'), - tooling::IncludeDirective::Include); + auto Insertion = HeaderIncludes.insert( + StringRef{Spelled}.trim("\"<>"), !Spelled.empty() && Spelled[0] == '<', + tooling::IncludeDirective::Include); if (Insertion) { InsertionsByOffset[Insertion->getOffset()] += Insertion->getReplacementText(); >From ec61f1dc4e559867801947cdf2e7808821b42da7 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Fri, 31 Jul 2026 13:53:40 -0700 Subject: [PATCH 4/8] Update with better policies for code with matching offsets. --- .../include-cleaner/lib/Analysis.cpp | 27 +++++++++++++++---- .../unittests/AnalysisTest.cpp | 13 +++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp index 609105566ce22..99e7375553e2f 100644 --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -163,6 +163,10 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots, return Results; } +bool isAngled(const std::string &String) { + return !String.empty() && String[0] == '<'; +} + std::string fixIncludes(const AnalysisResults &Results, llvm::StringRef FileName, llvm::StringRef Code, const format::FormatStyle &Style) { @@ -177,20 +181,33 @@ std::string fixIncludes(const AnalysisResults &Results, } } - llvm::DenseMap<unsigned, std::string> InsertionsByOffset; + struct InsertionInfo { + std::string Text; + unsigned Length = 0; + }; + llvm::DenseMap<unsigned, InsertionInfo> InsertionsByOffset; + for (auto &[Spelled, _] : Results.Missing) { auto Insertion = HeaderIncludes.insert( - StringRef{Spelled}.trim("\"<>"), !Spelled.empty() && Spelled[0] == '<', + llvm::StringRef{Spelled}.trim("\"<>"), isAngled(Spelled), tooling::IncludeDirective::Include); if (Insertion) { - InsertionsByOffset[Insertion->getOffset()] += - Insertion->getReplacementText(); + auto &Info = InsertionsByOffset[Insertion->getOffset()]; + Info.Text += Insertion->getReplacementText(); + if (Insertion->getLength() > 0) { + // We can concatenate pure insertions (length 0), but at most one + // true replacement (length > 0) to avoid overwriting the length. + assert(Info.Length == 0 && "Multiple replacements at same offset?"); + Info.Length = Insertion->getLength(); + } } } for (const auto &Entry : InsertionsByOffset) { + const auto &Info = Entry.second; + const unsigned Offset = Entry.first; cantFail( - R.add(tooling::Replacement(FileName, Entry.first, 0, Entry.second))); + R.add(tooling::Replacement(FileName, Offset, Info.Length, Info.Text))); } auto Positioned = cantFail(format::cleanupAroundReplacements(Code, R, Style)); return cantFail(tooling::applyAllReplacements(Code, Positioned)); diff --git a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp index ba5a3fbbcaeb2..95d700b8764b7 100644 --- a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp +++ b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp @@ -489,6 +489,19 @@ R"cpp(#include "d.h" #include "a.h")cpp"); } +TEST(FixIncludes, MultipleInsertionsSameOffset) { + AnalysisResults Results; + Results.Missing.emplace_back("\"a.h\"", Header("")); + Results.Missing.emplace_back("\"b.h\"", Header("")); + + // Empty code guarantees HeaderIncludes chooses offset 0 for both. + llvm::StringRef Code = ""; + + // Should concatenate them without conflict errors in Replacements::add + EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()), + "#include \"a.h\"\n#include \"b.h\"\n"); +} + MATCHER_P3(expandedAt, FileID, Offset, SM, "") { auto [ExpanedFileID, ExpandedOffset] = SM->getDecomposedExpansionLoc(arg); return ExpanedFileID == FileID && ExpandedOffset == Offset; >From 290cb6ad54c2e1cef8d3a007416350052010d3d5 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Mon, 17 Aug 2026 12:58:17 -0700 Subject: [PATCH 5/8] Move batch insertion logic into HeaderIncludes for potential reuse and locality. Add more tests as requested. --- .../include-cleaner/lib/Analysis.cpp | 34 ++------ .../unittests/AnalysisTest.cpp | 68 ++++++++++++++- .../clang/Tooling/Inclusions/HeaderIncludes.h | 46 ++++++++-- .../lib/Tooling/Inclusions/HeaderIncludes.cpp | 85 ++++++++++++++++++- 4 files changed, 190 insertions(+), 43 deletions(-) diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp index 99e7375553e2f..b1b055ee4728c 100644 --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -163,10 +163,6 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots, return Results; } -bool isAngled(const std::string &String) { - return !String.empty() && String[0] == '<'; -} - std::string fixIncludes(const AnalysisResults &Results, llvm::StringRef FileName, llvm::StringRef Code, const format::FormatStyle &Style) { @@ -181,33 +177,13 @@ std::string fixIncludes(const AnalysisResults &Results, } } - struct InsertionInfo { - std::string Text; - unsigned Length = 0; - }; - llvm::DenseMap<unsigned, InsertionInfo> InsertionsByOffset; - - for (auto &[Spelled, _] : Results.Missing) { - auto Insertion = HeaderIncludes.insert( - llvm::StringRef{Spelled}.trim("\"<>"), isAngled(Spelled), - tooling::IncludeDirective::Include); - if (Insertion) { - auto &Info = InsertionsByOffset[Insertion->getOffset()]; - Info.Text += Insertion->getReplacementText(); - if (Insertion->getLength() > 0) { - // We can concatenate pure insertions (length 0), but at most one - // true replacement (length > 0) to avoid overwriting the length. - assert(Info.Length == 0 && "Multiple replacements at same offset?"); - Info.Length = Insertion->getLength(); - } - } + llvm::SmallVector<tooling::HeaderIncludes::HeaderToInsert> HeadersToInsert; + for (const auto &[Spelled, _] : Results.Missing) { + HeadersToInsert.emplace_back(Spelled, tooling::IncludeDirective::Include); } - for (const auto &Entry : InsertionsByOffset) { - const auto &Info = Entry.second; - const unsigned Offset = Entry.first; - cantFail( - R.add(tooling::Replacement(FileName, Offset, Info.Length, Info.Text))); + for (const auto &Repl : HeaderIncludes.insert(HeadersToInsert)) { + cantFail(R.add(Repl)); } auto Positioned = cantFail(format::cleanupAroundReplacements(Code, R, Style)); return cantFail(tooling::applyAllReplacements(Code, Positioned)); diff --git a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp index 95d700b8764b7..87ffd7e6cf3c3 100644 --- a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp +++ b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp @@ -26,7 +26,6 @@ #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" -#include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/VirtualFileSystem.h" #include "llvm/Testing/Annotations/Annotations.h" @@ -398,7 +397,7 @@ TEST_F(AnalyzeTest, SpellingIncludesWithSymlinks) { } // Make sure that the references to implicit operator new/delete are reported as -// ambigious. +// ambiguous. TEST_F(AnalyzeTest, ImplicitOperatorNewDeleteNotMissing) { ExtraFS = llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>(); ExtraFS->addFile("header.h", @@ -719,5 +718,70 @@ TEST_F(WalkUsedTest, MacroConcat) { AllOf(Contains(Pair(Code.point("bar"), UnorderedElementsAre(Header))), Contains(Pair(Code.point("xyz"), UnorderedElementsAre(Header))))); } + +TEST(FixIncludes, MissingIncludesSortingAndGrouping) { + AnalysisResults Results; + Results.Missing.push_back({"\"b.h\"", Header("\"b.h\"")}); + Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); + Results.Missing.push_back({"<foo>", Header("<foo>")}); + + format::FormatStyle Style = format::getLLVMStyle(); + Style.Language = format::FormatStyle::LK_Cpp; + + std::string Code = R"cpp( +void bar(); +)cpp"; + + std::string Fixed = fixIncludes(Results, "test.cc", Code, Style); + EXPECT_EQ( + Fixed, + "\n#include \"a.h\"\n#include \"b.h\"\n#include <foo>\nvoid bar();\n"); +} + +TEST(FixIncludes, MainHeaderGrouping) { + AnalysisResults Results; + Results.Missing.push_back({"\"b.h\"", Header("\"b.h\"")}); + Results.Missing.push_back({"\"foo.h\"", Header("\"foo.h\"")}); + Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); + Results.Missing.push_back({"<vector>", Header("<vector>")}); + + format::FormatStyle Style = format::getLLVMStyle(); + Style.Language = format::FormatStyle::LK_Cpp; + + std::string Code = R"cpp( +void test(); +)cpp"; + + std::string Fixed = fixIncludes(Results, "foo.cc", Code, Style); + EXPECT_EQ(Fixed, "\n#include \"foo.h\"\n#include \"a.h\"\n#include " + "\"b.h\"\n#include <vector>\nvoid test();\n"); +} + +TEST(FixIncludes, MultipleInsertionsAndDeletions) { + AnalysisResults Results; + Include UnusedInc; + UnusedInc.Spelled = "unused.h"; + UnusedInc.Line = 1; + Results.Unused.push_back(&UnusedInc); + + Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); + Results.Missing.push_back({"<foo>", Header("<foo>")}); + + format::FormatStyle Style = format::getLLVMStyle(); + Style.Language = format::FormatStyle::LK_Cpp; + + std::string Code = R"cpp(#include "unused.h" + +void test(); +)cpp"; + + std::string Fixed = fixIncludes(Results, "test.cc", Code, Style); + EXPECT_EQ(Fixed, R"cpp(#include "a.h" +#include <foo> + +void test(); +)cpp"); +} + } // namespace } // namespace clang::include_cleaner diff --git a/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h b/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h index 72407e2b12062..21ef0a171fea8 100644 --- a/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h +++ b/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h @@ -9,13 +9,15 @@ #ifndef LLVM_CLANG_TOOLING_INCLUSIONS_HEADERINCLUDES_H #define LLVM_CLANG_TOOLING_INCLUSIONS_HEADERINCLUDES_H -#include "clang/Basic/SourceManager.h" +#include "clang/Basic/LLVM.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Inclusions/IncludeStyle.h" -#include "llvm/Support/Path.h" +#include "llvm/ADT/StringMap.h" #include "llvm/Support/Regex.h" #include <list> #include <optional> +#include <set> +#include <string> #include <unordered_map> namespace clang { @@ -51,8 +53,7 @@ enum class IncludeDirective { Include, Import }; /// file. class HeaderIncludes { public: - HeaderIncludes(llvm::StringRef FileName, llvm::StringRef Code, - const IncludeStyle &Style); + HeaderIncludes(StringRef FileName, StringRef Code, const IncludeStyle &Style); /// Inserts an #include or #import directive of \p Header into the code. /// If \p IsAngled is true, \p Header will be quoted with <> in the directive; @@ -73,15 +74,43 @@ class HeaderIncludes { /// same category in the code that should be sorted after \p IncludeName. If /// \p IncludeName already exists (with exactly the same spelling), this /// returns std::nullopt. - std::optional<tooling::Replacement> insert(llvm::StringRef Header, - bool IsAngled, + std::optional<tooling::Replacement> insert(StringRef Header, bool IsAngled, IncludeDirective Directive) const; + /// Represents a single header directive to be inserted in a batch operation. + /// + /// Usage: + /// - HeaderToInsert("<vector>") -> inserts #include <vector> + /// (auto-detects angled) + /// - HeaderToInsert("\"foo.h\"") -> inserts #include "foo.h" + /// (auto-detects quoted) + /// - HeaderToInsert("<foo>", IncludeDirective::Import) -> inserts #import + /// <foo> + /// - HeaderToInsert("foo.h", IncludeDirective::Include, /*IsAngled=*/false) + /// -> explicit IsAngled + struct HeaderToInsert { + // The header name, with any surrounding quotes or brackets removed. + std::string Header; + // Whether to insert #include or #import. + IncludeDirective Directive; + // Whether to use <> or "" for the header. If not set, the default is + // determined by the header name. + bool IsAngled; + + HeaderToInsert(StringRef RawOrSpelledHeader, + IncludeDirective Directive = IncludeDirective::Include, + std::optional<bool> IsAngled = std::nullopt); + }; + + /// Inserts a batch of headers into the code, sorting and grouping them + /// according to IncludeStyle and returning the replacements. + tooling::Replacements insert(ArrayRef<HeaderToInsert> Headers) const; + /// Removes all existing #includes and #imports of \p Header quoted with <> if /// \p IsAngled is true or "" if \p IsAngled is false. /// This doesn't resolve the header file path; it only deletes #includes and /// #imports with exactly the same spelling. - tooling::Replacements remove(llvm::StringRef Header, bool IsAngled) const; + tooling::Replacements remove(StringRef Header, bool IsAngled) const; // Matches a whole #include directive. static const llvm::Regex IncludeRegex; @@ -117,8 +146,7 @@ class HeaderIncludes { /// in the order they appear in the source file. /// See comment for "FormatStyle::IncludeCategories" for details about include /// priorities. - std::unordered_map<int, llvm::SmallVector<const Include *, 8>> - IncludesByPriority; + std::unordered_map<int, SmallVector<const Include *, 8>> IncludesByPriority; int FirstIncludeOffset; // All new headers should be inserted after this offset (e.g. after header diff --git a/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp b/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp index c3bbf6b5f2e73..09e2a64576a67 100644 --- a/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp +++ b/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp @@ -14,6 +14,8 @@ #include "clang/Lex/Token.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Inclusions/IncludeStyle.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/STLFunctionalExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" @@ -25,10 +27,12 @@ #include <cassert> #include <climits> #include <functional> +#include <iterator> #include <optional> #include <string> #include <type_traits> #include <utility> +#include <vector> namespace clang { namespace tooling { @@ -512,11 +516,86 @@ HeaderIncludes::insert(llvm::StringRef Header, bool IsAngled, return tooling::Replacement(FileName, InsertOffset, 0, NewInclude); } -tooling::Replacements HeaderIncludes::remove(llvm::StringRef IncludeName, +HeaderIncludes::HeaderToInsert::HeaderToInsert(StringRef RawOrSpelledHeader, + IncludeDirective Directive, + std::optional<bool> IsAngled) + : Directive(Directive) { + if (RawOrSpelledHeader.starts_with("<")) { + Header = RawOrSpelledHeader.trim("<>").str(); + this->IsAngled = IsAngled.value_or(true); + } else if (RawOrSpelledHeader.starts_with("\"")) { + Header = RawOrSpelledHeader.trim("\"").str(); + this->IsAngled = IsAngled.value_or(false); + } else { + Header = RawOrSpelledHeader.str(); + this->IsAngled = IsAngled.value_or(false); + } +} + +tooling::Replacements +HeaderIncludes::insert(llvm::ArrayRef<HeaderToInsert> Headers) const { + tooling::Replacements Result; + if (Headers.empty()) + return Result; + + std::vector<HeaderToInsert> SortedHeaders = Headers.vec(); + llvm::stable_sort(SortedHeaders, [&](const HeaderToInsert &L, + const HeaderToInsert &R) { + std::string QuotedL = + std::string(llvm::formatv(L.IsAngled ? "<{0}>" : "\"{0}\"", L.Header)); + std::string QuotedR = + std::string(llvm::formatv(R.IsAngled ? "<{0}>" : "\"{0}\"", R.Header)); + int PriorityL = Categories.getIncludePriority( + QuotedL, /*CheckMainHeader=*/!MainIncludeFound); + int PriorityR = Categories.getIncludePriority( + QuotedR, /*CheckMainHeader=*/!MainIncludeFound); + if (PriorityL != PriorityR) + return PriorityL < PriorityR; + if (L.Header != R.Header) + return L.Header < R.Header; + if (L.IsAngled != R.IsAngled) + return L.IsAngled < R.IsAngled; + return L.Directive > R.Directive; + }); + SortedHeaders.erase( + std::unique(SortedHeaders.begin(), SortedHeaders.end(), + [](const HeaderToInsert &L, const HeaderToInsert &R) { + return L.Header == R.Header && L.IsAngled == R.IsAngled; + }), + SortedHeaders.end()); + + struct InsertionInfo { + std::string Text; + unsigned Length = 0; + }; + llvm::DenseMap<unsigned, InsertionInfo> InsertionsByOffset; + + for (const auto &H : SortedHeaders) { + if (auto Insertion = insert(H.Header, H.IsAngled, H.Directive)) { + auto &Info = InsertionsByOffset[Insertion->getOffset()]; + Info.Text += Insertion->getReplacementText(); + if (Insertion->getLength() > 0) { + assert(Info.Length == 0 && "Multiple replacements at same offset?"); + Info.Length = Insertion->getLength(); + } + } + } + + for (const auto &Entry : InsertionsByOffset) { + const auto &Info = Entry.second; + const unsigned Offset = Entry.first; + cantFail(Result.add( + tooling::Replacement(FileName, Offset, Info.Length, Info.Text))); + } + + return Result; +} + +tooling::Replacements HeaderIncludes::remove(llvm::StringRef Header, bool IsAngled) const { - assert(IncludeName == trimInclude(IncludeName)); + assert(Header == trimInclude(Header)); tooling::Replacements Result; - auto Iter = ExistingIncludes.find(IncludeName); + auto Iter = ExistingIncludes.find(Header); if (Iter == ExistingIncludes.end()) return Result; for (const auto &Inc : Iter->second) { >From 7375bd5d98ebe677e4fb50b4d39b4c768676ae2f Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Tue, 25 Aug 2026 14:51:15 -0700 Subject: [PATCH 6/8] Responding to comments: - Restored `llvm::` prefixes - Added enum - Updated documentation - Made Format.cpp use the new bulk insertion function from HeaderIncludes. - Restored Analyze.cpp back to original calling through Format.cpp --- .../unittests/AnalysisTest.cpp | 28 ++++++++--------- .../clang/Tooling/Inclusions/HeaderIncludes.h | 31 ++++++++++++------- clang/lib/Format/Format.cpp | 25 +++++++-------- .../lib/Tooling/Inclusions/HeaderIncludes.cpp | 13 +++----- 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp index 87ffd7e6cf3c3..5d2643367c5bb 100644 --- a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp +++ b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp @@ -488,19 +488,6 @@ R"cpp(#include "d.h" #include "a.h")cpp"); } -TEST(FixIncludes, MultipleInsertionsSameOffset) { - AnalysisResults Results; - Results.Missing.emplace_back("\"a.h\"", Header("")); - Results.Missing.emplace_back("\"b.h\"", Header("")); - - // Empty code guarantees HeaderIncludes chooses offset 0 for both. - llvm::StringRef Code = ""; - - // Should concatenate them without conflict errors in Replacements::add - EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()), - "#include \"a.h\"\n#include \"b.h\"\n"); -} - MATCHER_P3(expandedAt, FileID, Offset, SM, "") { auto [ExpanedFileID, ExpandedOffset] = SM->getDecomposedExpansionLoc(arg); return ExpanedFileID == FileID && ExpandedOffset == Offset; @@ -742,8 +729,8 @@ TEST(FixIncludes, MainHeaderGrouping) { AnalysisResults Results; Results.Missing.push_back({"\"b.h\"", Header("\"b.h\"")}); Results.Missing.push_back({"\"foo.h\"", Header("\"foo.h\"")}); - Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); Results.Missing.push_back({"<vector>", Header("<vector>")}); + Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); format::FormatStyle Style = format::getLLVMStyle(); Style.Language = format::FormatStyle::LK_Cpp; @@ -783,5 +770,18 @@ void test(); )cpp"); } +TEST(FixIncludes, MultipleInsertionsSameOffset) { + AnalysisResults Results; + Results.Missing.emplace_back("\"a.h\"", Header("")); + Results.Missing.emplace_back("\"b.h\"", Header("")); + + // Empty code guarantees HeaderIncludes chooses offset 0 for both. + llvm::StringRef Code = ""; + + // Should concatenate them without conflict errors in Replacements::add + EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()), + "#include \"a.h\"\n#include \"b.h\"\n"); +} + } // namespace } // namespace clang::include_cleaner diff --git a/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h b/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h index 21ef0a171fea8..84b1d19e52c8a 100644 --- a/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h +++ b/clang/include/clang/Tooling/Inclusions/HeaderIncludes.h @@ -9,9 +9,10 @@ #ifndef LLVM_CLANG_TOOLING_INCLUSIONS_HEADERINCLUDES_H #define LLVM_CLANG_TOOLING_INCLUSIONS_HEADERINCLUDES_H -#include "clang/Basic/LLVM.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Inclusions/IncludeStyle.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringMap.h" #include "llvm/Support/Regex.h" #include <list> @@ -53,7 +54,8 @@ enum class IncludeDirective { Include, Import }; /// file. class HeaderIncludes { public: - HeaderIncludes(StringRef FileName, StringRef Code, const IncludeStyle &Style); + HeaderIncludes(llvm::StringRef FileName, llvm::StringRef Code, + const IncludeStyle &Style); /// Inserts an #include or #import directive of \p Header into the code. /// If \p IsAngled is true, \p Header will be quoted with <> in the directive; @@ -65,7 +67,7 @@ class HeaderIncludes { /// default. These code sections include: /// - raw string literals (containing #include). /// - #if blocks. - /// - Special #include's among declarations (e.g. functions). + /// - Special #includes among declarations (e.g. functions). /// /// Returns a replacement that inserts the new header into a suitable #include /// block of the same category. This respects the order of the existing @@ -74,7 +76,8 @@ class HeaderIncludes { /// same category in the code that should be sorted after \p IncludeName. If /// \p IncludeName already exists (with exactly the same spelling), this /// returns std::nullopt. - std::optional<tooling::Replacement> insert(StringRef Header, bool IsAngled, + std::optional<tooling::Replacement> insert(llvm::StringRef Header, + bool IsAngled, IncludeDirective Directive) const; /// Represents a single header directive to be inserted in a batch operation. @@ -89,35 +92,38 @@ class HeaderIncludes { /// - HeaderToInsert("foo.h", IncludeDirective::Include, /*IsAngled=*/false) /// -> explicit IsAngled struct HeaderToInsert { + enum class QuoteStyle { AUTO, ANGLED, QUOTED }; + // The header name, with any surrounding quotes or brackets removed. std::string Header; // Whether to insert #include or #import. IncludeDirective Directive; - // Whether to use <> or "" for the header. If not set, the default is - // determined by the header name. + // Whether to use <> or "" for the header. This can be set explicitly with + // QuoteStyle::ANGLED or QuoteStyle::QUOTED, or auto-detected based on + // `RawOrSpelledHeader` with QuoteStyle::AUTO. bool IsAngled; - HeaderToInsert(StringRef RawOrSpelledHeader, + HeaderToInsert(llvm::StringRef RawOrSpelledHeader, IncludeDirective Directive = IncludeDirective::Include, - std::optional<bool> IsAngled = std::nullopt); + QuoteStyle QuoteStyle = QuoteStyle::AUTO); }; /// Inserts a batch of headers into the code, sorting and grouping them /// according to IncludeStyle and returning the replacements. - tooling::Replacements insert(ArrayRef<HeaderToInsert> Headers) const; + tooling::Replacements insert(llvm::ArrayRef<HeaderToInsert> Headers) const; /// Removes all existing #includes and #imports of \p Header quoted with <> if /// \p IsAngled is true or "" if \p IsAngled is false. /// This doesn't resolve the header file path; it only deletes #includes and /// #imports with exactly the same spelling. - tooling::Replacements remove(StringRef Header, bool IsAngled) const; + tooling::Replacements remove(llvm::StringRef Header, bool IsAngled) const; // Matches a whole #include directive. static const llvm::Regex IncludeRegex; private: struct Include { - Include(StringRef Name, tooling::Range R, IncludeDirective D) + Include(llvm::StringRef Name, tooling::Range R, IncludeDirective D) : Name(Name), R(R), Directive(D) {} // An include header quoted with either <> or "". @@ -146,7 +152,8 @@ class HeaderIncludes { /// in the order they appear in the source file. /// See comment for "FormatStyle::IncludeCategories" for details about include /// priorities. - std::unordered_map<int, SmallVector<const Include *, 8>> IncludesByPriority; + std::unordered_map<int, llvm::SmallVector<const Include *, 8>> + IncludesByPriority; int FirstIncludeOffset; // All new headers should be inserted after this offset (e.g. after header diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp index e5533c32899a3..b39d64664d671 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -4211,6 +4211,7 @@ fixCppIncludeInsertions(StringRef Code, const tooling::Replacements &Replaces, } SmallVector<StringRef, 4> Matches; + SmallVector<tooling::HeaderIncludes::HeaderToInsert, 4> HeadersToInsert; for (const auto &R : HeaderInsertions) { auto IncludeDirective = R.getReplacementText(); bool Matched = @@ -4219,19 +4220,17 @@ fixCppIncludeInsertions(StringRef Code, const tooling::Replacements &Replaces, "'#include ...'"); (void)Matched; auto IncludeName = Matches[2]; - auto Replace = - Includes.insert(IncludeName.trim("\"<>"), IncludeName.starts_with("<"), - tooling::IncludeDirective::Include); - if (Replace) { - auto Err = Result.add(*Replace); - if (Err) { - consumeError(std::move(Err)); - unsigned NewOffset = - Result.getShiftedCodePosition(Replace->getOffset()); - auto Shifted = tooling::Replacement(FileName, NewOffset, 0, - Replace->getReplacementText()); - Result = Result.merge(tooling::Replacements(Shifted)); - } + HeadersToInsert.emplace_back(IncludeName, + tooling::IncludeDirective::Include); + } + for (const auto &Replace : Includes.insert(HeadersToInsert)) { + auto Err = Result.add(Replace); + if (Err) { + consumeError(std::move(Err)); + unsigned NewOffset = Result.getShiftedCodePosition(Replace.getOffset()); + auto Shifted = tooling::Replacement(FileName, NewOffset, 0, + Replace.getReplacementText()); + Result = Result.merge(tooling::Replacements(Shifted)); } } return Result; diff --git a/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp b/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp index 09e2a64576a67..285d4a40157db 100644 --- a/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp +++ b/clang/lib/Tooling/Inclusions/HeaderIncludes.cpp @@ -516,19 +516,16 @@ HeaderIncludes::insert(llvm::StringRef Header, bool IsAngled, return tooling::Replacement(FileName, InsertOffset, 0, NewInclude); } -HeaderIncludes::HeaderToInsert::HeaderToInsert(StringRef RawOrSpelledHeader, - IncludeDirective Directive, - std::optional<bool> IsAngled) +HeaderIncludes::HeaderToInsert::HeaderToInsert( + llvm::StringRef RawOrSpelledHeader, IncludeDirective Directive, + QuoteStyle QuoteStyle) : Directive(Directive) { if (RawOrSpelledHeader.starts_with("<")) { Header = RawOrSpelledHeader.trim("<>").str(); - this->IsAngled = IsAngled.value_or(true); + this->IsAngled = QuoteStyle != QuoteStyle::QUOTED; } else if (RawOrSpelledHeader.starts_with("\"")) { Header = RawOrSpelledHeader.trim("\"").str(); - this->IsAngled = IsAngled.value_or(false); - } else { - Header = RawOrSpelledHeader.str(); - this->IsAngled = IsAngled.value_or(false); + this->IsAngled = QuoteStyle == QuoteStyle::ANGLED; } } >From 298bbdf7b98ca00ae5efc76f1b390109eaac4b99 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Tue, 25 Aug 2026 15:22:09 -0700 Subject: [PATCH 7/8] Some missed changes. - Reset Analysis back to original version. - Move tests to single change block. --- .../include-cleaner/lib/Analysis.cpp | 27 ++++--------- .../unittests/AnalysisTest.cpp | 38 +++++++++---------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp index b1b055ee4728c..922a690dfc47c 100644 --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -20,10 +20,8 @@ #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/Core/Replacement.h" -#include "clang/Tooling/Inclusions/HeaderIncludes.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" #include "llvm/ADT/ArrayRef.h" -#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/STLFunctionalExtras.h" @@ -33,6 +31,7 @@ #include "llvm/Support/Error.h" #include "llvm/Support/ErrorHandling.h" #include <cassert> +#include <climits> #include <string> #include <utility> @@ -168,23 +167,13 @@ std::string fixIncludes(const AnalysisResults &Results, const format::FormatStyle &Style) { assert(Style.isCpp() && "Only C++ style supports include insertions!"); tooling::Replacements R; - tooling::HeaderIncludes HeaderIncludes(FileName, Code, Style.IncludeStyle); - - for (const Include *I : Results.Unused) { - auto Deletion = HeaderIncludes.remove(I->Spelled, I->Angled); - for (const auto &Del : Deletion) { - cantFail(R.add(Del)); - } - } - - llvm::SmallVector<tooling::HeaderIncludes::HeaderToInsert> HeadersToInsert; - for (const auto &[Spelled, _] : Results.Missing) { - HeadersToInsert.emplace_back(Spelled, tooling::IncludeDirective::Include); - } - - for (const auto &Repl : HeaderIncludes.insert(HeadersToInsert)) { - cantFail(R.add(Repl)); - } + // Encode insertions/deletions in the magic way clang-format understands. + for (const Include *I : Results.Unused) + cantFail(R.add(tooling::Replacement(FileName, UINT_MAX, 1, I->quote()))); + for (auto &[Spelled, _] : Results.Missing) + cantFail(R.add( + tooling::Replacement(FileName, UINT_MAX, 0, "#include " + Spelled))); + // "cleanup" actually turns the UINT_MAX replacements into concrete edits. auto Positioned = cantFail(format::cleanupAroundReplacements(Code, R, Style)); return cantFail(tooling::applyAllReplacements(Code, Positioned)); } diff --git a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp index 5d2643367c5bb..5df8792c93b2e 100644 --- a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp +++ b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp @@ -706,25 +706,6 @@ TEST_F(WalkUsedTest, MacroConcat) { Contains(Pair(Code.point("xyz"), UnorderedElementsAre(Header))))); } -TEST(FixIncludes, MissingIncludesSortingAndGrouping) { - AnalysisResults Results; - Results.Missing.push_back({"\"b.h\"", Header("\"b.h\"")}); - Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); - Results.Missing.push_back({"<foo>", Header("<foo>")}); - - format::FormatStyle Style = format::getLLVMStyle(); - Style.Language = format::FormatStyle::LK_Cpp; - - std::string Code = R"cpp( -void bar(); -)cpp"; - - std::string Fixed = fixIncludes(Results, "test.cc", Code, Style); - EXPECT_EQ( - Fixed, - "\n#include \"a.h\"\n#include \"b.h\"\n#include <foo>\nvoid bar();\n"); -} - TEST(FixIncludes, MainHeaderGrouping) { AnalysisResults Results; Results.Missing.push_back({"\"b.h\"", Header("\"b.h\"")}); @@ -783,5 +764,24 @@ TEST(FixIncludes, MultipleInsertionsSameOffset) { "#include \"a.h\"\n#include \"b.h\"\n"); } +TEST(FixIncludes, MissingIncludesSortingAndGrouping) { + AnalysisResults Results; + Results.Missing.push_back({"\"b.h\"", Header("\"b.h\"")}); + Results.Missing.push_back({"\"a.h\"", Header("\"a.h\"")}); + Results.Missing.push_back({"<foo>", Header("<foo>")}); + + format::FormatStyle Style = format::getLLVMStyle(); + Style.Language = format::FormatStyle::LK_Cpp; + + std::string Code = R"cpp( +void bar(); +)cpp"; + + std::string Fixed = fixIncludes(Results, "test.cc", Code, Style); + EXPECT_EQ( + Fixed, + "\n#include \"a.h\"\n#include \"b.h\"\n#include <foo>\nvoid bar();\n"); +} + } // namespace } // namespace clang::include_cleaner >From 11a538535de8fe7814cd8b5aa7952e6d53500e7a Mon Sep 17 00:00:00 2001 From: Dave MacLachlan <[email protected]> Date: Tue, 1 Sep 2026 20:41:32 -0700 Subject: [PATCH 8/8] Added tests as requested. --- clang/unittests/Format/CleanupTest.cpp | 33 +++++ .../unittests/Tooling/HeaderIncludesTest.cpp | 121 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/clang/unittests/Format/CleanupTest.cpp b/clang/unittests/Format/CleanupTest.cpp index 06ec78098ac1d..c36f5ae23c65c 100644 --- a/clang/unittests/Format/CleanupTest.cpp +++ b/clang/unittests/Format/CleanupTest.cpp @@ -464,6 +464,39 @@ TEST_F(CleanUpReplacementsTest, InsertMultipleNewHeadersAndSortGoogle) { EXPECT_EQ(Expected, formatAndApply(Code, Replaces)); } +TEST_F(CleanUpReplacementsTest, InsertMainHeaderAtTopAndSortLLVM) { + std::string Code = "int x;\n"; + std::string Expected = "#include \"fix.h\"\n" + "#include \"a.h\"\n" + "#include \"b.h\"\n" + "#include <vector>\n" + "int x;\n"; + tooling::Replacements Replaces = toReplacements({ + createInsertion("#include \"b.h\""), + createInsertion("#include <vector>"), + createInsertion("#include \"a.h\""), + createInsertion("#include \"fix.h\""), + }); + EXPECT_EQ(Expected, apply(Code, Replaces)); +} + +TEST_F(CleanUpReplacementsTest, InsertMainHeaderAtTopAndSortGoogle) { + std::string Code = "int x;\n"; + std::string Expected = "#include \"fix.h\"\n" + "#include <vector>\n" + "#include \"a.h\"\n" + "#include \"b.h\"\n" + "int x;\n"; + tooling::Replacements Replaces = toReplacements({ + createInsertion("#include \"b.h\""), + createInsertion("#include <vector>"), + createInsertion("#include \"a.h\""), + createInsertion("#include \"fix.h\""), + }); + Style = getGoogleStyle(FormatStyle::LK_Cpp); + EXPECT_EQ(Expected, apply(Code, Replaces)); +} + TEST_F(CleanUpReplacementsTest, NoNewLineAtTheEndOfCodeMultipleInsertions) { std::string Code = "#include <map>"; // FIXME: a better behavior is to only append on newline to Code, but this diff --git a/clang/unittests/Tooling/HeaderIncludesTest.cpp b/clang/unittests/Tooling/HeaderIncludesTest.cpp index e697538c56cc7..fdae072a81559 100644 --- a/clang/unittests/Tooling/HeaderIncludesTest.cpp +++ b/clang/unittests/Tooling/HeaderIncludesTest.cpp @@ -43,6 +43,16 @@ class HeaderIncludesTest : public ::testing::Test { return *Result; } + std::string + insertBatch(llvm::StringRef Code, + llvm::ArrayRef<HeaderIncludes::HeaderToInsert> Headers) { + HeaderIncludes Includes(FileName, Code, Style); + auto Replaces = Includes.insert(Headers); + auto Result = applyAllReplacements(Code, Replaces); + EXPECT_TRUE(static_cast<bool>(Result)); + return *Result; + } + std::string FileName = "fix.cpp"; IncludeStyle Style = format::getLLVMStyle().IncludeStyle; }; @@ -868,6 +878,117 @@ int main() { EXPECT_EQ(Expected, insert(Code, "<vector>")); } +TEST_F(HeaderIncludesTest, BatchInsertSortingAndGrouping) { + std::string Code = R"cpp( +void test(); +)cpp"; + std::string Expected = + "\n#include \"a.h\"\n#include \"b.h\"\n#include <foo>\nvoid test();\n"; + + EXPECT_EQ(Expected, + insertBatch(Code, {HeaderIncludes::HeaderToInsert("\"b.h\""), + HeaderIncludes::HeaderToInsert("\"a.h\""), + HeaderIncludes::HeaderToInsert("<foo>")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertMainHeader) { + FileName = "foo.cc"; + Style.IncludeIsMainRegex = "$"; + std::string Code = R"cpp( +void test(); +)cpp"; + std::string Expected = "\n#include \"foo.h\"\n#include \"a.h\"\n#include " + "\"b.h\"\n#include <vector>\nvoid test();\n"; + + EXPECT_EQ(Expected, + insertBatch(Code, {HeaderIncludes::HeaderToInsert("\"b.h\""), + HeaderIncludes::HeaderToInsert("\"foo.h\""), + HeaderIncludes::HeaderToInsert("\"a.h\""), + HeaderIncludes::HeaderToInsert("<vector>")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertDuplicateHeadersSameQuotes) { + std::string Code = R"cpp( +void test(); +)cpp"; + std::string Expected = "\n#include \"a.h\"\n#include <foo>\nvoid test();\n"; + + EXPECT_EQ(Expected, + insertBatch(Code, {HeaderIncludes::HeaderToInsert("\"a.h\""), + HeaderIncludes::HeaderToInsert("\"a.h\""), + HeaderIncludes::HeaderToInsert("<foo>")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertDuplicateHeadersMixedQuotes) { + std::string Code = R"cpp( +void test(); +)cpp"; + std::string Expected = "\n#include \"a.h\"\n#include <a.h>\nvoid test();\n"; + + EXPECT_EQ(Expected, + insertBatch(Code, {HeaderIncludes::HeaderToInsert("\"a.h\""), + HeaderIncludes::HeaderToInsert("<a.h>")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertDuplicateHeadersMixedDirectives) { + std::string Code = R"cpp( +void test(); +)cpp"; + std::string Expected = "\n#import \"a.h\"\n#include <foo>\nvoid test();\n"; + + EXPECT_EQ( + Expected, + insertBatch( + Code, + {HeaderIncludes::HeaderToInsert("\"a.h\"", IncludeDirective::Include), + HeaderIncludes::HeaderToInsert("\"a.h\"", IncludeDirective::Import), + HeaderIncludes::HeaderToInsert("<foo>")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertIntoExistingIncludes) { + std::string Code = R"cpp(#include "b.h" + +void test(); +)cpp"; + std::string Expected = R"cpp(#include "a.h" +#include "b.h" +#include "c.h" +#include <vector> + +void test(); +)cpp"; + + EXPECT_EQ(Expected, + insertBatch(Code, {HeaderIncludes::HeaderToInsert("\"c.h\""), + HeaderIncludes::HeaderToInsert("\"a.h\""), + HeaderIncludes::HeaderToInsert("<vector>")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertEmptyFile) { + std::string Code = ""; + std::string Expected = "#include \"a.h\"\n#include <foo>\n"; + + EXPECT_EQ(Expected, + insertBatch(Code, {HeaderIncludes::HeaderToInsert("<foo>"), + HeaderIncludes::HeaderToInsert("\"a.h\"")})); +} + +TEST_F(HeaderIncludesTest, BatchInsertImportsAlongsideIncludes) { + std::string Code = R"objc( +void test(); +)objc"; + std::string Expected = + "\n#import \"a.h\"\n#include \"b.h\"\n#import <foo>\nvoid test();\n"; + + EXPECT_EQ( + Expected, + insertBatch( + Code, + {HeaderIncludes::HeaderToInsert("\"b.h\"", IncludeDirective::Include), + HeaderIncludes::HeaderToInsert("\"a.h\"", IncludeDirective::Import), + HeaderIncludes::HeaderToInsert("<foo>", IncludeDirective::Import)})); +} + } // namespace } // namespace tooling } // namespace clang _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
