Skip to content

[clang][include cleaner] Fix MainHeader insertion issue - #212852

Open
dmaclach wants to merge 7 commits into
llvm:mainfrom
dmaclach:headerIncludes
Open

[clang][include cleaner] Fix MainHeader insertion issue#212852
dmaclach wants to merge 7 commits into
llvm:mainfrom
dmaclach:headerIncludes

Conversation

@dmaclach

@dmaclach dmaclach commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

When doing multiple header insertions (like from include cleaner) there was an issue with MainHeaders inserted in the wrong location. HeaderIncludes now supports a bulk insertion where it sorts the insertions appropriately before inserting them to make sure that the MainHeader ends up in the correct location.

Format.cpp has been updated to use the bulk insertion method correctly.
Tests added to verify.

@dmaclach

Copy link
Copy Markdown
Contributor Author

@kadircet

@llvmorg-github-actions

llvmorg-github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-clang-format

@llvm/pr-subscribers-clang-tools-extra

Author: dmaclach (dmaclach)

Changes

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.


Full diff: https://github.com/llvm/llvm-project/pull/212852.diff

1 Files Affected:

  • (modified) clang-tools-extra/include-cleaner/lib/Analysis.cpp (+26-7)
diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp
index e48a380211af0..a05c6145e6ca9 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>
 
 namespace clang::include_cleaner {
@@ -168,12 +169,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));

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 98107 tests passed
  • 3133 tests skipped
  • 2 tests failed

Failed Tests

(click on a test name to see its output)

AddressSanitizer-x86_64-linux

AddressSanitizer-x86_64-linux.TestCases/invalid-pointer-pairs-vector-extract.cpp
Exit Code: 0

Command Output (stdout):
--
# RUN: at line 2
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/./bin/clang  --driver-mode=g++ -fsanitize=address -mno-omit-leaf-frame-pointer -fno-omit-frame-pointer -fno-optimize-sibling-calls -gline-tables-only  -m64  -O2 /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp -o /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp -mllvm -asan-detect-invalid-pointer-pair
# executed command: /home/gha/actions-runner/_work/llvm-project/llvm-project/build/./bin/clang --driver-mode=g++ -fsanitize=address -mno-omit-leaf-frame-pointer -fno-omit-frame-pointer -fno-optimize-sibling-calls -gline-tables-only -m64 -O2 /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp -o /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp -mllvm -asan-detect-invalid-pointer-pair
# note: command had no output on stdout or stderr
# RUN: at line 3
env ASAN_OPTIONS=detect_invalid_pointer_pairs=1:halt_on_error=0  /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp 2>&1 | FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# executed command: env ASAN_OPTIONS=detect_invalid_pointer_pairs=1:halt_on_error=0 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp
# note: command had no output on stdout or stderr
# executed command: FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# note: command had no output on stdout or stderr
# RUN: at line 4
env ASAN_OPTIONS=detect_invalid_pointer_pairs=2:halt_on_error=0  /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp 2>&1 | FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# executed command: env ASAN_OPTIONS=detect_invalid_pointer_pairs=2:halt_on_error=0 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp
# note: command had no output on stdout or stderr
# executed command: FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# note: command had no output on stdout or stderr

--

AddressSanitizer-x86_64-linux-dynamic

AddressSanitizer-x86_64-linux-dynamic.TestCases/invalid-pointer-pairs-vector-extract.cpp
Exit Code: 0

