[clang-tools-extra] [include-cleaner] add fragment header support (PR #189458)
Daniil Dudkin via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 30 12:51:30 PDT 2026
https://github.com/unterumarmung updated https://github.com/llvm/llvm-project/pull/189458
>From 8f6ff6cbfb39e0b187eee423601a3cda048e2ab2 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Mon, 30 Mar 2026 14:24:55 +0300
Subject: [PATCH 1/3] [include-cleaner][NFC] expose and test `norlmalizePath`
Also fixes a bug where the root `/` path would become an empty string.
---
clang-tools-extra/include-cleaner/lib/Types.cpp | 4 +---
.../include-cleaner/lib/TypesInternal.h | 2 ++
.../include-cleaner/unittests/TypesTest.cpp | 16 +++++++++++++++-
3 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/clang-tools-extra/include-cleaner/lib/Types.cpp b/clang-tools-extra/include-cleaner/lib/Types.cpp
index 7a637639edf8b..d7635c258e522 100644
--- a/clang-tools-extra/include-cleaner/lib/Types.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Types.cpp
@@ -100,14 +100,12 @@ std::string Include::quote() const {
.str();
}
-static llvm::SmallString<128> normalizePath(llvm::StringRef Path) {
+llvm::SmallString<128> normalizePath(llvm::StringRef Path) {
namespace path = llvm::sys::path;
llvm::SmallString<128> P = Path;
path::remove_dots(P, /*remove_dot_dot=*/true);
path::native(P, path::Style::posix);
- while (!P.empty() && P.back() == '/')
- P.pop_back();
return P;
}
diff --git a/clang-tools-extra/include-cleaner/lib/TypesInternal.h b/clang-tools-extra/include-cleaner/lib/TypesInternal.h
index 0bd2766c900ab..60c28956c13c9 100644
--- a/clang-tools-extra/include-cleaner/lib/TypesInternal.h
+++ b/clang-tools-extra/include-cleaner/lib/TypesInternal.h
@@ -96,6 +96,8 @@ template <typename T> struct Hinted : public T {
}
};
+llvm::SmallString<128> normalizePath(llvm::StringRef Path);
+
} // namespace clang::include_cleaner
#endif
diff --git a/clang-tools-extra/include-cleaner/unittests/TypesTest.cpp b/clang-tools-extra/include-cleaner/unittests/TypesTest.cpp
index 609563da488e3..cdc719d620b4c 100644
--- a/clang-tools-extra/include-cleaner/unittests/TypesTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/TypesTest.cpp
@@ -1,4 +1,4 @@
-//===-- RecordTest.cpp ----------------------------------------------------===//
+//===-- TypesTest.cpp -----------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "clang-include-cleaner/Types.h"
+#include "TypesInternal.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/FileSystemOptions.h"
#include "clang/Tooling/Inclusions/StandardLibrary.h"
@@ -98,5 +99,18 @@ TEST(RecordedIncludesTest, MatchVerbatimMixedAbsoluteRelative) {
EXPECT_THAT(Inc.match(Header("<bar.h>")), IsEmpty());
}
+TEST(NormalizePathTest, RemovesDotSegments) {
+ EXPECT_EQ(normalizePath("foo/./bar/../baz").str(), "foo/baz");
+ EXPECT_EQ(normalizePath("foo/bar/").str(), "foo/bar");
+}
+
+TEST(NormalizePathTest, CanonicalizesSeparators) {
+ EXPECT_EQ(normalizePath("foo\\bar").str(), "foo/bar");
+}
+
+TEST(NormalizePathTest, PreservesRootPath) {
+ EXPECT_EQ(normalizePath("/").str(), "/");
+}
+
} // namespace
} // namespace clang::include_cleaner
>From 47a1dde26f791d1afb35d19ca6bbb3e1b83bb7e1 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Mon, 30 Mar 2026 14:25:56 +0300
Subject: [PATCH 2/3] [include-cleaner][NFC] record macro refs in direct
includes
---
.../include/clang-include-cleaner/Record.h | 5 +-
.../include-cleaner/lib/Record.cpp | 26 ++++---
.../include-cleaner/unittests/RecordTest.cpp | 71 ++++++++++++++++++-
3 files changed, 90 insertions(+), 12 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..98c2b63489f41 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
@@ -132,7 +132,7 @@ struct RecordedAST {
std::vector<Decl *> Roots;
};
-/// Recorded main-file preprocessor events relevant to include-cleaner.
+/// Recorded preprocessor events relevant to include-cleaner.
///
/// This doesn't include facts that we record globally for the whole TU, even
/// when they occur in the main file (e.g. IWYU pragmas).
@@ -140,7 +140,8 @@ struct RecordedPP {
/// The callback (when installed into clang) tracks macros/includes in this.
std::unique_ptr<PPCallbacks> record(const Preprocessor &PP);
- /// Describes where macros were used in the main file.
+ /// Describes where macros were used in the main file or in files directly
+ /// included by the main file.
std::vector<SymbolReference> MacroReferences;
/// The include directives seen in the main file.
diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp
index 0284d6842e2d2..a19fcf525919d 100644
--- a/clang-tools-extra/include-cleaner/lib/Record.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Record.cpp
@@ -84,13 +84,13 @@ class PPRecorder : public PPCallbacks {
void MacroExpands(const Token &MacroName, const MacroDefinition &MD,
SourceRange Range, const MacroArgs *Args) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroName.getLocation()))
return;
recordMacroRef(MacroName, *MD.getMacroInfo());
}
void MacroDefined(const Token &MacroName, const MacroDirective *MD) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroName.getLocation()))
return;
const auto *MI = MD->getMacroInfo();
@@ -110,7 +110,7 @@ class PPRecorder : public PPCallbacks {
void MacroUndefined(const Token &MacroName, const MacroDefinition &MD,
const MacroDirective *) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroName.getLocation()))
return;
if (const auto *MI = MD.getMacroInfo())
recordMacroRef(MacroName, *MI);
@@ -118,7 +118,7 @@ class PPRecorder : public PPCallbacks {
void Ifdef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroNameTok.getLocation()))
return;
if (const auto *MI = MD.getMacroInfo())
recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous);
@@ -126,7 +126,7 @@ class PPRecorder : public PPCallbacks {
void Ifndef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroNameTok.getLocation()))
return;
if (const auto *MI = MD.getMacroInfo())
recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous);
@@ -136,14 +136,14 @@ class PPRecorder : public PPCallbacks {
using PPCallbacks::Elifndef;
void Elifdef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroNameTok.getLocation()))
return;
if (const auto *MI = MD.getMacroInfo())
recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous);
}
void Elifndef(SourceLocation Loc, const Token &MacroNameTok,
const MacroDefinition &MD) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroNameTok.getLocation()))
return;
if (const auto *MI = MD.getMacroInfo())
recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous);
@@ -151,13 +151,23 @@ class PPRecorder : public PPCallbacks {
void Defined(const Token &MacroNameTok, const MacroDefinition &MD,
SourceRange Range) override {
- if (!Active)
+ if (!shouldRecordMacroRef(MacroNameTok.getLocation()))
return;
if (const auto *MI = MD.getMacroInfo())
recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous);
}
private:
+ bool shouldRecordMacroRef(SourceLocation Loc) const {
+ const SourceLocation ExpandedLoc = SM.getExpansionLoc(Loc);
+ const FileID FID = SM.getFileID(ExpandedLoc);
+ if (FID == SM.getMainFileID())
+ return true;
+ const SourceLocation IncludeLoc = SM.getIncludeLoc(FID);
+ return IncludeLoc.isValid() &&
+ SM.getFileID(IncludeLoc) == SM.getMainFileID();
+ }
+
void recordMacroRef(const Token &Tok, const MacroInfo &MI,
RefType RT = RefType::Explicit) {
if (MI.isBuiltinMacro())
diff --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
index cbf7bae23b365..c4b785f763750 100644
--- a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -209,8 +209,8 @@ TEST_F(RecordPPTest, CapturesMacroRefs) {
#define $def^X 1
// Refs, but not in main file.
- #define Y X
- int one = X;
+ #define Y $hdr^X
+ int one = $hdr^X;
)cpp");
llvm::Annotations MainFile(R"cpp(
#define EARLY X // not a ref, no definition
@@ -244,11 +244,15 @@ TEST_F(RecordPPTest, CapturesMacroRefs) {
std::vector<unsigned> RefOffsets;
std::vector<unsigned> ExpOffsets; // Expansion locs of refs in macro locs.
+ std::vector<unsigned> HeaderOffsets;
for (const auto &Ref : Recorded.MacroReferences) {
if (Ref.Target == OrigX) {
auto [FID, Off] = SM.getDecomposedLoc(Ref.RefLocation);
if (FID == SM.getMainFileID()) {
RefOffsets.push_back(Off);
+ } else if (FID == SM.translateFile(*AST.fileManager().getOptionalFileRef(
+ "header.h"))) {
+ HeaderOffsets.push_back(Off);
} else if (Ref.RefLocation.isMacroID() &&
SM.isWrittenInMainFile(SM.getExpansionLoc(Ref.RefLocation))) {
ExpOffsets.push_back(
@@ -260,6 +264,7 @@ TEST_F(RecordPPTest, CapturesMacroRefs) {
}
EXPECT_THAT(RefOffsets, ElementsAreArray(MainFile.points()));
EXPECT_THAT(ExpOffsets, ElementsAreArray(MainFile.points("exp")));
+ EXPECT_THAT(HeaderOffsets, ElementsAreArray(Header.points("hdr")));
}
TEST_F(RecordPPTest, CapturesConditionalMacroRefs) {
@@ -300,6 +305,68 @@ TEST_F(RecordPPTest, CapturesConditionalMacroRefs) {
EXPECT_THAT(RefOffsets, ElementsAreArray(MainFile.points()));
}
+TEST_F(RecordPPTest, CapturesConditionalMacroRefsInDirectIncludes) {
+ llvm::Annotations Header(R"cpp(
+ #ifdef ^X
+ #endif
+
+ #if defined(^X)
+ #endif
+
+ #ifndef ^X
+ #endif
+
+ #ifdef Y
+ #elifdef ^X
+ #endif
+
+ #ifndef ^X
+ #elifndef ^X
+ #endif
+ )cpp");
+
+ Inputs.Code = R"cpp(
+ #define X 1
+ #include "header.h"
+ )cpp";
+ Inputs.ExtraFiles["header.h"] = Header.code();
+ Inputs.ExtraArgs.push_back("-std=c++2b");
+ auto AST = build();
+
+ std::vector<unsigned> RefOffsets;
+ SourceManager &SM = AST.sourceManager();
+ FileID HeaderID =
+ SM.translateFile(*AST.fileManager().getOptionalFileRef("header.h"));
+ for (const auto &Ref : Recorded.MacroReferences) {
+ auto [FID, Off] = SM.getDecomposedLoc(Ref.RefLocation);
+ ASSERT_EQ(FID, HeaderID);
+ EXPECT_EQ(Ref.RT, RefType::Ambiguous);
+ EXPECT_EQ("X", Ref.Target.macro().Name->getName());
+ RefOffsets.push_back(Off);
+ }
+ EXPECT_THAT(RefOffsets, ElementsAreArray(Header.points()));
+}
+
+TEST_F(RecordPPTest, DoesNotCaptureMacroRefsInTransitiveIncludes) {
+ Inputs.Code = R"cpp(
+ #define X 1
+ #include "header.h"
+ )cpp";
+ Inputs.ExtraFiles["header.h"] = R"cpp(
+ #include "nested.h"
+ )cpp";
+ Inputs.ExtraFiles["nested.h"] = R"cpp(
+ int one = X;
+
+ #ifdef X
+ #endif
+ )cpp";
+ Inputs.ExtraArgs.push_back("-std=c++2b");
+ build();
+
+ EXPECT_THAT(Recorded.MacroReferences, IsEmpty());
+}
+
class PragmaIncludeTest : public ::testing::Test {
protected:
// We don't build an AST, we just run a preprocessor action!
>From dd878075111c2c7d4f276dcf5c5fe850114d77d2 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Mon, 30 Mar 2026 14:26:42 +0300
Subject: [PATCH 3/3] [include-cleaner] add fragment header support
---
clang-tools-extra/docs/ReleaseNotes.rst | 8 +
.../include/clang-include-cleaner/Analysis.h | 107 +++-
.../include-cleaner/lib/Analysis.cpp | 524 +++++++++++++---
.../include-cleaner/lib/Record.cpp | 9 +-
.../include-cleaner/lib/Types.cpp | 17 +
.../include-cleaner/lib/TypesInternal.h | 10 +
.../include-cleaner/test/Inputs/a.inc | 4 +
.../include-cleaner/test/Inputs/b.inc | 4 +
.../include-cleaner/test/Inputs/gen.inc | 4 +
.../test/Inputs/generated/gen.inc | 4 +
.../include-cleaner/test/Inputs/inner.inc | 4 +
.../include-cleaner/test/Inputs/outer.inc | 2 +
.../include-cleaner/test/Inputs/vector | 4 +
.../test/fragments-block-comment.cpp | 6 +
.../test/fragments-empty-comment.cpp | 6 +
.../include-cleaner/test/fragments-multi.cpp | 10 +
.../test/fragments-nonrecursive.cpp | 5 +
.../test/fragments-spelled-path.cpp | 8 +
.../include-cleaner/test/fragments.cpp | 16 +
.../include-cleaner/tool/IncludeCleaner.cpp | 127 +++-
.../unittests/AnalysisTest.cpp | 578 +++++++++++++++++-
21 files changed, 1306 insertions(+), 151 deletions(-)
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/a.inc
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/b.inc
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/gen.inc
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/generated/gen.inc
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/inner.inc
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/outer.inc
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/vector
create mode 100644 clang-tools-extra/include-cleaner/test/fragments-block-comment.cpp
create mode 100644 clang-tools-extra/include-cleaner/test/fragments-empty-comment.cpp
create mode 100644 clang-tools-extra/include-cleaner/test/fragments-multi.cpp
create mode 100644 clang-tools-extra/include-cleaner/test/fragments-nonrecursive.cpp
create mode 100644 clang-tools-extra/include-cleaner/test/fragments-spelled-path.cpp
create mode 100644 clang-tools-extra/include-cleaner/test/fragments.cpp
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index eb735e6e62ee4..9f733f7f09065 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -450,6 +450,14 @@ Miscellaneous
Improvements to include-fixer
-----------------------------
+Improvements to clang-include-cleaner
+-------------------------------------
+
+- Added :program:`clang-include-cleaner` support for treating matching direct
+ includes as fragments of the main file with ``--fragment-headers`` and for
+ optionally annotating preserved includes with
+ ``--fragment-dependency-comment-format``.
+
Improvements to clang-include-fixer
-----------------------------------
diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h
index c3241763237d1..d53796c659384 100644
--- a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h
+++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h
@@ -16,21 +16,24 @@
#include "clang/Format/Format.h"
#include "clang/Lex/HeaderSearch.h"
#include "clang/Lex/Preprocessor.h"
+#include "clang/Tooling/Core/Replacement.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
+#include <functional>
+#include <optional>
#include <string>
#include <utility>
namespace clang {
class SourceLocation;
+class FileID;
class SourceManager;
class Decl;
class FileEntry;
class HeaderSearch;
namespace tooling {
-class Replacements;
struct IncludeStyle;
} // namespace tooling
namespace include_cleaner {
@@ -60,24 +63,100 @@ void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
llvm::ArrayRef<SymbolReference> MacroRefs,
const PragmaIncludes *PI, const Preprocessor &PP,
UsedSymbolCB CB);
+/// Overload that allows customizing which FileIDs are treated as "main file".
+/// The predicate is evaluated on the expansion file of a reference.
+void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
+ llvm::ArrayRef<SymbolReference> MacroRefs,
+ const PragmaIncludes *PI, const Preprocessor &PP, UsedSymbolCB CB,
+ llvm::function_ref<bool(FileID)> IsMainFile);
+
+/// A location kind where a symbol reference is observed.
+enum class SymbolReferenceOrigin {
+ MainFile,
+ Preamble,
+ Fragment,
+};
+
+/// A missing include finding with per-reference provenance.
+struct MissingIncludeRef {
+ SymbolReference Ref;
+ llvm::SmallVector<Header> Providers;
+ SymbolReferenceOrigin Origin = SymbolReferenceOrigin::MainFile;
+ // Null if the fragment file has multiple direct include sites.
+ const Include *FragmentInclude = nullptr;
+};
+/// An include kept alive only by fragment usage.
+struct FragmentDependency {
+ const Include *Preserved = nullptr;
+ llvm::SmallVector<const Include *> Fragments;
+};
+
+/// The result of include-cleaner analysis for one main file.
struct AnalysisResults {
std::vector<const Include *> Unused;
- // Spellings, like "<vector>" paired with the Header that generated it.
- std::vector<std::pair<std::string, Header>> Missing;
+ // Deduplicated insertion plan, e.g. "<vector>" paired with the chosen
+ // provider Header.
+ std::vector<std::pair<std::string, Header>> MissingIncludes;
+ // Per-reference provenance for consumers that need richer diagnostics.
+ std::vector<MissingIncludeRef> MissingRefs;
+ std::vector<FragmentDependency> FragmentDependencies;
+};
+
+/// Analysis configuration shared by include-cleaner consumers.
+struct AnalysisOptions {
+ /// No analysis will be performed for headers that satisfy the predicate.
+ std::function<bool(const Header &)> HeaderFilter;
+ /// A predicate matched against normalized resolved paths, and normalized
+ /// spelled paths as a fallback, to identify direct include fragments.
+ std::function<bool(llvm::StringRef)> FragmentHeaderFilter;
};
/// Determine which headers should be inserted or removed from the main file.
/// This exposes conclusions but not reasons: use lower-level walkUsed for that.
-///
-/// The HeaderFilter is a predicate that receives absolute path or spelling
-/// without quotes/brackets, when a phyiscal file doesn't exist.
-/// No analysis will be performed for headers that satisfy the predicate.
-AnalysisResults
-analyze(llvm::ArrayRef<Decl *> ASTRoots,
- llvm::ArrayRef<SymbolReference> MacroRefs, const Includes &I,
- const PragmaIncludes *PI, const Preprocessor &PP,
- llvm::function_ref<bool(llvm::StringRef)> HeaderFilter = nullptr);
+AnalysisResults analyze(llvm::ArrayRef<Decl *> ASTRoots,
+ llvm::ArrayRef<SymbolReference> MacroRefs,
+ const Includes &I, const PragmaIncludes *PI,
+ const Preprocessor &PP,
+ const AnalysisOptions &Options = {});
+
+enum class FragmentDependencyCommentStatus {
+ CanInsert,
+ AlreadyPresent,
+ ConflictingComment,
+};
+
+/// Planned comment state for an include kept alive by fragments.
+struct FragmentDependencyComment {
+ const Include *Preserved = nullptr;
+ llvm::SmallVector<const Include *> Fragments;
+ std::string Text;
+ FragmentDependencyCommentStatus Status =
+ FragmentDependencyCommentStatus::CanInsert;
+ std::optional<tooling::Replacement> Replacement;
+};
+
+/// Replacements computed from include-cleaner findings.
+struct IncludeFixes {
+ tooling::Replacements Replacements;
+ std::vector<FragmentDependencyComment> FragmentComments;
+};
+
+/// Options for turning analysis results into source edits.
+struct FixIncludesOptions {
+ /// Raw trailing comment text without the leading //.
+ ///
+ /// When it contains `{0}`, that placeholder is replaced with a comma-
+ /// separated list of direct fragment include spellings that keep the include
+ /// alive.
+ llvm::StringRef FragmentDependencyCommentFormat;
+};
+
+/// Computes replacements to apply include-cleaner findings to the main file.
+IncludeFixes computeIncludeFixes(const AnalysisResults &Results,
+ llvm::StringRef FileName, llvm::StringRef Code,
+ const format::FormatStyle &IncludeStyle,
+ const FixIncludesOptions &Options = {});
/// Removes unused includes and inserts missing ones in the main file.
/// Returns the modified main-file code.
@@ -85,6 +164,10 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots,
std::string fixIncludes(const AnalysisResults &Results,
llvm::StringRef FileName, llvm::StringRef Code,
const format::FormatStyle &IncludeStyle);
+std::string fixIncludes(const AnalysisResults &Results,
+ llvm::StringRef FileName, llvm::StringRef Code,
+ const format::FormatStyle &IncludeStyle,
+ const FixIncludesOptions &Options);
/// Gets all the providers for a symbol by traversing each location.
/// Returned headers are sorted by relevance, first element is the most
diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp
index e48a380211af0..fa64a9dea13c3 100644
--- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp
@@ -8,6 +8,7 @@
#include "clang-include-cleaner/Analysis.h"
#include "AnalysisInternal.h"
+#include "TypesInternal.h"
#include "clang-include-cleaner/IncludeSpeller.h"
#include "clang-include-cleaner/Record.h"
#include "clang-include-cleaner/Types.h"
@@ -22,9 +23,11 @@
#include "clang/Tooling/Core/Replacement.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"
+#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
@@ -32,13 +35,91 @@
#include "llvm/Support/ErrorHandling.h"
#include <cassert>
#include <climits>
+#include <optional>
#include <string>
namespace clang::include_cleaner {
namespace {
+
+struct ClassifiedReference {
+ SymbolReferenceOrigin Origin;
+ // Null if the fragment file has multiple direct include sites.
+ const Include *FragmentInclude = nullptr;
+};
+
+class FragmentTracker {
+public:
+ FragmentTracker(const Includes &Inc, const SourceManager &SM,
+ const std::function<bool(llvm::StringRef)> &HeaderFilter)
+ : SM(SM) {
+ if (!HeaderFilter)
+ return;
+
+ for (const Include &I : Inc.all()) {
+ if (!I.Resolved ||
+ locateInMainFile(I.HashLocation, SM) != MainFileLocation::MainFile) {
+ continue;
+ }
+
+ // Match the canonical path first, but fall back to the spelled include
+ // so generated paths can still be configured even when resolution loses
+ // that spelling detail.
+ const llvm::SmallString<128> ResolvedPath =
+ normalizePath(I.Resolved->getName());
+ bool IsFragment = HeaderFilter(ResolvedPath);
+ if (!IsFragment) {
+ const llvm::SmallString<128> SpelledPath = normalizePath(I.Spelled);
+ if (!SpelledPath.empty())
+ IsFragment = HeaderFilter(SpelledPath);
+ }
+ if (!IsFragment)
+ continue;
+
+ // Fragments are intentionally direct-includes-only for now. If a
+ // fragment includes another file, that nested include keeps normal
+ // header semantics.
+ IncludeSitesByFile[&I.Resolved->getFileEntry()].push_back(&I);
+ DirectIncludes.insert(&I);
+ }
+ }
+
+ std::optional<ClassifiedReference> classify(FileID FID) const {
+ if (FID == SM.getMainFileID())
+ return ClassifiedReference{SymbolReferenceOrigin::MainFile};
+ if (FID == SM.getPreambleFileID())
+ return ClassifiedReference{SymbolReferenceOrigin::Preamble};
+ auto FE = SM.getFileEntryRefForID(FID);
+ if (!FE)
+ return std::nullopt;
+ auto It = IncludeSitesByFile.find(&FE->getFileEntry());
+ if (It == IncludeSitesByFile.end())
+ return std::nullopt;
+ if (It->second.size() != 1)
+ return ClassifiedReference{SymbolReferenceOrigin::Fragment};
+ return ClassifiedReference{SymbolReferenceOrigin::Fragment,
+ It->second.front()};
+ }
+
+ bool isDirectInclude(const Include *I) const {
+ return DirectIncludes.contains(I);
+ }
+
+private:
+ const SourceManager &SM;
+ llvm::DenseMap<const FileEntry *, llvm::SmallVector<const Include *>>
+ IncludeSitesByFile;
+ llvm::DenseSet<const Include *> DirectIncludes;
+};
+
+using UsedSymbolWithOriginCB = llvm::function_ref<void(
+ const SymbolReference &Ref, llvm::ArrayRef<Header> Providers,
+ const ClassifiedReference &Info)>;
+
bool shouldIgnoreMacroReference(const Preprocessor &PP, const Macro &M) {
- auto *MI = PP.getMacroInfo(M.Name);
+ auto &MutablePP = const_cast<Preprocessor &>(PP);
+ auto MD = MutablePP.getMacroDefinitionAtLoc(M.Name, M.Definition);
+ auto *MI = MD ? MD.getMacroInfo() : PP.getMacroInfo(M.Name);
// Macros that expand to themselves are confusing from user's point of view.
// They usually aspect the usage to be attributed to the underlying decl and
// not the macro definition. So ignore such macros (e.g. std{in,out,err} are
@@ -47,15 +128,12 @@ bool shouldIgnoreMacroReference(const Preprocessor &PP, const Macro &M) {
return MI && MI->getNumTokens() == 1 && MI->isObjectLike() &&
MI->getReplacementToken(0).getIdentifierInfo() == M.Name;
}
-} // namespace
-void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
- llvm::ArrayRef<SymbolReference> MacroRefs,
- const PragmaIncludes *PI, const Preprocessor &PP,
- UsedSymbolCB CB) {
+void walkUsedWithOrigins(
+ llvm::ArrayRef<Decl *> ASTRoots, llvm::ArrayRef<SymbolReference> MacroRefs,
+ const PragmaIncludes *PI, const Preprocessor &PP, UsedSymbolWithOriginCB CB,
+ llvm::function_ref<std::optional<ClassifiedReference>(FileID)> Classify) {
const auto &SM = PP.getSourceManager();
- // This is duplicated in writeHTMLReport, changes should be mirrored there.
- tooling::stdlib::Recognizer Recognizer;
for (auto *Root : ASTRoots) {
walkAST(*Root, [&](SourceLocation Loc, NamedDecl &ND, RefType RT) {
auto SpellLoc = SM.getSpellingLoc(Loc);
@@ -71,111 +149,385 @@ void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
SpellLoc = SM.getSpellingLoc(Loc);
}
auto FID = SM.getFileID(SpellLoc);
- if (FID != SM.getMainFileID() && FID != SM.getPreambleFileID())
+ auto Info = Classify(FID);
+ if (!Info)
return;
// FIXME: Most of the work done here is repetitive. It might be useful to
// have a cache/batching.
SymbolReference SymRef{ND, Loc, RT};
- return CB(SymRef, headersForSymbol(ND, PP, PI));
+ return CB(SymRef, headersForSymbol(ND, PP, PI), *Info);
});
}
+
for (const SymbolReference &MacroRef : MacroRefs) {
assert(MacroRef.Target.kind() == Symbol::Macro);
- if (!SM.isWrittenInMainFile(SM.getSpellingLoc(MacroRef.RefLocation)) ||
- shouldIgnoreMacroReference(PP, MacroRef.Target.macro()))
+ if (shouldIgnoreMacroReference(PP, MacroRef.Target.macro()))
continue;
- CB(MacroRef, headersForSymbol(MacroRef.Target, PP, PI));
+ auto FID = SM.getFileID(SM.getSpellingLoc(MacroRef.RefLocation));
+ auto Info = Classify(FID);
+ if (!Info)
+ continue;
+ CB(MacroRef, headersForSymbol(MacroRef.Target, PP, PI), *Info);
}
}
-AnalysisResults
-analyze(llvm::ArrayRef<Decl *> ASTRoots,
- llvm::ArrayRef<SymbolReference> MacroRefs, const Includes &Inc,
- const PragmaIncludes *PI, const Preprocessor &PP,
- llvm::function_ref<bool(llvm::StringRef)> HeaderFilter) {
- auto &SM = PP.getSourceManager();
- const auto MainFile = *SM.getFileEntryRefForID(SM.getMainFileID());
+bool isFilteredInclude(const Include &I,
+ llvm::function_ref<bool(const Header &)> HeaderFilter,
+ const Preprocessor &PP) {
+ if (I.Angled) {
+ auto Lang = PP.getLangOpts().CPlusPlus ? tooling::stdlib::Lang::CXX
+ : tooling::stdlib::Lang::C;
+ if (auto StdHeader = tooling::stdlib::Header::named(I.quote(), Lang);
+ StdHeader && HeaderFilter(*StdHeader)) {
+ return true;
+ }
+ }
+ return I.Resolved && HeaderFilter(*I.Resolved);
+}
+
+bool shouldSuppressIncludeDiagnostic(
+ const Include &I, FileEntryRef MainFile,
+ llvm::function_ref<bool(const Header &)> HeaderFilter,
+ const PragmaIncludes *PI, const Preprocessor &PP,
+ OptionalDirectoryEntryRef ResourceDir) {
+ if (!I.Resolved || I.Resolved->getDir() == ResourceDir ||
+ isFilteredInclude(I, HeaderFilter, PP)) {
+ return true;
+ }
+ if (!PI)
+ return false;
+ if (PI->shouldKeep(*I.Resolved))
+ return true;
+ // Check if main file is the public interface for a private header. If so
+ // we shouldn't diagnose it as unused, or add fragment comments for it.
+ if (auto PHeader = PI->getPublic(*I.Resolved); !PHeader.empty()) {
+ PHeader = PHeader.trim("<>\"");
+ if (MainFile.getName().ends_with(PHeader))
+ return true;
+ }
+ return false;
+}
+
+} // namespace
+
+void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
+ llvm::ArrayRef<SymbolReference> MacroRefs,
+ const PragmaIncludes *PI, const Preprocessor &PP,
+ UsedSymbolCB CB) {
+ const auto &SM = PP.getSourceManager();
+ walkUsed(ASTRoots, MacroRefs, PI, PP, CB, [&](FileID FID) {
+ return FID == SM.getMainFileID() || FID == SM.getPreambleFileID();
+ });
+}
+
+void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
+ llvm::ArrayRef<SymbolReference> MacroRefs,
+ const PragmaIncludes *PI, const Preprocessor &PP, UsedSymbolCB CB,
+ llvm::function_ref<bool(FileID)> IsMainFile) {
+ walkUsedWithOrigins(
+ ASTRoots, MacroRefs, PI, PP,
+ [&](const SymbolReference &Ref, llvm::ArrayRef<Header> Providers,
+ const ClassifiedReference &) { CB(Ref, Providers); },
+ [&](FileID FID) -> std::optional<ClassifiedReference> {
+ if (!IsMainFile(FID))
+ return std::nullopt;
+ return ClassifiedReference{SymbolReferenceOrigin::MainFile};
+ });
+}
+
+namespace {
+
+class IncludeUsage {
+public:
+ void mark(const Include *I, const ClassifiedReference &Info) {
+ Used.insert(I);
+ if (Info.Origin != SymbolReferenceOrigin::Fragment) {
+ UsedByMainOrPreamble.insert(I);
+ return;
+ }
+
+ UsedByFragment.insert(I);
+ if (!Info.FragmentInclude)
+ return;
+ auto &Reasons = ByPreserved[I];
+ if (!llvm::is_contained(Reasons, Info.FragmentInclude))
+ Reasons.push_back(Info.FragmentInclude);
+ }
+
+ bool contains(const Include *I) const { return Used.contains(I); }
+
+ bool isFragmentOnly(const Include *I) const {
+ return UsedByFragment.contains(I) && !UsedByMainOrPreamble.contains(I);
+ }
+
+ llvm::SmallVector<const Include *> fragmentSites(const Include *I) const {
+ auto It = ByPreserved.find(I);
+ if (It == ByPreserved.end())
+ return {};
+ return It->second;
+ }
+
+private:
llvm::DenseSet<const Include *> Used;
+ llvm::DenseSet<const Include *> UsedByMainOrPreamble;
+ llvm::DenseSet<const Include *> UsedByFragment;
+ llvm::DenseMap<const Include *, llvm::SmallVector<const Include *>>
+ ByPreserved;
+};
+
+class MissingIncludeCollector {
+public:
+ void add(llvm::StringRef Spelling, const Header &Provider) {
+ Missing.try_emplace(Spelling, Provider);
+ }
+
+ std::vector<std::pair<std::string, Header>> take() && {
+ std::vector<std::pair<std::string, Header>> Result;
+ Result.reserve(Missing.size());
+ for (auto &E : Missing)
+ Result.emplace_back(E.first().str(), E.second);
+ llvm::sort(Result);
+ return Result;
+ }
+
+private:
llvm::StringMap<Header> Missing;
- constexpr auto DefaultHeaderFilter = [](llvm::StringRef) { return false; };
- if (!HeaderFilter)
- HeaderFilter = DefaultHeaderFilter;
+};
+
+} // namespace
+
+AnalysisResults analyze(llvm::ArrayRef<Decl *> ASTRoots,
+ llvm::ArrayRef<SymbolReference> MacroRefs,
+ const Includes &Inc, const PragmaIncludes *PI,
+ const Preprocessor &PP,
+ const AnalysisOptions &Options) {
+ auto &SM = PP.getSourceManager();
+ const auto MainFile = *SM.getFileEntryRefForID(SM.getMainFileID());
+ auto HeaderFilter = [&](const Header &H) {
+ return Options.HeaderFilter && Options.HeaderFilter(H);
+ };
+ FragmentTracker Fragments(Inc, SM, Options.FragmentHeaderFilter);
+ IncludeUsage Usage;
+ MissingIncludeCollector MissingIncludes;
+ std::vector<MissingIncludeRef> MissingRefs;
OptionalDirectoryEntryRef ResourceDir =
PP.getHeaderSearchInfo().getModuleMap().getBuiltinDir();
- walkUsed(ASTRoots, MacroRefs, PI, PP,
- [&](const SymbolReference &Ref, llvm::ArrayRef<Header> Providers) {
- bool Satisfied = false;
- for (const Header &H : Providers) {
- if (H.kind() == Header::Physical &&
- (H.physical() == MainFile ||
- H.physical().getDir() == ResourceDir)) {
- Satisfied = true;
- }
- for (const Include *I : Inc.match(H)) {
- Used.insert(I);
- Satisfied = true;
- }
- }
- // Bail out if we can't (or need not) insert an include.
- if (Satisfied || Providers.empty() || Ref.RT != RefType::Explicit)
- return;
- if (HeaderFilter(Providers.front().resolvedPath()))
- return;
- // Check if we have any headers with the same spelling, in edge
- // cases like `#include_next "foo.h"`, the user can't ever
- // include the physical foo.h, but can have a spelling that
- // refers to it.
- auto Spelling = spellHeader(
- {Providers.front(), PP.getHeaderSearchInfo(), MainFile});
- for (const Include *I : Inc.match(Header{Spelling})) {
- Used.insert(I);
- Satisfied = true;
- }
- if (!Satisfied)
- Missing.try_emplace(std::move(Spelling), Providers.front());
- });
-
- AnalysisResults Results;
+
+ walkUsedWithOrigins(
+ ASTRoots, MacroRefs, PI, PP,
+ [&](const SymbolReference &Ref, llvm::ArrayRef<Header> Providers,
+ const ClassifiedReference &Info) {
+ bool Satisfied = false;
+ for (const Header &H : Providers) {
+ if (H.kind() == Header::Physical &&
+ (H.physical() == MainFile ||
+ H.physical().getDir() == ResourceDir)) {
+ Satisfied = true;
+ }
+ for (const Include *I : Inc.match(H)) {
+ Usage.mark(I, Info);
+ Satisfied = true;
+ }
+ }
+
+ if (Satisfied || Providers.empty() || Ref.RT != RefType::Explicit)
+ return;
+ if (HeaderFilter(Providers.front()))
+ return;
+
+ auto Spelling = spellHeader(
+ {Providers.front(), PP.getHeaderSearchInfo(), MainFile});
+ for (const Include *I : Inc.match(Header{Spelling})) {
+ Usage.mark(I, Info);
+ Satisfied = true;
+ }
+ if (Satisfied)
+ return;
+
+ // MissingIncludes drives edits, while MissingRefs preserves where the
+ // unsatisfied use came from for higher-level diagnostics.
+ MissingIncludes.add(Spelling, Providers.front());
+ MissingRefs.push_back(MissingIncludeRef{
+ Ref, llvm::SmallVector<Header>(Providers.begin(), Providers.end()),
+ Info.Origin, Info.FragmentInclude});
+ },
+ [&](FileID FID) { return Fragments.classify(FID); });
+
+ AnalysisResults Results{{}, {}, std::move(MissingRefs), {}};
for (const Include &I : Inc.all()) {
- if (Used.contains(&I) || !I.Resolved ||
- HeaderFilter(I.Resolved->getName()) ||
- I.Resolved->getDir() == ResourceDir)
- continue;
- if (PI) {
- if (PI->shouldKeep(*I.Resolved))
- continue;
- // Check if main file is the public interface for a private header. If so
- // we shouldn't diagnose it as unused.
- if (auto PHeader = PI->getPublic(*I.Resolved); !PHeader.empty()) {
- PHeader = PHeader.trim("<>\"");
- // Since most private -> public mappings happen in a verbatim way, we
- // check textually here. This might go wrong in presence of symlinks or
- // header mappings. But that's not different than rest of the places.
- if (MainFile.getName().ends_with(PHeader))
- continue;
+ bool Suppressed = shouldSuppressIncludeDiagnostic(I, MainFile, HeaderFilter,
+ PI, PP, ResourceDir);
+ if (Usage.contains(&I)) {
+ const llvm::SmallVector<const Include *> FragmentSites =
+ Usage.fragmentSites(&I);
+ // Ambiguous fragment sites still preserve the header, but get no comment.
+ if (!Suppressed && Usage.isFragmentOnly(&I) &&
+ !Fragments.isDirectInclude(&I) && !FragmentSites.empty()) {
+ Results.FragmentDependencies.push_back(
+ FragmentDependency{&I, FragmentSites});
}
+ continue;
}
+ if (Suppressed)
+ continue;
Results.Unused.push_back(&I);
}
- for (auto &E : Missing)
- Results.Missing.emplace_back(E.first().str(), E.second);
- llvm::sort(Results.Missing);
+ Results.MissingIncludes = std::move(MissingIncludes).take();
return Results;
}
-std::string fixIncludes(const AnalysisResults &Results,
- llvm::StringRef FileName, llvm::StringRef Code,
- const format::FormatStyle &Style) {
+namespace {
+
+// computeIncludeFixes works on plain text, so comment planning needs its own
+// lightweight line lookup rather than SourceManager-based offsets.
+class LineIndex {
+public:
+ explicit LineIndex(llvm::StringRef Code) : Code(Code) {
+ Starts.push_back(0);
+ for (unsigned I = 0; I < Code.size(); ++I) {
+ if (Code[I] == '\n')
+ Starts.push_back(I + 1);
+ }
+ Starts.push_back(Code.size());
+ }
+
+ llvm::StringRef line(unsigned OneBasedLine) const {
+ auto [Start, End] = bounds(OneBasedLine);
+ return Code.slice(Start, End);
+ }
+
+ unsigned trimmedLineEnd(unsigned OneBasedLine) const {
+ auto [Start, End] = bounds(OneBasedLine);
+ while (End > Start && (Code[End - 1] == ' ' || Code[End - 1] == '\t'))
+ --End;
+ return End;
+ }
+
+private:
+ std::pair<unsigned, unsigned> bounds(unsigned OneBasedLine) const {
+ if (OneBasedLine == 0 || OneBasedLine >= Starts.size())
+ return {Code.size(), Code.size()};
+ unsigned Start = Starts[OneBasedLine - 1];
+ unsigned End =
+ Starts[OneBasedLine] == 0 ? Code.size() : Starts[OneBasedLine] - 1;
+ if (End > Start && Code[End - 1] == '\r')
+ --End;
+ return {Start, End};
+ }
+
+ llvm::StringRef Code;
+ llvm::SmallVector<unsigned> Starts;
+};
+
+std::string formatFragmentDependencyComment(
+ llvm::StringRef Format, llvm::ArrayRef<const Include *> FragmentIncludes) {
+ if (Format.empty())
+ return {};
+
+ std::string FragmentList;
+ for (const Include *Fragment : FragmentIncludes) {
+ if (!FragmentList.empty())
+ FragmentList += ", ";
+ FragmentList += Fragment->quote();
+ }
+
+ std::string Result;
+ llvm::StringRef Remaining = Format;
+ while (true) {
+ auto Pos = Remaining.find("{0}");
+ if (Pos == llvm::StringRef::npos) {
+ Result += Remaining.str();
+ return Result;
+ }
+ Result += Remaining.take_front(Pos).str();
+ Result += FragmentList;
+ Remaining = Remaining.drop_front(Pos + 3);
+ }
+}
+
+FragmentDependencyComment inspectFragmentDependencyComment(
+ const FragmentDependency &Dependency, llvm::StringRef FileName,
+ const LineIndex &Lines, llvm::StringRef CommentFormat) {
+ FragmentDependencyComment Comment{
+ Dependency.Preserved, Dependency.Fragments,
+ formatFragmentDependencyComment(CommentFormat, Dependency.Fragments),
+ FragmentDependencyCommentStatus::CanInsert, std::nullopt};
+ if (Comment.Text.empty())
+ return Comment;
+
+ llvm::StringRef Line = Lines.line(Dependency.Preserved->Line);
+ size_t IncludePos = Line.find(Dependency.Preserved->quote());
+ if (IncludePos == llvm::StringRef::npos) {
+ Comment.Status = FragmentDependencyCommentStatus::ConflictingComment;
+ return Comment;
+ }
+
+ llvm::StringRef Tail =
+ Line.drop_front(IncludePos + Dependency.Preserved->quote().size());
+ llvm::StringRef TrimmedTail = Tail.ltrim(" \t");
+ if (TrimmedTail.empty()) {
+ Comment.Status = FragmentDependencyCommentStatus::CanInsert;
+ Comment.Replacement = tooling::Replacement(
+ FileName, Lines.trimmedLineEnd(Dependency.Preserved->Line), 0,
+ " // " + Comment.Text);
+ return Comment;
+ }
+
+ if (TrimmedTail.starts_with("//")) {
+ llvm::StringRef Existing = TrimmedTail.drop_front(2).trim();
+ Comment.Status = Existing == Comment.Text
+ ? FragmentDependencyCommentStatus::AlreadyPresent
+ : FragmentDependencyCommentStatus::ConflictingComment;
+ return Comment;
+ }
+
+ Comment.Status = FragmentDependencyCommentStatus::ConflictingComment;
+ return Comment;
+}
+
+} // namespace
+
+IncludeFixes computeIncludeFixes(const AnalysisResults &Results,
+ llvm::StringRef FileName, llvm::StringRef Code,
+ const format::FormatStyle &Style,
+ const FixIncludesOptions &Options) {
assert(Style.isCpp() && "Only C++ style supports include insertions!");
- tooling::Replacements R;
- // Encode insertions/deletions in the magic way clang-format understands.
+ IncludeFixes Fixes;
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(
+ cantFail(Fixes.Replacements.add(
+ tooling::Replacement(FileName, UINT_MAX, 1, I->quote())));
+ for (const auto &[Spelled, _] : Results.MissingIncludes)
+ cantFail(Fixes.Replacements.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));
+
+ if (Options.FragmentDependencyCommentFormat.empty())
+ return Fixes;
+
+ LineIndex Lines(Code);
+ for (const FragmentDependency &Dependency : Results.FragmentDependencies) {
+ FragmentDependencyComment Comment = inspectFragmentDependencyComment(
+ Dependency, FileName, Lines, Options.FragmentDependencyCommentFormat);
+ if (Comment.Replacement)
+ cantFail(Fixes.Replacements.add(*Comment.Replacement));
+ Fixes.FragmentComments.push_back(std::move(Comment));
+ }
+ return Fixes;
+}
+
+std::string fixIncludes(const AnalysisResults &Results,
+ llvm::StringRef FileName, llvm::StringRef Code,
+ const format::FormatStyle &Style) {
+ return fixIncludes(Results, FileName, Code, Style, {});
+}
+
+std::string fixIncludes(const AnalysisResults &Results,
+ llvm::StringRef FileName, llvm::StringRef Code,
+ const format::FormatStyle &Style,
+ const FixIncludesOptions &Options) {
+ IncludeFixes Fixes =
+ computeIncludeFixes(Results, FileName, Code, Style, Options);
+ auto Positioned = cantFail(
+ format::cleanupAroundReplacements(Code, Fixes.Replacements, Style));
return cantFail(tooling::applyAllReplacements(Code, Positioned));
}
diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp
index a19fcf525919d..2632f58ba8c9d 100644
--- a/clang-tools-extra/include-cleaner/lib/Record.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Record.cpp
@@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//
#include "clang-include-cleaner/Record.h"
+#include "TypesInternal.h"
#include "clang-include-cleaner/Types.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
@@ -159,13 +160,7 @@ class PPRecorder : public PPCallbacks {
private:
bool shouldRecordMacroRef(SourceLocation Loc) const {
- const SourceLocation ExpandedLoc = SM.getExpansionLoc(Loc);
- const FileID FID = SM.getFileID(ExpandedLoc);
- if (FID == SM.getMainFileID())
- return true;
- const SourceLocation IncludeLoc = SM.getIncludeLoc(FID);
- return IncludeLoc.isValid() &&
- SM.getFileID(IncludeLoc) == SM.getMainFileID();
+ return locateInMainFile(Loc, SM) != MainFileLocation::Other;
}
void recordMacroRef(const Token &Tok, const MacroInfo &MI,
diff --git a/clang-tools-extra/include-cleaner/lib/Types.cpp b/clang-tools-extra/include-cleaner/lib/Types.cpp
index d7635c258e522..8a102cc2ab848 100644
--- a/clang-tools-extra/include-cleaner/lib/Types.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Types.cpp
@@ -10,6 +10,7 @@
#include "TypesInternal.h"
#include "clang/AST/Decl.h"
#include "clang/Basic/FileEntry.h"
+#include "clang/Basic/SourceManager.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
@@ -109,6 +110,22 @@ llvm::SmallString<128> normalizePath(llvm::StringRef Path) {
return P;
}
+MainFileLocation locateInMainFile(SourceLocation Loc, const SourceManager &SM) {
+ const SourceLocation ExpandedLoc = SM.getExpansionLoc(Loc);
+ const FileID FID = SM.getFileID(ExpandedLoc);
+ if (FID == SM.getMainFileID() || FID == SM.getPreambleFileID())
+ return MainFileLocation::MainFile;
+ const SourceLocation IncludeLoc = SM.getIncludeLoc(FID);
+ if (!IncludeLoc.isValid())
+ return MainFileLocation::Other;
+ const FileID IncludeFID = SM.getFileID(SM.getExpansionLoc(IncludeLoc));
+ if (IncludeFID == SM.getMainFileID() ||
+ IncludeFID == SM.getPreambleFileID()) {
+ return MainFileLocation::DirectInclude;
+ }
+ return MainFileLocation::Other;
+}
+
void Includes::addSearchDirectory(llvm::StringRef Path) {
SearchPath.try_emplace(normalizePath(Path));
}
diff --git a/clang-tools-extra/include-cleaner/lib/TypesInternal.h b/clang-tools-extra/include-cleaner/lib/TypesInternal.h
index 60c28956c13c9..df53deaebf72f 100644
--- a/clang-tools-extra/include-cleaner/lib/TypesInternal.h
+++ b/clang-tools-extra/include-cleaner/lib/TypesInternal.h
@@ -19,6 +19,9 @@
namespace llvm {
class raw_ostream;
}
+namespace clang {
+class SourceManager;
+}
namespace clang::include_cleaner {
/// A place where a symbol can be provided.
/// It is either a physical file of the TU (SourceLocation) or a logical
@@ -49,6 +52,12 @@ struct SymbolLocation {
};
llvm::raw_ostream &operator<<(llvm::raw_ostream &, const SymbolLocation &);
+enum class MainFileLocation {
+ MainFile,
+ DirectInclude,
+ Other,
+};
+
/// Represents properties of a symbol provider.
///
/// Hints represents the properties of the edges traversed when finding headers
@@ -97,6 +106,7 @@ template <typename T> struct Hinted : public T {
};
llvm::SmallString<128> normalizePath(llvm::StringRef Path);
+MainFileLocation locateInMainFile(SourceLocation Loc, const SourceManager &SM);
} // namespace clang::include_cleaner
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/a.inc b/clang-tools-extra/include-cleaner/test/Inputs/a.inc
new file mode 100644
index 0000000000000..1d44a1554bcb8
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/a.inc
@@ -0,0 +1,4 @@
+#pragma once
+struct A {
+ std::vector<int> Values;
+};
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/b.inc b/clang-tools-extra/include-cleaner/test/Inputs/b.inc
new file mode 100644
index 0000000000000..a36c95d1eb829
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/b.inc
@@ -0,0 +1,4 @@
+#pragma once
+struct B {
+ std::vector<int> Values;
+};
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/gen.inc b/clang-tools-extra/include-cleaner/test/Inputs/gen.inc
new file mode 100644
index 0000000000000..966fff546162e
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/gen.inc
@@ -0,0 +1,4 @@
+#pragma once
+struct Gen {
+ std::vector<int> Values;
+};
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/generated/gen.inc b/clang-tools-extra/include-cleaner/test/Inputs/generated/gen.inc
new file mode 100644
index 0000000000000..051180bcc7a83
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/generated/gen.inc
@@ -0,0 +1,4 @@
+#pragma once
+struct SpelledGen {
+ std::vector<int> Values;
+};
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/inner.inc b/clang-tools-extra/include-cleaner/test/Inputs/inner.inc
new file mode 100644
index 0000000000000..2f52678c091ca
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/inner.inc
@@ -0,0 +1,4 @@
+#pragma once
+struct Inner {
+ std::vector<int> Values;
+};
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/outer.inc b/clang-tools-extra/include-cleaner/test/Inputs/outer.inc
new file mode 100644
index 0000000000000..4f68e88d1730a
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/outer.inc
@@ -0,0 +1,2 @@
+#pragma once
+#include "inner.inc"
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/vector b/clang-tools-extra/include-cleaner/test/Inputs/vector
new file mode 100644
index 0000000000000..a2956ae8e54da
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/vector
@@ -0,0 +1,4 @@
+#pragma once
+namespace std {
+template <typename T> struct vector {};
+} // namespace std
diff --git a/clang-tools-extra/include-cleaner/test/fragments-block-comment.cpp b/clang-tools-extra/include-cleaner/test/fragments-block-comment.cpp
new file mode 100644
index 0000000000000..c5a8e9b73a3ef
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/fragments-block-comment.cpp
@@ -0,0 +1,6 @@
+#include <vector> /* keep me */
+#include "gen.inc"
+
+Gen G;
+
+// RUN: clang-include-cleaner -print=changes %s --fragment-headers=.*\.inc$ --fragment-dependency-comment-format='needed by {0}' -- -I%S/Inputs/ | count 0
diff --git a/clang-tools-extra/include-cleaner/test/fragments-empty-comment.cpp b/clang-tools-extra/include-cleaner/test/fragments-empty-comment.cpp
new file mode 100644
index 0000000000000..50ffc87c10969
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/fragments-empty-comment.cpp
@@ -0,0 +1,6 @@
+#include <vector>
+#include "gen.inc"
+
+Gen G;
+
+// RUN: clang-include-cleaner -print=changes %s --fragment-headers=.*\.inc$ -- -I%S/Inputs/ | count 0
diff --git a/clang-tools-extra/include-cleaner/test/fragments-multi.cpp b/clang-tools-extra/include-cleaner/test/fragments-multi.cpp
new file mode 100644
index 0000000000000..8b29a475ce151
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/fragments-multi.cpp
@@ -0,0 +1,10 @@
+#include <vector>
+#include "a.inc"
+#include "b.inc"
+
+A AValue;
+B BValue;
+
+// RUN: clang-include-cleaner -print=changes %s --fragment-headers=.*\.inc$ --fragment-dependency-comment-format='needed by {0}' -- -I%S/Inputs/ | FileCheck --check-prefix=CHANGES %s
+// CHANGES: ~ <vector> @Line:1 // needed by "a.inc", "b.inc"
+// CHANGES-NOT: - <vector>
diff --git a/clang-tools-extra/include-cleaner/test/fragments-nonrecursive.cpp b/clang-tools-extra/include-cleaner/test/fragments-nonrecursive.cpp
new file mode 100644
index 0000000000000..a34bb34623c24
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/fragments-nonrecursive.cpp
@@ -0,0 +1,5 @@
+#include <vector>
+#include "outer.inc"
+
+// RUN: clang-include-cleaner -print=changes %s --fragment-headers=.*\.inc$ -- -I%S/Inputs/ | FileCheck --check-prefix=CHANGES %s
+// CHANGES: - <vector> @Line:1
diff --git a/clang-tools-extra/include-cleaner/test/fragments-spelled-path.cpp b/clang-tools-extra/include-cleaner/test/fragments-spelled-path.cpp
new file mode 100644
index 0000000000000..45174240ab498
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/fragments-spelled-path.cpp
@@ -0,0 +1,8 @@
+#include <vector>
+#include "generated/gen.inc"
+
+SpelledGen G;
+
+// RUN: clang-include-cleaner -print=changes %s --fragment-headers='generated/gen\.inc' --fragment-dependency-comment-format='needed by {0}' -- -I%S/Inputs/ | FileCheck --check-prefix=CHANGES %s
+// CHANGES: ~ <vector> @Line:1 // needed by "generated/gen.inc"
+// CHANGES-NOT: - <vector>
diff --git a/clang-tools-extra/include-cleaner/test/fragments.cpp b/clang-tools-extra/include-cleaner/test/fragments.cpp
new file mode 100644
index 0000000000000..efa1dd52b4f48
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/fragments.cpp
@@ -0,0 +1,16 @@
+#include <vector>
+#include "gen.inc"
+
+Gen G;
+
+// RUN: clang-include-cleaner -print=changes %s --fragment-headers=.*\\.inc$ --fragment-dependency-comment-format='needed by {0}' -- -I%S/Inputs/ | FileCheck --check-prefix=CHANGES %s
+// CHANGES: ~ <vector> @Line:1 // needed by "gen.inc"
+// CHANGES-NOT: - <vector>
+
+// RUN: clang-include-cleaner -print %s --fragment-headers=.*\\.inc$ --fragment-dependency-comment-format='needed by {0}' -- -I%S/Inputs/ | FileCheck --check-prefix=PRINT %s
+// PRINT: #include <vector> // needed by "gen.inc"
+
+// RUN: cp %s %t.cpp
+// RUN: clang-include-cleaner -edit %t.cpp --fragment-headers=.*\\.inc$ --fragment-dependency-comment-format='IWYU pragma: keep' -- -I%S/Inputs/
+// RUN: FileCheck --check-prefix=EDIT %s < %t.cpp
+// EDIT: #include <vector> // IWYU pragma: keep
diff --git a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
index fefbfc3a9614d..cfd29ce8d6a65 100644
--- a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
+++ b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
@@ -31,9 +31,16 @@
namespace clang {
namespace include_cleaner {
+
namespace {
namespace cl = llvm::cl;
+llvm::SmallVector<Decl *> topLevelDecls(ASTContext &Ctx);
+std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag,
+ bool AnchorToSuffix);
+std::function<bool(const Header &)> headerFilter();
+std::function<bool(llvm::StringRef)> fragmentHeaderFilter();
+
llvm::StringRef Overview = llvm::StringLiteral(R"(
clang-include-cleaner analyzes the #include directives in source code.
@@ -73,6 +80,24 @@ cl::opt<std::string> IgnoreHeaders{
cl::cat(IncludeCleaner),
};
+cl::opt<std::string> FragmentHeaders{
+ "fragment-headers",
+ cl::desc("A comma-separated list of regular expressions matched against "
+ "normalized include paths. Matching direct includes are treated "
+ "as fragments of the main file."),
+ cl::init(""),
+ cl::cat(IncludeCleaner),
+};
+
+cl::opt<std::string> FragmentDependencyCommentFormat{
+ "fragment-dependency-comment-format",
+ cl::desc("Append this trailing comment to includes that are used only by "
+ "fragment headers. Use {0} for the matched fragment include "
+ "spellings."),
+ cl::init(""),
+ cl::cat(IncludeCleaner),
+};
+
enum class PrintStyle { Changes, Final };
cl::opt<PrintStyle> Print{
"print",
@@ -130,15 +155,23 @@ format::FormatStyle getStyle(llvm::StringRef Filename) {
class Action : public clang::ASTFrontendAction {
public:
- Action(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter,
+ Action(std::function<bool(const Header &)> HeaderFilter,
+ std::function<bool(llvm::StringRef)> FragmentHeaderFilter,
+ std::string FragmentDependencyCommentFormat,
llvm::StringMap<std::string> &EditedFiles)
- : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {}
+ : HeaderFilter(std::move(HeaderFilter)),
+ FragmentHeaderFilter(std::move(FragmentHeaderFilter)),
+ FragmentDependencyCommentFormat(
+ std::move(FragmentDependencyCommentFormat)),
+ EditedFiles(EditedFiles) {}
private:
RecordedAST AST;
RecordedPP PP;
PragmaIncludes PI;
- llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;
+ std::function<bool(const Header &)> HeaderFilter;
+ std::function<bool(llvm::StringRef)> FragmentHeaderFilter;
+ std::string FragmentDependencyCommentFormat;
llvm::StringMap<std::string> &EditedFiles;
bool BeginInvocation(CompilerInstance &CI) override {
@@ -192,9 +225,13 @@ class Action : public clang::ASTFrontendAction {
SM.getFileManager().makeAbsolutePath(AbsPath);
llvm::StringRef Code = SM.getBufferData(SM.getMainFileID());
+ AnalysisOptions AnalyzeOptions;
+ AnalyzeOptions.HeaderFilter = HeaderFilter;
+ AnalyzeOptions.FragmentHeaderFilter = FragmentHeaderFilter;
+ llvm::SmallVector<Decl *> RootDecls = topLevelDecls(*AST.Ctx);
auto Results =
- analyze(AST.Roots, PP.MacroReferences, PP.Includes, &PI,
- getCompilerInstance().getPreprocessor(), HeaderFilter);
+ analyze(RootDecls, PP.MacroReferences, PP.Includes, &PI,
+ getCompilerInstance().getPreprocessor(), AnalyzeOptions);
if (!Insert) {
llvm::errs()
@@ -213,18 +250,34 @@ class Action : public clang::ASTFrontendAction {
}
if (!Insert || DisableInsert)
- Results.Missing.clear();
+ Results.MissingIncludes.clear();
if (!Remove || DisableRemove)
Results.Unused.clear();
- std::string Final = fixIncludes(Results, AbsPath, Code, getStyle(AbsPath));
+ format::FormatStyle Style = getStyle(AbsPath);
+ FixIncludesOptions FixOptions;
+ FixOptions.FragmentDependencyCommentFormat =
+ FragmentDependencyCommentFormat;
+ IncludeFixes Fixes =
+ computeIncludeFixes(Results, AbsPath, Code, Style, FixOptions);
+ auto Positioned = cantFail(
+ format::cleanupAroundReplacements(Code, Fixes.Replacements, Style));
+ std::string Final =
+ cantFail(tooling::applyAllReplacements(Code, Positioned));
if (Print.getNumOccurrences()) {
switch (Print) {
case PrintStyle::Changes:
for (const Include *I : Results.Unused)
llvm::outs() << "- " << I->quote() << " @Line:" << I->Line << "\n";
- for (const auto &[I, _] : Results.Missing)
+ for (const auto &[I, _] : Results.MissingIncludes)
llvm::outs() << "+ " << I << "\n";
+ for (const auto &Comment : Fixes.FragmentComments) {
+ if (!Comment.Replacement)
+ continue;
+ llvm::outs() << "~ " << Comment.Preserved->quote()
+ << " @Line:" << Comment.Preserved->Line << " // "
+ << Comment.Text << "\n";
+ }
break;
case PrintStyle::Final:
llvm::outs() << Final;
@@ -232,7 +285,7 @@ class Action : public clang::ASTFrontendAction {
}
}
- if (!Results.Missing.empty() || !Results.Unused.empty())
+ if (!Fixes.Replacements.empty())
EditedFiles.try_emplace(AbsPath, Final);
}
@@ -246,17 +299,24 @@ class Action : public clang::ASTFrontendAction {
return;
}
writeHTMLReport(AST.Ctx->getSourceManager().getMainFileID(), PP.Includes,
- AST.Roots, PP.MacroReferences, *AST.Ctx,
+ topLevelDecls(*AST.Ctx), PP.MacroReferences, *AST.Ctx,
getCompilerInstance().getPreprocessor(), &PI, OS);
}
};
class ActionFactory : public tooling::FrontendActionFactory {
public:
- ActionFactory(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter)
- : HeaderFilter(HeaderFilter) {}
+ ActionFactory(std::function<bool(const Header &)> HeaderFilter,
+ std::function<bool(llvm::StringRef)> FragmentHeaderFilter,
+ std::string FragmentDependencyCommentFormat)
+ : HeaderFilter(std::move(HeaderFilter)),
+ FragmentHeaderFilter(std::move(FragmentHeaderFilter)),
+ FragmentDependencyCommentFormat(
+ std::move(FragmentDependencyCommentFormat)) {}
std::unique_ptr<clang::FrontendAction> create() override {
- return std::make_unique<Action>(HeaderFilter, EditedFiles);
+ return std::make_unique<Action>(HeaderFilter, FragmentHeaderFilter,
+ FragmentDependencyCommentFormat,
+ EditedFiles);
}
const llvm::StringMap<std::string> &editedFiles() const {
@@ -264,19 +324,31 @@ class ActionFactory : public tooling::FrontendActionFactory {
}
private:
- llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;
+ std::function<bool(const Header &)> HeaderFilter;
+ std::function<bool(llvm::StringRef)> FragmentHeaderFilter;
+ std::string FragmentDependencyCommentFormat;
// Map from file name to final code with the include edits applied.
llvm::StringMap<std::string> EditedFiles;
};
+llvm::SmallVector<Decl *> topLevelDecls(ASTContext &Ctx) {
+ llvm::SmallVector<Decl *> Decls;
+ for (Decl *D : Ctx.getTranslationUnitDecl()->decls())
+ Decls.push_back(D);
+ return Decls;
+}
+
// Compiles a regex list into a function that return true if any match a header.
// Prints and returns nullptr if any regexes are invalid.
-std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag) {
+std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag,
+ bool AnchorToSuffix) {
auto FilterRegs = std::make_shared<std::vector<llvm::Regex>>();
llvm::SmallVector<llvm::StringRef> Headers;
RegexFlag.split(Headers, ',', -1, /*KeepEmpty=*/false);
for (auto HeaderPattern : Headers) {
- std::string AnchoredPattern = "(" + HeaderPattern.str() + ")$";
+ std::string AnchoredPattern = HeaderPattern.str();
+ if (AnchorToSuffix)
+ AnchoredPattern = "(" + AnchoredPattern + ")$";
llvm::Regex CompiledRegex(AnchoredPattern);
std::string RegexError;
if (!CompiledRegex.isValid(RegexError)) {
@@ -295,21 +367,26 @@ std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag) {
};
}
-std::function<bool(llvm::StringRef)> headerFilter() {
- auto OnlyMatches = matchesAny(OnlyHeaders);
- auto IgnoreMatches = matchesAny(IgnoreHeaders);
+std::function<bool(const Header &)> headerFilter() {
+ auto OnlyMatches = matchesAny(OnlyHeaders, /*AnchorToSuffix=*/true);
+ auto IgnoreMatches = matchesAny(IgnoreHeaders, /*AnchorToSuffix=*/true);
if (!OnlyMatches || !IgnoreMatches)
return nullptr;
- return [OnlyMatches, IgnoreMatches](llvm::StringRef Header) {
- if (!OnlyHeaders.empty() && !OnlyMatches(Header))
+ return [OnlyMatches, IgnoreMatches](const Header &Header) {
+ llvm::StringRef Path = Header.resolvedPath();
+ if (!OnlyHeaders.empty() && !OnlyMatches(Path))
return true;
- if (!IgnoreHeaders.empty() && IgnoreMatches(Header))
+ if (!IgnoreHeaders.empty() && IgnoreMatches(Path))
return true;
return false;
};
}
+std::function<bool(llvm::StringRef)> fragmentHeaderFilter() {
+ return matchesAny(FragmentHeaders, /*AnchorToSuffix=*/false);
+}
+
// Maps absolute path of each files of each compilation commands to the
// absolute path of the input file.
llvm::Expected<std::map<std::string, std::string, std::less<>>>
@@ -388,9 +465,11 @@ int main(int argc, const char **argv) {
clang::tooling::ClangTool Tool(CDB, OptionsParser->getSourcePathList());
auto HeaderFilter = headerFilter();
- if (!HeaderFilter)
+ auto FragmentHeaderFilter = fragmentHeaderFilter();
+ if (!HeaderFilter || !FragmentHeaderFilter)
return 1; // error already reported.
- ActionFactory Factory(HeaderFilter);
+ ActionFactory Factory(HeaderFilter, FragmentHeaderFilter,
+ FragmentDependencyCommentFormat);
auto ErrorCode = Tool.run(&Factory);
if (Edit) {
for (const auto &NameAndContent : Factory.editedFiles()) {
diff --git a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp
index ba5a3fbbcaeb2..7fa7317294586 100644
--- a/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp
@@ -39,6 +39,14 @@
#include <vector>
namespace clang::include_cleaner {
+
+static std::vector<Decl *> topLevelDecls(TestAST &AST) {
+ std::vector<Decl *> Decls;
+ for (auto *D : AST.context().getTranslationUnitDecl()->decls())
+ Decls.push_back(D);
+ return Decls;
+}
+
namespace {
using testing::_;
using testing::AllOf;
@@ -269,7 +277,8 @@ int x = a + c;
const Include *B = PP.Includes.atLine(3);
ASSERT_EQ(B->Spelled, "b.h");
- EXPECT_THAT(Results.Missing, ElementsAre(Pair("\"c.h\"", Header(CHeader))));
+ EXPECT_THAT(Results.MissingIncludes,
+ ElementsAre(Pair("\"c.h\"", Header(CHeader))));
EXPECT_THAT(Results.Unused, ElementsAre(B));
}
@@ -317,7 +326,7 @@ TEST_F(AnalyzeTest, ResourceDirIsIgnored) {
TestAST AST(Inputs);
auto Results = analyze({}, {}, PP.Includes, &PI, AST.preprocessor());
EXPECT_THAT(Results.Unused, testing::IsEmpty());
- EXPECT_THAT(Results.Missing, testing::IsEmpty());
+ EXPECT_THAT(Results.MissingIncludes, testing::IsEmpty());
}
TEST_F(AnalyzeTest, DifferentHeaderSameSpelling) {
@@ -342,7 +351,7 @@ TEST_F(AnalyzeTest, DifferentHeaderSameSpelling) {
DeclsInTU.push_back(D);
auto Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor());
EXPECT_THAT(Results.Unused, testing::IsEmpty());
- EXPECT_THAT(Results.Missing, testing::IsEmpty());
+ EXPECT_THAT(Results.MissingIncludes, testing::IsEmpty());
}
TEST_F(AnalyzeTest, SpellingIncludesWithSymlinks) {
@@ -368,33 +377,361 @@ TEST_F(AnalyzeTest, SpellingIncludesWithSymlinks) {
/*ModificationTime=*/{});
TestAST AST(Inputs);
- std::vector<Decl *> DeclsInTU;
- for (auto *D : AST.context().getTranslationUnitDecl()->decls())
- DeclsInTU.push_back(D);
+ std::vector<Decl *> DeclsInTU = topLevelDecls(AST);
auto Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor());
// Check that we're spelling header using the symlink, and not underlying
// path.
- EXPECT_THAT(Results.Missing, testing::ElementsAre(Pair("\"inner.h\"", _)));
+ EXPECT_THAT(Results.MissingIncludes,
+ testing::ElementsAre(Pair("\"inner.h\"", _)));
// header.h should be unused.
EXPECT_THAT(Results.Unused, Not(testing::IsEmpty()));
{
// Make sure filtering is also applied to symlink, not underlying file.
- auto HeaderFilter = [](llvm::StringRef Path) { return Path == "inner.h"; };
- Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor(),
- HeaderFilter);
- EXPECT_THAT(Results.Missing, testing::ElementsAre(Pair("\"inner.h\"", _)));
+ AnalysisOptions Options;
+ Options.HeaderFilter = [](const Header &H) {
+ return H.resolvedPath() == "inner.h";
+ };
+ Results =
+ analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor(), Options);
+ EXPECT_THAT(Results.MissingIncludes,
+ testing::ElementsAre(Pair("\"inner.h\"", _)));
// header.h should be unused.
EXPECT_THAT(Results.Unused, Not(testing::IsEmpty()));
}
{
- auto HeaderFilter = [](llvm::StringRef Path) { return Path == "header.h"; };
- Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor(),
- HeaderFilter);
+ AnalysisOptions Options;
+ Options.HeaderFilter = [](const Header &H) {
+ return H.resolvedPath() == "header.h";
+ };
+ Results =
+ analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor(), Options);
// header.h should be ignored now.
EXPECT_THAT(Results.Unused, Not(testing::IsEmpty()));
- EXPECT_THAT(Results.Missing, testing::ElementsAre(Pair("\"inner.h\"", _)));
+ EXPECT_THAT(Results.MissingIncludes,
+ testing::ElementsAre(Pair("\"inner.h\"", _)));
+ }
+}
+
+TEST_F(AnalyzeTest, FragmentDeclUsePreservesInclude) {
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "gen.inc"
+Holder H;
+)cpp";
+ Inputs.ExtraFiles["support.h"] = guard(R"cpp(
+ namespace support {
+ template <typename> struct vector {};
+ }
+ )cpp");
+ Inputs.ExtraFiles["gen.inc"] = guard(R"cpp(
+ struct Holder {
+ support::vector<int> Values;
+ };
+ )cpp");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Vector = PP.Includes.atLine(2);
+ const Include *Fragment = PP.Includes.atLine(3);
+ ASSERT_NE(Vector, nullptr);
+ ASSERT_NE(Fragment, nullptr);
+ EXPECT_THAT(Results.Unused, testing::IsEmpty());
+ ASSERT_THAT(Results.FragmentDependencies, testing::SizeIs(1));
+ EXPECT_EQ(Results.FragmentDependencies.front().Preserved, Vector);
+ EXPECT_THAT(Results.FragmentDependencies.front().Fragments,
+ ElementsAre(Fragment));
+}
+
+TEST_F(AnalyzeTest, FragmentDependencyExcludedWhenAlsoUsedInMainFile) {
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "gen.inc"
+support::vector<int> MainValues;
+)cpp";
+ Inputs.ExtraFiles["support.h"] = guard(R"cpp(
+ namespace support {
+ template <typename> struct vector {};
}
+ )cpp");
+ Inputs.ExtraFiles["gen.inc"] = guard(R"cpp(
+ struct Holder {
+ support::vector<int> Values;
+ };
+ )cpp");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Support = PP.Includes.atLine(2);
+ ASSERT_NE(Support, nullptr);
+ EXPECT_THAT(Results.Unused, testing::Not(Contains(Support)));
+ EXPECT_THAT(Results.FragmentDependencies, testing::IsEmpty());
+}
+
+TEST_F(AnalyzeTest, MultipleFragmentsPreserveSingleInclude) {
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "a.inc"
+#include "b.inc"
+A AValue;
+B BValue;
+)cpp";
+ Inputs.ExtraFiles["support.h"] = guard(R"cpp(
+ namespace support {
+ template <typename> struct vector {};
+ }
+ )cpp");
+ Inputs.ExtraFiles["a.inc"] = guard(R"cpp(
+ struct A {
+ support::vector<int> Values;
+ };
+ )cpp");
+ Inputs.ExtraFiles["b.inc"] = guard(R"cpp(
+ struct B {
+ support::vector<int> Values;
+ };
+ )cpp");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Support = PP.Includes.atLine(2);
+ const Include *FragmentA = PP.Includes.atLine(3);
+ const Include *FragmentB = PP.Includes.atLine(4);
+ ASSERT_NE(Support, nullptr);
+ ASSERT_NE(FragmentA, nullptr);
+ ASSERT_NE(FragmentB, nullptr);
+ EXPECT_THAT(Results.Unused, testing::IsEmpty());
+ ASSERT_THAT(Results.FragmentDependencies, testing::SizeIs(1));
+ EXPECT_EQ(Results.FragmentDependencies.front().Preserved, Support);
+ EXPECT_THAT(Results.FragmentDependencies.front().Fragments,
+ ElementsAre(FragmentA, FragmentB));
+}
+
+TEST_F(AnalyzeTest, FragmentDependencyDeduplicatesFragmentReason) {
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "gen.inc"
+Holder H;
+)cpp";
+ Inputs.ExtraFiles["support.h"] = guard(R"cpp(
+ namespace support {
+ template <typename> struct vector {};
+ }
+ )cpp");
+ Inputs.ExtraFiles["gen.inc"] = guard(R"cpp(
+ struct Holder {
+ support::vector<int> First;
+ support::vector<int> Second;
+ };
+ )cpp");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Fragment = PP.Includes.atLine(3);
+ ASSERT_NE(Fragment, nullptr);
+ ASSERT_THAT(Results.FragmentDependencies, testing::SizeIs(1));
+ EXPECT_THAT(Results.FragmentDependencies.front().Fragments,
+ ElementsAre(Fragment));
+}
+
+TEST_F(AnalyzeTest, DuplicateFragmentIncludesPreserveIncludeWithoutProvenance) {
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "gen.inc"
+#include "./gen.inc"
+)cpp";
+ Inputs.ExtraFiles["support.h"] = guard("struct SupportType {};");
+ Inputs.ExtraFiles["gen.inc"] = "void use(SupportType);\n";
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Support = PP.Includes.atLine(2);
+ ASSERT_NE(Support, nullptr);
+ EXPECT_THAT(Results.Unused, testing::Not(Contains(Support)));
+ EXPECT_THAT(Results.FragmentDependencies, testing::IsEmpty());
+}
+
+TEST_F(AnalyzeTest, FragmentMacroUsePreservesInclude) {
+ Inputs.Code = R"cpp(
+#include "macro.h"
+#include "gen.inc"
+)cpp";
+ Inputs.ExtraFiles["macro.h"] = guard("#define FOO 42");
+ Inputs.ExtraFiles["gen.inc"] = guard("int Value = FOO;");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Macro = PP.Includes.atLine(2);
+ const Include *Fragment = PP.Includes.atLine(3);
+ ASSERT_NE(Macro, nullptr);
+ ASSERT_NE(Fragment, nullptr);
+ EXPECT_THAT(Results.Unused, testing::IsEmpty());
+ ASSERT_THAT(Results.FragmentDependencies, testing::SizeIs(1));
+ EXPECT_EQ(Results.FragmentDependencies.front().Preserved, Macro);
+ EXPECT_THAT(Results.FragmentDependencies.front().Fragments,
+ ElementsAre(Fragment));
+}
+
+TEST_F(AnalyzeTest, FragmentHeadersAreNotRecursive) {
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "outer.inc"
+)cpp";
+ Inputs.ExtraFiles["support.h"] = guard(R"cpp(
+ namespace support {
+ template <typename> struct vector {};
+ }
+ )cpp");
+ Inputs.ExtraFiles["outer.inc"] = guard(R"cpp(
+ #include "inner.inc"
+ )cpp");
+ Inputs.ExtraFiles["inner.inc"] = guard(R"cpp(
+ struct Inner {
+ support::vector<int> Values;
+ };
+ )cpp");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Support = PP.Includes.atLine(2);
+ ASSERT_NE(Support, nullptr);
+ EXPECT_THAT(Results.Unused, Contains(Support));
+ EXPECT_THAT(Results.FragmentDependencies, testing::IsEmpty());
+}
+
+TEST_F(AnalyzeTest, FragmentHeaderMatcherFallsBackToSpelledPath) {
+ Inputs.ExtraArgs.push_back("-I/headers");
+ Inputs.Code = R"cpp(
+#include "support.h"
+#include "generated/gen.inc"
+Holder H;
+)cpp";
+ Inputs.ExtraFiles["/headers/support.h"] = guard(R"cpp(
+ namespace support {
+ template <typename> struct vector {};
+ }
+ )cpp");
+ Inputs.ExtraFiles["/headers/generated/gen.inc"] = guard(R"cpp(
+ struct Holder {
+ support::vector<int> Values;
+ };
+ )cpp");
+
+ TestAST AST(Inputs);
+ const Include *Fragment = PP.Includes.atLine(3);
+ ASSERT_NE(Fragment, nullptr);
+ ASSERT_TRUE(Fragment->Resolved.has_value());
+ EXPECT_NE(normalizePath(Fragment->Resolved->getName()).str(),
+ "generated/gen.inc");
+
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path == "generated/gen.inc";
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Support = PP.Includes.atLine(2);
+ ASSERT_NE(Support, nullptr);
+ ASSERT_THAT(Results.FragmentDependencies, testing::SizeIs(1));
+ EXPECT_EQ(Results.FragmentDependencies.front().Preserved, Support);
+ EXPECT_THAT(Results.FragmentDependencies.front().Fragments,
+ ElementsAre(Fragment));
+}
+
+TEST_F(AnalyzeTest, FragmentMissingIncludeRecordsOrigin) {
+ Inputs.Code = R"cpp(
+#include "gen.inc"
+)cpp";
+ Inputs.ExtraFiles["foo.h"] = guard("int foo();");
+ Inputs.ExtraFiles["gen.inc"] = guard(R"cpp(
+ #include "foo.h"
+ int Value = foo();
+ )cpp");
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ const Include *Fragment = PP.Includes.atLine(2);
+ ASSERT_NE(Fragment, nullptr);
+ ASSERT_THAT(Results.MissingRefs, testing::SizeIs(1));
+ EXPECT_EQ(Results.MissingRefs.front().Origin,
+ SymbolReferenceOrigin::Fragment);
+ EXPECT_EQ(Results.MissingRefs.front().FragmentInclude, Fragment);
+ EXPECT_EQ(Results.MissingIncludes.front().first, "\"foo.h\"");
+}
+
+TEST_F(AnalyzeTest, DuplicateFragmentIncludesSuppressMissingProvenance) {
+ Inputs.Code = R"cpp(
+#include "gen.inc"
+#include "./gen.inc"
+)cpp";
+ Inputs.ExtraFiles["foo.h"] = guard("int foo();");
+ Inputs.ExtraFiles["gen.inc"] = R"cpp(
+ #include "foo.h"
+ auto useFoo() -> decltype(foo());
+ )cpp";
+
+ TestAST AST(Inputs);
+ AnalysisOptions Options;
+ Options.FragmentHeaderFilter = [](llvm::StringRef Path) {
+ return Path.ends_with(".inc");
+ };
+ auto Results = analyze(topLevelDecls(AST), PP.MacroReferences, PP.Includes,
+ &PI, AST.preprocessor(), Options);
+
+ ASSERT_FALSE(Results.MissingRefs.empty());
+ EXPECT_EQ(Results.MissingRefs.front().Origin,
+ SymbolReferenceOrigin::Fragment);
+ EXPECT_EQ(Results.MissingRefs.front().FragmentInclude, nullptr);
+ EXPECT_EQ(Results.MissingIncludes.front().first, "\"foo.h\"");
}
// Make sure that the references to implicit operator new/delete are reported as
@@ -422,7 +759,7 @@ TEST_F(AnalyzeTest, ImplicitOperatorNewDeleteNotMissing) {
for (auto *D : AST.context().getTranslationUnitDecl()->decls())
DeclsInTU.push_back(D);
auto Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor());
- EXPECT_THAT(Results.Missing, testing::IsEmpty());
+ EXPECT_THAT(Results.MissingIncludes, testing::IsEmpty());
}
TEST_F(AnalyzeTest, ImplicitOperatorNewDeleteNotUnused) {
@@ -467,14 +804,14 @@ TEST(FixIncludes, Basic) {
Inc.add(I);
AnalysisResults Results;
- Results.Missing.emplace_back("\"aa.h\"", Header(""));
- Results.Missing.emplace_back("\"ab.h\"", Header(""));
- Results.Missing.emplace_back("<e.h>", Header(""));
+ Results.MissingIncludes.emplace_back("\"aa.h\"", Header(""));
+ Results.MissingIncludes.emplace_back("\"ab.h\"", Header(""));
+ Results.MissingIncludes.emplace_back("<e.h>", Header(""));
Results.Unused.push_back(Inc.atLine(3));
Results.Unused.push_back(Inc.atLine(4));
EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()),
-R"cpp(#include "d.h"
+ R"cpp(#include "d.h"
#include "a.h"
#include "aa.h"
#include "ab.h"
@@ -482,13 +819,210 @@ R"cpp(#include "d.h"
)cpp");
Results = {};
- Results.Missing.emplace_back("\"d.h\"", Header(""));
+ Results.MissingIncludes.emplace_back("\"d.h\"", Header(""));
Code = R"cpp(#include "a.h")cpp";
EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()),
-R"cpp(#include "d.h"
+ R"cpp(#include "d.h"
#include "a.h")cpp");
}
+TEST(FixIncludes, FragmentDependencyComments) {
+ llvm::StringRef Code = R"cpp(#include "a.h"
+#include "gen.inc"
+#include "existing.h" // needed by "gen.inc"
+#include "conflict.h" // keep me
+)cpp";
+
+ Includes Inc;
+ Include A;
+ A.Spelled = "a.h";
+ A.Line = 1;
+ Inc.add(A);
+ Include Gen;
+ Gen.Spelled = "gen.inc";
+ Gen.Line = 2;
+ Inc.add(Gen);
+ Include Existing;
+ Existing.Spelled = "existing.h";
+ Existing.Line = 3;
+ Inc.add(Existing);
+ Include Conflict;
+ Conflict.Spelled = "conflict.h";
+ Conflict.Line = 4;
+ Inc.add(Conflict);
+
+ AnalysisResults Results;
+ Results.FragmentDependencies.push_back({Inc.atLine(1), {Inc.atLine(2)}});
+ Results.FragmentDependencies.push_back({Inc.atLine(3), {Inc.atLine(2)}});
+ Results.FragmentDependencies.push_back({Inc.atLine(4), {Inc.atLine(2)}});
+
+ FixIncludesOptions Options;
+ Options.FragmentDependencyCommentFormat = "needed by {0}";
+ IncludeFixes Fixes = computeIncludeFixes(Results, "d.cc", Code,
+ format::getLLVMStyle(), Options);
+
+ ASSERT_THAT(Fixes.FragmentComments, testing::SizeIs(3));
+ EXPECT_EQ(Fixes.FragmentComments[0].Status,
+ FragmentDependencyCommentStatus::CanInsert);
+ ASSERT_TRUE(Fixes.FragmentComments[0].Replacement.has_value());
+ EXPECT_EQ(Fixes.FragmentComments[1].Status,
+ FragmentDependencyCommentStatus::AlreadyPresent);
+ EXPECT_FALSE(Fixes.FragmentComments[1].Replacement.has_value());
+ EXPECT_EQ(Fixes.FragmentComments[2].Status,
+ FragmentDependencyCommentStatus::ConflictingComment);
+ EXPECT_FALSE(Fixes.FragmentComments[2].Replacement.has_value());
+
+ EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle(), Options),
+ R"cpp(#include "a.h" // needed by "gen.inc"
+#include "gen.inc"
+#include "existing.h" // needed by "gen.inc"
+#include "conflict.h" // keep me
+)cpp");
+}
+
+TEST(FixIncludes, FragmentDependencyCommentsEmptyFormat) {
+ llvm::StringRef Code = R"cpp(#include "a.h"
+#include "gen.inc"
+)cpp";
+
+ Includes Inc;
+ Include A;
+ A.Spelled = "a.h";
+ A.Line = 1;
+ Inc.add(A);
+ Include Gen;
+ Gen.Spelled = "gen.inc";
+ Gen.Line = 2;
+ Inc.add(Gen);
+
+ AnalysisResults Results;
+ Results.FragmentDependencies.push_back({Inc.atLine(1), {Inc.atLine(2)}});
+
+ IncludeFixes Fixes =
+ computeIncludeFixes(Results, "d.cc", Code, format::getLLVMStyle(), {});
+ EXPECT_THAT(Fixes.FragmentComments, testing::IsEmpty());
+ EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()), Code);
+}
+
+TEST(FixIncludes, FragmentDependencyCommentsLiteralFormat) {
+ llvm::StringRef Code = R"cpp(#include "a.h"
+#include "gen.inc"
+)cpp";
+
+ Includes Inc;
+ Include A;
+ A.Spelled = "a.h";
+ A.Line = 1;
+ Inc.add(A);
+ Include Gen;
+ Gen.Spelled = "gen.inc";
+ Gen.Line = 2;
+ Inc.add(Gen);
+
+ AnalysisResults Results;
+ Results.FragmentDependencies.push_back({Inc.atLine(1), {Inc.atLine(2)}});
+
+ FixIncludesOptions Options;
+ Options.FragmentDependencyCommentFormat = "IWYU pragma: keep";
+ IncludeFixes Fixes = computeIncludeFixes(Results, "d.cc", Code,
+ format::getLLVMStyle(), Options);
+ ASSERT_THAT(Fixes.FragmentComments, testing::SizeIs(1));
+ EXPECT_EQ(Fixes.FragmentComments.front().Text, "IWYU pragma: keep");
+ EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle(), Options),
+ R"cpp(#include "a.h" // IWYU pragma: keep
+#include "gen.inc"
+)cpp");
+}
+
+TEST(FixIncludes, FragmentDependencyCommentsMultipleFragments) {
+ llvm::StringRef Code = R"cpp(#include "a.h"
+#include "a.inc"
+#include "b.inc"
+)cpp";
+
+ Includes Inc;
+ Include A;
+ A.Spelled = "a.h";
+ A.Line = 1;
+ Inc.add(A);
+ Include FragmentA;
+ FragmentA.Spelled = "a.inc";
+ FragmentA.Line = 2;
+ Inc.add(FragmentA);
+ Include FragmentB;
+ FragmentB.Spelled = "b.inc";
+ FragmentB.Line = 3;
+ Inc.add(FragmentB);
+
+ AnalysisResults Results;
+ Results.FragmentDependencies.push_back(
+ {Inc.atLine(1), {Inc.atLine(2), Inc.atLine(3)}});
+
+ FixIncludesOptions Options;
+ Options.FragmentDependencyCommentFormat = "needed by {0}";
+ IncludeFixes Fixes = computeIncludeFixes(Results, "d.cc", Code,
+ format::getLLVMStyle(), Options);
+ ASSERT_THAT(Fixes.FragmentComments, testing::SizeIs(1));
+ EXPECT_EQ(Fixes.FragmentComments.front().Text,
+ "needed by \"a.inc\", \"b.inc\"");
+}
+
+TEST(FixIncludes, FragmentDependencyCommentsBlockCommentConflict) {
+ llvm::StringRef Code = R"cpp(#include "a.h" /* keep me */
+#include "gen.inc"
+)cpp";
+
+ Includes Inc;
+ Include A;
+ A.Spelled = "a.h";
+ A.Line = 1;
+ Inc.add(A);
+ Include Gen;
+ Gen.Spelled = "gen.inc";
+ Gen.Line = 2;
+ Inc.add(Gen);
+
+ AnalysisResults Results;
+ Results.FragmentDependencies.push_back({Inc.atLine(1), {Inc.atLine(2)}});
+
+ FixIncludesOptions Options;
+ Options.FragmentDependencyCommentFormat = "needed by {0}";
+ IncludeFixes Fixes = computeIncludeFixes(Results, "d.cc", Code,
+ format::getLLVMStyle(), Options);
+ ASSERT_THAT(Fixes.FragmentComments, testing::SizeIs(1));
+ EXPECT_EQ(Fixes.FragmentComments.front().Status,
+ FragmentDependencyCommentStatus::ConflictingComment);
+ EXPECT_FALSE(Fixes.FragmentComments.front().Replacement.has_value());
+}
+
+TEST(FixIncludes, FragmentDependencyCommentsCRLFAndTrailingWhitespace) {
+ llvm::StringRef Code = "#include \"a.h\" \r\n#include \"gen.inc\"\r\n";
+
+ Includes Inc;
+ Include A;
+ A.Spelled = "a.h";
+ A.Line = 1;
+ Inc.add(A);
+ Include Gen;
+ Gen.Spelled = "gen.inc";
+ Gen.Line = 2;
+ Inc.add(Gen);
+
+ AnalysisResults Results;
+ Results.FragmentDependencies.push_back({Inc.atLine(1), {Inc.atLine(2)}});
+
+ FixIncludesOptions Options;
+ Options.FragmentDependencyCommentFormat = "needed by {0}";
+ IncludeFixes Fixes = computeIncludeFixes(Results, "d.cc", Code,
+ format::getLLVMStyle(), Options);
+ ASSERT_THAT(Fixes.FragmentComments, testing::SizeIs(1));
+ ASSERT_TRUE(Fixes.FragmentComments.front().Replacement.has_value());
+ size_t ExpectedOffset = Code.find(" \r\n");
+ ASSERT_NE(ExpectedOffset, llvm::StringRef::npos);
+ EXPECT_EQ(Fixes.FragmentComments.front().Replacement->getOffset(),
+ ExpectedOffset);
+}
+
MATCHER_P3(expandedAt, FileID, Offset, SM, "") {
auto [ExpanedFileID, ExpandedOffset] = SM->getDecomposedExpansionLoc(arg);
return ExpanedFileID == FileID && ExpandedOffset == Offset;
More information about the cfe-commits
mailing list