[clang-tools-extra] [include-cleaner] Handle non-self-contained includes. (PR #185495)
Jacques Pienaar via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 9 20:56:45 PDT 2026
https://github.com/jpienaar updated https://github.com/llvm/llvm-project/pull/185495
>From 5d28b78b7188e8893d50258cebefb8b747df681e Mon Sep 17 00:00:00 2001
From: Jacques Pienaar <jacques+gh at japienaar.info>
Date: Mon, 9 Mar 2026 19:13:36 +0000
Subject: [PATCH 1/3] [include-cleaner] Handle non-self-contained includes.
Modify include-cleaner to recognize declarations in non-self-contained
headers as roots for analysis, and to correctly attribute symbol references
to these headers. This ensures that symbols used from such headers (like
TableGen-generated .inc files) are correctly identified and their includes
are not flagged as unused.
---
.../include/clang-include-cleaner/Record.h | 10 +++++++
.../include-cleaner/lib/Analysis.cpp | 6 ++--
.../include-cleaner/lib/Record.cpp | 30 ++++++++++++++++---
.../include-cleaner/tool/IncludeCleaner.cpp | 4 ++-
.../unittests/AnalysisTest.cpp | 27 +++++++++++++++--
.../include-cleaner/unittests/RecordTest.cpp | 27 +++++++++++++++--
6 files changed, 91 insertions(+), 13 deletions(-)
diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
index 2dcb5ea2555c5..1e6fd8ea23cd6 100644
--- a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
+++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
@@ -125,6 +125,11 @@ struct RecordedAST {
std::unique_ptr<ASTConsumer> record();
ASTContext *Ctx = nullptr;
+ /// If set, some declarations from non-self-contained headers will be included
+ /// in Roots.
+ /// These are headers that are included by the main file and are not
+ /// self-contained.
+ const PragmaIncludes *PI = nullptr;
/// The set of declarations written at file scope inside the main file.
///
/// These are the roots of the subtrees that should be traversed to find uses.
@@ -132,6 +137,11 @@ struct RecordedAST {
std::vector<Decl *> Roots;
};
+/// Returns true if FID is the main file or a non-self-contained file included
+/// by the main file (transitively through other non-self-contained files).
+bool isUsedAsMainFile(FileID FID, const SourceManager &SM,
+ const PragmaIncludes *PI);
+
/// Recorded main-file preprocessor events relevant to include-cleaner.
///
/// This doesn't include facts that we record globally for the whole TU, even
diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp
index e48a380211af0..8d93e21c06284 100644
--- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp
@@ -70,8 +70,7 @@ void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
RT = RefType::Ambiguous;
SpellLoc = SM.getSpellingLoc(Loc);
}
- auto FID = SM.getFileID(SpellLoc);
- if (FID != SM.getMainFileID() && FID != SM.getPreambleFileID())
+ if (!isUsedAsMainFile(SM.getFileID(SpellLoc), SM, PI))
return;
// FIXME: Most of the work done here is repetitive. It might be useful to
// have a cache/batching.
@@ -81,7 +80,8 @@ void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
}
for (const SymbolReference &MacroRef : MacroRefs) {
assert(MacroRef.Target.kind() == Symbol::Macro);
- if (!SM.isWrittenInMainFile(SM.getSpellingLoc(MacroRef.RefLocation)) ||
+ if (!isUsedAsMainFile(SM.getFileID(SM.getSpellingLoc(MacroRef.RefLocation)),
+ SM, PI) ||
shouldIgnoreMacroReference(PP, MacroRef.Target.macro()))
continue;
CB(MacroRef, headersForSymbol(MacroRef.Target, PP, PI));
diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp
index 0284d6842e2d2..8b67832971f0d 100644
--- a/clang-tools-extra/include-cleaner/lib/Record.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Record.cpp
@@ -460,6 +460,20 @@ bool PragmaIncludes::shouldKeep(const FileEntry *FE) const {
NonSelfContainedFiles.contains(FE->getUniqueID());
}
+bool isUsedAsMainFile(FileID FID, const SourceManager &SM,
+ const PragmaIncludes *PI) {
+ if (FID == SM.getMainFileID() || FID == SM.getPreambleFileID())
+ return true;
+ SourceLocation IncludeLoc = SM.getIncludeLoc(FID);
+ if (IncludeLoc.isInvalid())
+ return false;
+ if (PI) {
+ if (auto FE = SM.getFileEntryRefForID(FID); FE && !PI->isSelfContained(*FE))
+ return isUsedAsMainFile(SM.getFileID(IncludeLoc), SM, PI);
+ }
+ return false;
+}
+
namespace {
template <typename T> bool isImplicitTemplateSpecialization(const Decl *D) {
if (const auto *TD = dyn_cast<T>(D))
@@ -476,10 +490,7 @@ std::unique_ptr<ASTConsumer> RecordedAST::record() {
Recorder(RecordedAST *Out) : Out(Out) {}
void Initialize(ASTContext &Ctx) override { Out->Ctx = &Ctx; }
bool HandleTopLevelDecl(DeclGroupRef DG) override {
- const auto &SM = Out->Ctx->getSourceManager();
for (Decl *D : DG) {
- if (!SM.isWrittenInMainFile(SM.getExpansionLoc(D->getLocation())))
- continue;
if (isImplicitTemplateSpecialization<FunctionDecl>(D) ||
isImplicitTemplateSpecialization<CXXRecordDecl>(D) ||
isImplicitTemplateSpecialization<VarDecl>(D))
@@ -487,7 +498,18 @@ std::unique_ptr<ASTConsumer> RecordedAST::record() {
// FIXME: Filter out certain Obj-C as well.
Out->Roots.push_back(D);
}
- return ASTConsumer::HandleTopLevelDecl(DG);
+ return true;
+ }
+
+ void HandleTranslationUnit(ASTContext &Ctx) override {
+ auto NotMain = [&](Decl *D) {
+ const auto &SM = Out->Ctx->getSourceManager();
+ return !isUsedAsMainFile(
+ SM.getFileID(SM.getExpansionLoc(D->getLocation())), SM, Out->PI);
+ };
+ Out->Roots.erase(
+ std::remove_if(Out->Roots.begin(), Out->Roots.end(), NotMain),
+ Out->Roots.end());
}
};
diff --git a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
index fefbfc3a9614d..16f7a840a51fd 100644
--- a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
+++ b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
@@ -132,7 +132,9 @@ class Action : public clang::ASTFrontendAction {
public:
Action(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter,
llvm::StringMap<std::string> &EditedFiles)
- : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {}
+ : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {
+ AST.PI = &PI;
+ }
private:
RecordedAST AST;
diff --git a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp
index ba5a3fbbcaeb2..a4236fea978a2 100644
--- a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp
@@ -77,7 +77,8 @@ class WalkUsedTest : public testing::Test {
const auto &SM = AST.sourceManager();
llvm::SmallVector<Decl *> TopLevelDecls;
for (Decl *D : AST.context().getTranslationUnitDecl()->decls()) {
- if (!SM.isWrittenInMainFile(SM.getExpansionLoc(D->getLocation())))
+ if (!isUsedAsMainFile(SM.getFileID(SM.getExpansionLoc(D->getLocation())),
+ SM, &PI))
continue;
TopLevelDecls.emplace_back(D);
}
@@ -85,7 +86,7 @@ class WalkUsedTest : public testing::Test {
walkUsed(TopLevelDecls, MacroRefs, &PI, AST.preprocessor(),
[&](const SymbolReference &Ref, llvm::ArrayRef<Header> Providers) {
auto [FID, Offset] = SM.getDecomposedLoc(Ref.RefLocation);
- if (FID != SM.getMainFileID())
+ if (!isUsedAsMainFile(FID, SM, &PI))
ADD_FAILURE() << "Reference outside of the main file!";
OffsetToProviders.emplace(Offset, Providers.vec());
});
@@ -138,6 +139,28 @@ TEST_F(WalkUsedTest, Basic) {
Pair(Code.point("move"), UnorderedElementsAre(UtilitySTL))));
}
+TEST_F(WalkUsedTest, NonSelfContained) {
+ llvm::Annotations Code(R"cpp(
+ #include "header.inc"
+ void $bar^bar() {
+ $foo^foo();
+ }
+ )cpp");
+ Inputs.Code = Code.code();
+ Inputs.ExtraFiles["header.inc"] = R"cpp(
+ void foo();
+ )cpp";
+ TestAST AST(Inputs);
+ auto &SM = AST.sourceManager();
+ auto HeaderFile = Header(*AST.fileManager().getOptionalFileRef("header.inc"));
+ auto MainFile = Header(*SM.getFileEntryRefForID(SM.getMainFileID()));
+ EXPECT_THAT(
+ offsetToProviders(AST),
+ UnorderedElementsAre(
+ Pair(Code.point("bar"), UnorderedElementsAre(MainFile)),
+ Pair(Code.point("foo"), UnorderedElementsAre(MainFile, HeaderFile))));
+}
+
TEST_F(WalkUsedTest, MultipleProviders) {
llvm::Annotations Code(R"cpp(
#include "header1.h"
diff --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
index cbf7bae23b365..91d1600b9ff53 100644
--- a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -55,6 +55,10 @@ MATCHER_P(named, N, "") {
return false;
}
+std::string guard(llvm::StringRef Code) {
+ return "#pragma once\n" + Code.str();
+}
+
MATCHER_P(FileNamed, N, "") {
llvm::StringRef ActualName =
llvm::sys::path::remove_leading_dotslash(arg.getName());
@@ -72,18 +76,25 @@ class RecordASTTest : public ::testing::Test {
RecordASTTest() {
struct RecordAction : public ASTFrontendAction {
RecordedAST &Out;
- RecordAction(RecordedAST &Out) : Out(Out) {}
+ PragmaIncludes &PI;
+ RecordAction(RecordedAST &Out, PragmaIncludes &PI) : Out(Out), PI(PI) {}
+ bool BeginSourceFileAction(CompilerInstance &CI) override {
+ PI.record(CI);
+ Out.PI = &PI;
+ return true;
+ }
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
StringRef) override {
return Out.record();
}
};
Inputs.MakeAction = [this] {
- return std::make_unique<RecordAction>(Recorded);
+ return std::make_unique<RecordAction>(Recorded, PI);
};
}
TestAST build() { return TestAST(Inputs); }
+ PragmaIncludes PI;
};
// Top-level decl from the main file is a root, nested ones aren't.
@@ -103,7 +114,7 @@ TEST_F(RecordASTTest, Namespace) {
// Decl in included file is not a root.
TEST_F(RecordASTTest, Inclusion) {
- Inputs.ExtraFiles["header.h"] = "void headerFunc();";
+ Inputs.ExtraFiles["header.h"] = "#pragma once\nvoid headerFunc();";
Inputs.Code = R"cpp(
#include "header.h"
void mainFunc();
@@ -112,6 +123,16 @@ TEST_F(RecordASTTest, Inclusion) {
EXPECT_THAT(Recorded.Roots, testing::ElementsAre(named("mainFunc")));
}
+// Decl in non-self-contained included file is a root.
+TEST_F(RecordASTTest, NonSelfContainedInclusion) {
+ Inputs.ExtraFiles["header.inc"] = "void headerFunc();";
+ Inputs.Code = R"cpp(
+ #include "header.inc"
+ )cpp";
+ auto AST = build();
+ EXPECT_THAT(Recorded.Roots, testing::ElementsAre(named("headerFunc")));
+}
+
// Decl from macro expanded into the main file is a root.
TEST_F(RecordASTTest, Macros) {
Inputs.ExtraFiles["header.h"] = "#define X void x();";
>From db2218be39d990591dbc4e33b0f57aa48beda95b Mon Sep 17 00:00:00 2001
From: Jacques Pienaar <jacques+gh at japienaar.info>
Date: Mon, 9 Mar 2026 19:32:28 +0000
Subject: [PATCH 2/3] Remove unused func
---
clang-tools-extra/include-cleaner/unittests/RecordTest.cpp | 4 ----
1 file changed, 4 deletions(-)
diff --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
index 91d1600b9ff53..fa7227fe22ae6 100644
--- a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -55,10 +55,6 @@ MATCHER_P(named, N, "") {
return false;
}
-std::string guard(llvm::StringRef Code) {
- return "#pragma once\n" + Code.str();
-}
-
MATCHER_P(FileNamed, N, "") {
llvm::StringRef ActualName =
llvm::sys::path::remove_leading_dotslash(arg.getName());
>From affd24464893e6f44bb58d6a48b500180ca9883b Mon Sep 17 00:00:00 2001
From: Jacques Pienaar <jacques+gh at japienaar.info>
Date: Tue, 10 Mar 2026 03:56:32 +0000
Subject: [PATCH 3/3] Fix missing pragma that was set via function dropped
---
clang-tools-extra/include-cleaner/unittests/RecordTest.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
index fa7227fe22ae6..b62214a86e910 100644
--- a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -143,6 +143,7 @@ TEST_F(RecordASTTest, Macros) {
// Decl from template instantiation is filtered out from roots.
TEST_F(RecordASTTest, ImplicitTemplates) {
Inputs.ExtraFiles["dispatch.h"] = R"cpp(
+ #pragma once
struct A {
static constexpr int value = 1;
};
More information about the cfe-commits
mailing list