Command Output (stdout):
--
# RUN: at line 2
/home/gha/actions-runner/_work/llvm-project/llvm-project/build/./bin/clang  --driver-mode=g++ -fsanitize=address -mno-omit-leaf-frame-pointer -fno-omit-frame-pointer -fno-optimize-sibling-calls -gline-tables-only  -m64  -shared-libasan -O2 /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp -o /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxDynamicConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp -mllvm -asan-detect-invalid-pointer-pair
# executed command: /home/gha/actions-runner/_work/llvm-project/llvm-project/build/./bin/clang --driver-mode=g++ -fsanitize=address -mno-omit-leaf-frame-pointer -fno-omit-frame-pointer -fno-optimize-sibling-calls -gline-tables-only -m64 -shared-libasan -O2 /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp -o /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxDynamicConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp -mllvm -asan-detect-invalid-pointer-pair
# note: command had no output on stdout or stderr
# RUN: at line 3
env ASAN_OPTIONS=detect_invalid_pointer_pairs=1:halt_on_error=0  /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxDynamicConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp 2>&1 | FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# executed command: env ASAN_OPTIONS=detect_invalid_pointer_pairs=1:halt_on_error=0 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxDynamicConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp
# note: command had no output on stdout or stderr
# executed command: FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# note: command had no output on stdout or stderr
# RUN: at line 4
env ASAN_OPTIONS=detect_invalid_pointer_pairs=2:halt_on_error=0  /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxDynamicConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp 2>&1 | FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# executed command: env ASAN_OPTIONS=detect_invalid_pointer_pairs=2:halt_on_error=0 /home/gha/actions-runner/_work/llvm-project/llvm-project/build/runtimes/runtimes-bins/compiler-rt/test/asan/X86_64LinuxDynamicConfig/TestCases/Output/invalid-pointer-pairs-vector-extract.cpp.tmp
# note: command had no output on stdout or stderr
# executed command: FileCheck /home/gha/actions-runner/_work/llvm-project/llvm-project/compiler-rt/test/asan/TestCases/invalid-pointer-pairs-vector-extract.cpp
# note: command had no output on stdout or stderr

--

If these failures are unrelated to your changes (for example tests are broken or flaky at HEAD), please open an issue at https://github.com/llvm/llvm-project/issues and add the infrastructure label.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 57773 tests passed
  • 2887 tests skipped

✅ The build succeeded and all tests passed.

@dmaclach
dmaclach force-pushed the headerIncludes branch 2 times, most recently from 8dd3892 to d49a325 Compare August 3, 2026 19:40
@kadircet

kadircet commented Aug 4, 2026

Copy link
Copy Markdown
Member

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.

Thanks, unfortunately I also no longer remember the reason why we went with clang-format's special handling in the first place. I am worried that these might be behaving differently on certain edge cases.

What do we gain in particular by migrating from one way of doing this to another (apart from new way being less magic)?

@dmaclach

Copy link
Copy Markdown
Contributor Author

#213751 has landed (which is the reason for making this change). With #213751 adding an include and an import to the same path will dedupe and give you just the import.

(also the new way is less magic!)

Comment on lines +192 to +204
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();
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am slightly worried about all the new logic here, especially around grouping/sorting of includes etc.

can you add some more unittests for exercising those? e.g. have "b.h" and then "a.h" as missing, also have a "foo.h" include on a file named "foo.cc", preferably some more with insertions of "a.h" and <foo> etc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more tests as requested. Turns out that one of the new tests would've failed in the old code.

Moved the batch insertion logic into HeaderIncludes for locality and potential future reuse by others.


HeaderToInsert(StringRef RawOrSpelledHeader,
IncludeDirective Directive = IncludeDirective::Include,
std::optional<bool> IsAngled = std::nullopt);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum Spelling { AUTO, ANGLED, QUOTES } instead of tri-state (or just make it explicit IsAngled)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// returns std::nullopt.
std::optional<tooling::Replacement> insert(llvm::StringRef Header,
bool IsAngled,
std::optional<tooling::Replacement> insert(StringRef Header, bool IsAngled,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we keep llvm:: here and elsewhere ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this->IsAngled = IsAngled.value_or(false);
} else {
Header = RawOrSpelledHeader.str();
this->IsAngled = IsAngled.value_or(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we document this default somewhere?

moreover the rest of the logic is using IsAngled as a fallback, even if it's set explicitly, which feels unexpected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the enum it is pretty self documenting. Added documentation as well though.

if (Headers.empty())
return Result;

std::vector<HeaderToInsert> SortedHeaders = Headers.vec();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am uneasy about re-implementing all of this logic, can we try to utilize bits from clang/include/clang/Format/Format.h or figure out why format::cleanupAroundReplacements call in include-cleaner/lib/Analysis.cpp is not getting those fixed ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So Format.cpp depends on HeaderIncludes. I have updated Format.cpp to use my new bulk insertion function. This overall improves Format.cpp in that previously it mishandled Main Headers when multiple headers were being added one by one. Now when doing bulk insertions we pass them to the single insertion function in the correct order to make the bulk insertion respect MainHeaders, quoted includes and angle brackets appropriately.

…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.
  - 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
- Reset Analysis back to original version.
- Move tests to single change block.
@dmaclach dmaclach changed the title [include cleaner] Use tooling::HeaderIncludes for include insertions and deletions [clang][include cleaner] Fix MainHeader insertion issue Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants