[clang-tools-extra] [clang-include-cleaner] Add --mapping-file support for IWYU .imp mapping files. (PR #195987)
Dmitrii Kuragin via cfe-commits
cfe-commits at lists.llvm.org
Wed May 6 07:15:50 PDT 2026
https://github.com/sstepashka updated https://github.com/llvm/llvm-project/pull/195987
>From 67b3761a7388620f1b4e3bdbed9be384bc07ec93 Mon Sep 17 00:00:00 2001
From: Dmitrii Kuragin <kuraginmail at gmail.com>
Date: Tue, 5 May 2026 09:36:55 -0700
Subject: [PATCH] [clang-include-cleaner] Add --mapping-file support for IWYU
.imp mapping files.
It is (probably) a good step toward adding support in clangd request: https://github.com/clangd/clangd/issues/1276
---
.../clang-include-cleaner/MappingFile.h | 65 +++
.../include/clang-include-cleaner/Record.h | 34 +-
.../include-cleaner/lib/CMakeLists.txt | 3 +-
.../include-cleaner/lib/FindHeaders.cpp | 23 +
.../include-cleaner/lib/MappingFile.cpp | 239 +++++++++++
.../include-cleaner/lib/Record.cpp | 90 +++-
.../test/Inputs/private_header.h | 2 +
.../test/Inputs/public_header.h | 1 +
.../test/Inputs/umbrella_inc.h | 2 +
.../include-cleaner/test/mapping-file.cpp | 43 ++
.../include-cleaner/tool/IncludeCleaner.cpp | 68 ++-
.../include-cleaner/unittests/CMakeLists.txt | 1 +
.../unittests/MappingFileTest.cpp | 393 ++++++++++++++++++
13 files changed, 953 insertions(+), 11 deletions(-)
create mode 100644 clang-tools-extra/include-cleaner/include/clang-include-cleaner/MappingFile.h
create mode 100644 clang-tools-extra/include-cleaner/lib/MappingFile.cpp
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/private_header.h
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/public_header.h
create mode 100644 clang-tools-extra/include-cleaner/test/Inputs/umbrella_inc.h
create mode 100644 clang-tools-extra/include-cleaner/test/mapping-file.cpp
create mode 100644 clang-tools-extra/include-cleaner/unittests/MappingFileTest.cpp
diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/MappingFile.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/MappingFile.h
new file mode 100644
index 0000000000000..e0dfa390e7035
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/MappingFile.h
@@ -0,0 +1,65 @@
+//===--- MappingFile.h - IWYU mapping file support ------------------C++-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Support for IWYU mapping files (.imp).
+// Format:
+// https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUMappings.md
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef CLANG_INCLUDE_CLEANER_MAPPINGFILE_H
+#define CLANG_INCLUDE_CLEANER_MAPPINGFILE_H
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/Support/Error.h"
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace clang::include_cleaner {
+
+/// Parsed data from IWYU mapping files (.imp).
+///
+/// Mapping files declare two kinds of relationships:
+/// - Include mappings: "when a symbol comes from <private.h>, use <public.h>
+/// instead."
+/// - Symbol mappings: "when symbol Foo is referenced, include <foo.h>."
+struct MappingFile {
+ /// Maps a private header path (bare, no brackets) to the public header
+ /// spelling that should be used instead, e.g. "foo/detail.h" -> "<foo.h>".
+ llvm::StringMap<std::string> IncludeMappings;
+
+ /// Maps a symbol name (possibly qualified) to the header spelling that
+ /// should be included for it, e.g. "NULL" -> "<stddef.h>".
+ llvm::StringMap<std::string> SymbolMappings;
+
+ /// Regex patterns for include mappings (from "@<...>" entries).
+ /// Each entry is (raw_regex, public_header_spelling).
+ /// Patterns are matched against path suffixes, e.g. "AE/.*" from "@<AE/.*>".
+ std::vector<std::pair<std::string, std::string>> IncludeRegexPatterns;
+
+ /// Merges \p Other into this mapping. For duplicate keys, \p Other wins.
+ void merge(MappingFile Other);
+};
+
+/// Parse one or more IWYU mapping files (.imp) into \p Result.
+///
+/// Each file is a YAML array of objects with "include", "symbol", or "ref"
+/// keys. The format is JSON-compatible YAML: supports unquoted private/public
+/// visibility values, trailing commas, and # line comments.
+/// "ref" entries are resolved relative to the file that contains them.
+///
+/// Multiple files are merged; later entries for the same key win.
+/// Returns an error if any file cannot be read or has invalid syntax.
+llvm::Expected<MappingFile>
+parseMappingFiles(llvm::ArrayRef<std::string> Paths);
+
+} // namespace clang::include_cleaner
+
+#endif // CLANG_INCLUDE_CLEANER_MAPPINGFILE_H
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..a462fa2fdd36b 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
@@ -17,15 +17,19 @@
#ifndef CLANG_INCLUDE_CLEANER_RECORD_H
#define CLANG_INCLUDE_CLEANER_RECORD_H
+#include "clang-include-cleaner/MappingFile.h"
#include "clang-include-cleaner/Types.h"
#include "clang/Basic/SourceLocation.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Allocator.h"
#include "llvm/Support/FileSystem/UniqueID.h"
#include <memory>
+#include <string>
+#include <utility>
#include <vector>
namespace clang {
@@ -62,9 +66,25 @@ class PragmaIncludes {
bool shouldKeep(const FileEntry *FE) const;
/// Returns the public mapping include for the given physical header file.
- /// Returns "" if there is none.
+ /// Returns "" if there is none. Checks both IWYU pragmas in source and
+ /// externally-loaded mapping files.
llvm::StringRef getPublic(const FileEntry *File) const;
+ /// Loads external header and symbol mappings from a parsed MappingFile.
+ /// These supplement IWYU pragmas found in source code.
+ void loadMapping(const MappingFile &Mapping);
+
+ /// Returns the header spelling for a symbol name from external mapping files.
+ /// Returns "" if there is no mapping. Checks both the simple name and any
+ /// qualified name provided.
+ llvm::StringRef getExternalSymbolHeader(llvm::StringRef SymbolName) const;
+
+ /// Returns the public header spelling for an include spelled as \p BarePath
+ /// (without angle-bracket or quote delimiters, e.g.
+ /// "CarbonCore/MacMemory.h"). Checks exact external include mappings first,
+ /// then regex patterns. Returns "" if no mapping is found.
+ llvm::StringRef getPublicForSpelling(llvm::StringRef BarePath) const;
+
/// Returns all direct exporter headers for the given header file.
/// Returns empty if there is none.
llvm::SmallVector<FileEntryRef> getExporters(const FileEntry *File,
@@ -117,6 +137,18 @@ class PragmaIncludes {
std::vector<std::shared_ptr<const llvm::BumpPtrAllocator>> Arena;
// FIXME: add support for clang use_instead pragma
+
+ /// External include mappings from mapping files.
+ /// Key: bare path suffix (e.g. "foo/bar.h"); Value: public header spelling.
+ llvm::StringMap<std::string> ExternalIncludes;
+
+ /// Validated, anchored regex patterns from "@<...>" include mapping entries.
+ /// Stored as strings; compiled at match time to keep PragmaIncludes copyable.
+ std::vector<std::pair<std::string, std::string>> ExternalIncludeRegexMappings;
+
+ /// External symbol mappings from mapping files.
+ /// Key: symbol name, possibly qualified; Value: public header spelling.
+ llvm::StringMap<std::string> ExternalSymbols;
};
/// Recorded main-file parser events relevant to include-cleaner.
diff --git a/clang-tools-extra/include-cleaner/lib/CMakeLists.txt b/clang-tools-extra/include-cleaner/lib/CMakeLists.txt
index bb92f468027ca..2431acad27c78 100644
--- a/clang-tools-extra/include-cleaner/lib/CMakeLists.txt
+++ b/clang-tools-extra/include-cleaner/lib/CMakeLists.txt
@@ -2,10 +2,11 @@ set(LLVM_LINK_COMPONENTS Support)
add_clang_library(clangIncludeCleaner STATIC
Analysis.cpp
- IncludeSpeller.cpp
FindHeaders.cpp
HTMLReport.cpp
+ IncludeSpeller.cpp
LocateSymbol.cpp
+ MappingFile.cpp
Record.cpp
Types.cpp
WalkAST.cpp
diff --git a/clang-tools-extra/include-cleaner/lib/FindHeaders.cpp b/clang-tools-extra/include-cleaner/lib/FindHeaders.cpp
index b96d9a70728c2..3301e0556bcb9 100644
--- a/clang-tools-extra/include-cleaner/lib/FindHeaders.cpp
+++ b/clang-tools-extra/include-cleaner/lib/FindHeaders.cpp
@@ -29,6 +29,7 @@
#include <optional>
#include <queue>
#include <set>
+#include <string>
#include <utility>
namespace clang::include_cleaner {
@@ -253,6 +254,28 @@ llvm::SmallVector<Header> headersForSymbol(const Symbol &S,
for (auto &Loc : locateSymbol(S, PP.getLangOpts()))
Headers.append(applyHints(findHeaders(Loc, SM, PI), Loc.Hint));
}
+
+ // Apply external symbol mappings from mapping files. We check both the
+ // simple (unqualified) name and, for declarations, the fully-qualified name.
+ if (PI) {
+ auto AddSymbolMapping = [&](llvm::StringRef Name) {
+ if (Name.empty())
+ return;
+ llvm::StringRef Spelling = PI->getExternalSymbolHeader(Name);
+ if (!Spelling.empty())
+ Headers.emplace_back(Header(Spelling), Hints::PublicHeader |
+ Hints::PreferredHeader |
+ Hints::CompleteSymbol);
+ };
+ AddSymbolMapping(symbolName(S));
+ if (S.kind() == Symbol::Declaration) {
+ if (const auto *ND = llvm::dyn_cast<NamedDecl>(&S.declaration())) {
+ std::string QualName = ND->getQualifiedNameAsString();
+ if (QualName != symbolName(S))
+ AddSymbolMapping(QualName);
+ }
+ }
+ }
// If two Headers probably refer to the same file (e.g. Verbatim(foo.h) and
// Physical(/path/to/foo.h), we won't deduplicate them or merge their hints
llvm::stable_sort(
diff --git a/clang-tools-extra/include-cleaner/lib/MappingFile.cpp b/clang-tools-extra/include-cleaner/lib/MappingFile.cpp
new file mode 100644
index 0000000000000..7eb881b8f1fd3
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/lib/MappingFile.cpp
@@ -0,0 +1,239 @@
+//===--- MappingFile.cpp - IWYU mapping file support ----------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang-include-cleaner/MappingFile.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSet.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/SourceMgr.h"
+#include "llvm/Support/YAMLParser.h"
+#include "llvm/Support/raw_ostream.h"
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace clang::include_cleaner {
+
+void MappingFile::merge(MappingFile Other) {
+ for (auto &E : Other.IncludeMappings)
+ IncludeMappings[E.getKey()] = std::move(E.getValue());
+ for (auto &E : Other.SymbolMappings)
+ SymbolMappings[E.getKey()] = std::move(E.getValue());
+ IncludeRegexPatterns.insert(
+ IncludeRegexPatterns.end(),
+ std::make_move_iterator(Other.IncludeRegexPatterns.begin()),
+ std::make_move_iterator(Other.IncludeRegexPatterns.end()));
+}
+
+namespace {
+
+// Strip surrounding <> or "" delimiters, yielding the bare path.
+static std::string stripDelimiters(llvm::StringRef S) {
+ S = S.trim();
+ if ((S.starts_with("<") && S.ends_with(">")) ||
+ (S.starts_with("\"") && S.ends_with("\"")))
+ return S.substr(1, S.size() - 2).str();
+ return S.str();
+}
+
+// Ensure a header spelling has angle brackets or quotes.
+static std::string ensureQuoted(llvm::StringRef S) {
+ S = S.trim();
+ if (S.starts_with("<") || S.starts_with("\""))
+ return S.str();
+ return "<" + S.str() + ">";
+}
+
+struct ParseResult {
+ MappingFile Mapping;
+ std::vector<std::string> Refs;
+};
+
+// The four fields common to "include" and "symbol" mapping entries.
+struct EntryFields {
+ std::string From;
+ std::string FromVisibility;
+ std::string To;
+ std::string ToVisibility;
+};
+
+llvm::Expected<ParseResult> parseOneFile(llvm::StringRef FilePath);
+
+// Parses YAML content and returns a ParseResult containing the mapping data
+// and raw (unresolved) ref paths. The caller resolves refs against a base dir.
+llvm::Expected<ParseResult> parseContent(llvm::StringRef Content) {
+ // Capture YAML diagnostics instead of printing to stderr.
+ std::string DiagStr;
+ llvm::raw_string_ostream DiagOS(DiagStr);
+ llvm::SourceMgr SM;
+ SM.setDiagHandler(
+ [](const llvm::SMDiagnostic &D, void *Ctx) {
+ auto *OS = static_cast<llvm::raw_string_ostream *>(Ctx);
+ D.print("", *OS, false);
+ },
+ &DiagOS);
+
+ llvm::yaml::Stream YAMLStream(Content, SM);
+ ParseResult PR;
+
+ // Returns the four scalar fields of an "include" or "symbol" entry, or
+ // std::nullopt if the node is not a sequence of exactly 4 scalars.
+ // Always drains the full sequence so the YAML stream stays consistent.
+ auto ParseMappingFields =
+ [](llvm::yaml::Node *N) -> std::optional<EntryFields> {
+ auto *Seq = llvm::dyn_cast<llvm::yaml::SequenceNode>(N);
+ if (!Seq)
+ return std::nullopt;
+ EntryFields E;
+ std::string *Fields[] = {&E.From, &E.FromVisibility, &E.To,
+ &E.ToVisibility};
+ int Idx = 0;
+ bool Invalid = false;
+ for (llvm::yaml::Node &Item : *Seq) {
+ auto *S = llvm::dyn_cast<llvm::yaml::ScalarNode>(&Item);
+ if (!S) {
+ Invalid = true;
+ } else if (Idx < 4) {
+ llvm::SmallString<64> St;
+ *Fields[Idx] = S->getValue(St).str();
+ }
+ ++Idx;
+ }
+ if (Invalid || Idx != 4)
+ return std::nullopt;
+ return E;
+ };
+
+ for (llvm::yaml::document_iterator DI = YAMLStream.begin(),
+ DE = YAMLStream.end();
+ DI != DE; ++DI) {
+ llvm::yaml::Node *Root = DI->getRoot();
+ if (!Root)
+ break;
+
+ auto *TopSeq = llvm::dyn_cast<llvm::yaml::SequenceNode>(Root);
+ if (!TopSeq)
+ return llvm::createStringError(llvm::inconvertibleErrorCode(),
+ "expected a top-level sequence");
+
+ for (llvm::yaml::Node &Item : *TopSeq) {
+ auto *MapNode = llvm::dyn_cast<llvm::yaml::MappingNode>(&Item);
+ if (!MapNode)
+ continue;
+
+ // Iterate ALL key-value pairs — partial iteration leaves the YAML
+ // stream in a mid-parse state that yaml::skip() cannot recover from.
+ for (llvm::yaml::KeyValueNode &KV : *MapNode) {
+ auto *K = llvm::dyn_cast<llvm::yaml::ScalarNode>(KV.getKey());
+ llvm::yaml::Node *EntryVal = KV.getValue();
+ if (!K || !EntryVal)
+ continue;
+
+ llvm::SmallString<16> KeyStorage;
+ llvm::StringRef EntryType = K->getValue(KeyStorage);
+
+ if (EntryType == "ref") {
+ auto *ValScalar = llvm::dyn_cast<llvm::yaml::ScalarNode>(EntryVal);
+ if (!ValScalar)
+ return llvm::createStringError(llvm::inconvertibleErrorCode(),
+ "'ref' value must be a string");
+ llvm::SmallString<256> ValStorage;
+ PR.Refs.push_back(ValScalar->getValue(ValStorage).str());
+ continue;
+ }
+
+ // { "include": [from, from_vis, to, to_vis] }
+ if (EntryType == "include") {
+ std::optional<EntryFields> E = ParseMappingFields(EntryVal);
+ if (!E)
+ continue;
+ // Regex patterns: IWYU uses '@' as a prefix, e.g. "@<foo/.*>".
+ if (llvm::StringRef(E->From).trim().starts_with("@")) {
+ llvm::StringRef Pat = llvm::StringRef(E->From).trim().drop_front(1);
+ std::string RawPat = stripDelimiters(Pat);
+ if (!RawPat.empty())
+ PR.Mapping.IncludeRegexPatterns.push_back(
+ {RawPat, ensureQuoted(E->To)});
+ continue;
+ }
+ std::string Key = stripDelimiters(E->From);
+ if (Key.empty())
+ continue;
+ PR.Mapping.IncludeMappings[Key] = ensureQuoted(E->To);
+ continue;
+ }
+
+ // { "symbol": [name, sym_vis, header, hdr_vis] }
+ if (EntryType == "symbol") {
+ std::optional<EntryFields> E = ParseMappingFields(EntryVal);
+ if (!E)
+ continue;
+ if (E->From.empty())
+ continue;
+ PR.Mapping.SymbolMappings[E->From] = ensureQuoted(E->To);
+ continue;
+ }
+ }
+ }
+ }
+
+ if (YAMLStream.failed())
+ return llvm::createStringError(llvm::inconvertibleErrorCode(),
+ DiagStr.empty() ? "invalid YAML" : DiagStr);
+
+ return PR;
+}
+
+llvm::Expected<ParseResult> parseOneFile(llvm::StringRef FilePath) {
+ llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> MBOrErr =
+ llvm::MemoryBuffer::getFile(FilePath);
+ if (!MBOrErr)
+ return llvm::createFileError(FilePath, MBOrErr.getError());
+
+ llvm::Expected<ParseResult> PR = parseContent((*MBOrErr)->getBuffer());
+ if (!PR)
+ return llvm::createFileError(FilePath, PR.takeError());
+
+ // Resolve raw ref paths relative to this file's directory.
+ llvm::StringRef Dir = llvm::sys::path::parent_path(FilePath);
+ for (auto &Ref : PR->Refs) {
+ if (!llvm::sys::path::is_absolute(Ref)) {
+ llvm::SmallString<256> Resolved(Dir);
+ llvm::sys::path::append(Resolved, Ref);
+ Ref = std::string(Resolved);
+ }
+ }
+ return PR;
+}
+
+} // namespace
+
+llvm::Expected<MappingFile>
+parseMappingFiles(llvm::ArrayRef<std::string> Paths) {
+ MappingFile Result;
+ std::vector<std::string> Queue(Paths.begin(), Paths.end());
+ llvm::StringSet<> Visited;
+ while (!Queue.empty()) {
+ std::string Path = std::move(Queue.back());
+ Queue.pop_back();
+ if (!Visited.insert(Path).second)
+ continue;
+ llvm::Expected<ParseResult> PR = parseOneFile(Path);
+ if (!PR)
+ return PR.takeError();
+ Result.merge(std::move(PR->Mapping));
+ for (auto &Ref : PR->Refs)
+ Queue.push_back(std::move(Ref));
+ }
+ return Result;
+}
+
+} // namespace clang::include_cleaner
diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp
index 0284d6842e2d2..b95cf76c4d1eb 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 "clang-include-cleaner/MappingFile.h"
#include "clang-include-cleaner/Types.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
@@ -36,6 +37,7 @@
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem/UniqueID.h"
#include "llvm/Support/Path.h"
+#include "llvm/Support/Regex.h"
#include "llvm/Support/StringSaver.h"
#include <algorithm>
#include <assert.h>
@@ -414,9 +416,67 @@ void PragmaIncludes::record(Preprocessor &P) {
llvm::StringRef PragmaIncludes::getPublic(const FileEntry *F) const {
auto It = IWYUPublic.find(F->getUniqueID());
- if (It == IWYUPublic.end())
+ if (It != IWYUPublic.end())
+ return It->getSecond();
+
+ llvm::SmallString<256> NPath(F->tryGetRealPathName());
+ llvm::sys::path::native(NPath, llvm::sys::path::Style::posix);
+
+ // Check one candidate string against exact then regex mappings.
+ auto CheckCandidate = [&](llvm::StringRef Candidate) -> llvm::StringRef {
+ if (auto MI = ExternalIncludes.find(Candidate);
+ MI != ExternalIncludes.end())
+ return MI->getValue();
+ for (const auto &[R, Target] : ExternalIncludeRegexMappings)
+ if (llvm::Regex(R).match(Candidate))
+ return Target;
return "";
- return It->getSecond();
+ };
+
+ // Try each suffix of the real path (handles flat layouts like
+ // /usr/include/AE/foo.h).
+ llvm::StringRef Path = NPath;
+ while (!Path.empty()) {
+ if (auto Mapped = CheckCandidate(Path); !Mapped.empty())
+ return Mapped;
+ auto Slash = Path.find('/');
+ if (Slash == llvm::StringRef::npos)
+ break;
+ Path = Path.drop_front(Slash + 1);
+ }
+ return "";
+}
+
+void PragmaIncludes::loadMapping(const MappingFile &Mapping) {
+ for (const auto &E : Mapping.IncludeMappings)
+ ExternalIncludes[E.getKey()] = E.getValue();
+ for (const auto &E : Mapping.SymbolMappings)
+ ExternalSymbols[E.getKey()] = E.getValue();
+ for (const auto &[Pattern, Target] : Mapping.IncludeRegexPatterns) {
+ // Anchor to the start of each path suffix we test against.
+ std::string Anchored = "^(" + Pattern + ")";
+ std::string Err;
+ if (llvm::Regex(Anchored).isValid(Err))
+ ExternalIncludeRegexMappings.emplace_back(std::move(Anchored), Target);
+ }
+}
+
+llvm::StringRef
+PragmaIncludes::getExternalSymbolHeader(llvm::StringRef Name) const {
+ auto It = ExternalSymbols.find(Name);
+ if (It != ExternalSymbols.end())
+ return It->getValue();
+ return "";
+}
+
+llvm::StringRef
+PragmaIncludes::getPublicForSpelling(llvm::StringRef BarePath) const {
+ if (auto It = ExternalIncludes.find(BarePath); It != ExternalIncludes.end())
+ return It->getValue();
+ for (const auto &[R, Target] : ExternalIncludeRegexMappings)
+ if (llvm::Regex(R).match(BarePath))
+ return Target;
+ return "";
}
static llvm::SmallVector<FileEntryRef>
@@ -452,7 +512,31 @@ bool PragmaIncludes::isSelfContained(const FileEntry *FE) const {
}
bool PragmaIncludes::isPrivate(const FileEntry *FE) const {
- return IWYUPublic.contains(FE->getUniqueID());
+ if (IWYUPublic.contains(FE->getUniqueID()))
+ return true;
+
+ llvm::SmallString<256> NPath(FE->tryGetRealPathName());
+ llvm::sys::path::native(NPath, llvm::sys::path::Style::posix);
+
+ auto IsPrivateCandidate = [&](llvm::StringRef Candidate) -> bool {
+ if (ExternalIncludes.contains(Candidate))
+ return true;
+ for (const auto &[R, Target] : ExternalIncludeRegexMappings)
+ if (llvm::Regex(R).match(Candidate))
+ return true;
+ return false;
+ };
+
+ llvm::StringRef Path = NPath;
+ while (!Path.empty()) {
+ if (IsPrivateCandidate(Path))
+ return true;
+ auto Slash = Path.find('/');
+ if (Slash == llvm::StringRef::npos)
+ break;
+ Path = Path.drop_front(Slash + 1);
+ }
+ return false;
}
bool PragmaIncludes::shouldKeep(const FileEntry *FE) const {
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/private_header.h b/clang-tools-extra/include-cleaner/test/Inputs/private_header.h
new file mode 100644
index 0000000000000..ac81342025357
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/private_header.h
@@ -0,0 +1,2 @@
+#pragma once
+int private_func();
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/public_header.h b/clang-tools-extra/include-cleaner/test/Inputs/public_header.h
new file mode 100644
index 0000000000000..6f70f09beec22
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/public_header.h
@@ -0,0 +1 @@
+#pragma once
diff --git a/clang-tools-extra/include-cleaner/test/Inputs/umbrella_inc.h b/clang-tools-extra/include-cleaner/test/Inputs/umbrella_inc.h
new file mode 100644
index 0000000000000..212da96153e99
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/umbrella_inc.h
@@ -0,0 +1,2 @@
+#pragma once
+#include "private_header.h"
diff --git a/clang-tools-extra/include-cleaner/test/mapping-file.cpp b/clang-tools-extra/include-cleaner/test/mapping-file.cpp
new file mode 100644
index 0000000000000..34b4d6ada8b1a
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/mapping-file.cpp
@@ -0,0 +1,43 @@
+// Tests for --mapping-file support (IWYU .imp format).
+// umbrella_inc.h transitively provides private_func via private_header.h.
+
+// Set up a temporary include-mapping file: private_header.h -> <public_header.h>
+// RUN: echo '[{"include": ["<private_header.h>", "private", "<public_header.h>", "public"]}]' > %t.inc.imp
+
+// Without a mapping file: the tool suggests the direct provider (private_header.h).
+// RUN: clang-include-cleaner -print=changes %s -- -I%S/Inputs/ | \
+// RUN: FileCheck --check-prefix=NOMAP %s
+// NOMAP: - "umbrella_inc.h"
+// NOMAP: + "private_header.h"
+// NOMAP-NOT: public_header
+
+// With the include mapping file: the tool suggests the mapped public header.
+// RUN: clang-include-cleaner --mapping-file=%t.inc.imp -print=changes %s -- -I%S/Inputs/ | \
+// RUN: FileCheck --check-prefix=INCMAP %s
+// INCMAP: + <public_header.h>
+// INCMAP-NOT: + "private_header.h"
+
+// Symbol mapping: map "private_func" symbol to <public_header.h>
+// RUN: echo '[{"symbol": ["private_func", "private", "<public_header.h>", "public"]}]' > %t.sym.imp
+
+// With the symbol mapping file: the tool suggests the mapped header for the symbol.
+// RUN: clang-include-cleaner --mapping-file=%t.sym.imp -print=changes %s -- -I%S/Inputs/ | \
+// RUN: FileCheck --check-prefix=SYMMAP %s
+// SYMMAP: + <public_header.h>
+
+// Multiple mapping files can be specified simultaneously.
+// RUN: clang-include-cleaner --mapping-file=%t.inc.imp --mapping-file=%t.sym.imp \
+// RUN: -print=changes %s -- -I%S/Inputs/ | \
+// RUN: FileCheck --check-prefix=MULTI %s
+// MULTI: + <public_header.h>
+
+// Regex pattern: "@<private_header.h>" matches private_header.h by suffix.
+// RUN: echo '[{"include": ["@<private_header.h>", "private", "<public_header.h>", "public"]}]' > %t.regex.imp
+// RUN: clang-include-cleaner --mapping-file=%t.regex.imp -print=changes %s -- -I%S/Inputs/ | \
+// RUN: FileCheck --check-prefix=REGEXMAP %s
+// REGEXMAP: + <public_header.h>
+// REGEXMAP-NOT: + "private_header.h"
+
+#include "umbrella_inc.h"
+
+int x = private_func();
diff --git a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
index fefbfc3a9614d..4812e2c6ca2c6 100644
--- a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
+++ b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
@@ -8,6 +8,7 @@
#include "AnalysisInternal.h"
#include "clang-include-cleaner/Analysis.h"
+#include "clang-include-cleaner/MappingFile.h"
#include "clang-include-cleaner/Record.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
@@ -18,6 +19,7 @@
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSet.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/Regex.h"
@@ -116,6 +118,14 @@ cl::opt<bool> DisableRemove{
cl::cat(IncludeCleaner),
};
+cl::list<std::string> MappingFiles{
+ "mapping-file",
+ cl::desc("Path to an IWYU mapping file (.imp). May be specified multiple "
+ "times. Entries in mapping files supplement IWYU pragmas in "
+ "source code."),
+ cl::cat(IncludeCleaner),
+};
+
std::atomic<unsigned> Errors = ATOMIC_VAR_INIT(0);
format::FormatStyle getStyle(llvm::StringRef Filename) {
@@ -131,8 +141,10 @@ format::FormatStyle getStyle(llvm::StringRef Filename) {
class Action : public clang::ASTFrontendAction {
public:
Action(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter,
- llvm::StringMap<std::string> &EditedFiles)
- : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {}
+ llvm::StringMap<std::string> &EditedFiles,
+ const MappingFile *Mapping = nullptr)
+ : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles), Mapping(Mapping) {
+ }
private:
RecordedAST AST;
@@ -140,6 +152,7 @@ class Action : public clang::ASTFrontendAction {
PragmaIncludes PI;
llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;
llvm::StringMap<std::string> &EditedFiles;
+ const MappingFile *Mapping;
bool BeginInvocation(CompilerInstance &CI) override {
// We only perform include-cleaner analysis. So we disable diagnostics that
@@ -164,6 +177,8 @@ class Action : public clang::ASTFrontendAction {
auto &P = CI.getPreprocessor();
P.addPPCallbacks(PP.record(P));
PI.record(getCompilerInstance());
+ if (Mapping)
+ PI.loadMapping(*Mapping);
ASTFrontendAction::ExecuteAction();
}
@@ -196,6 +211,33 @@ class Action : public clang::ASTFrontendAction {
analyze(AST.Roots, PP.MacroReferences, PP.Includes, &PI,
getCompilerInstance().getPreprocessor(), HeaderFilter);
+ // Apply mapping file patterns directly to the spelled include suggestions.
+ // This handles headers whose physical paths don't reconstruct the include
+ // spelling (e.g. macOS framework sub-headers accessed via umbrella).
+ if (Mapping) {
+ // Collect headers already directly included in the main file.
+ llvm::StringSet<> DirectIncludes;
+ for (const auto &Inc : PP.Includes.all())
+ DirectIncludes.insert(Inc.quote());
+
+ llvm::StringSet<> AddedMapped;
+ std::vector<std::pair<std::string, Header>> NewMissing;
+ for (auto &[Spelling, H] : Results.Missing) {
+ llvm::StringRef Bare = llvm::StringRef(Spelling).trim("<>\"");
+ llvm::StringRef Mapped = PI.getPublicForSpelling(Bare);
+ if (!Mapped.empty()) {
+ // Replace private suggestion with its public mapping.
+ // Skip if the mapped header is already included or already queued.
+ if (!DirectIncludes.count(Mapped) &&
+ AddedMapped.insert(Mapped).second)
+ NewMissing.emplace_back(Mapped.str(), H);
+ } else {
+ NewMissing.emplace_back(std::move(Spelling), H);
+ }
+ }
+ Results.Missing = std::move(NewMissing);
+ }
+
if (!Insert) {
llvm::errs()
<< "warning: '-insert=0' is deprecated in favor of "
@@ -252,11 +294,12 @@ class Action : public clang::ASTFrontendAction {
};
class ActionFactory : public tooling::FrontendActionFactory {
public:
- ActionFactory(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter)
- : HeaderFilter(HeaderFilter) {}
+ ActionFactory(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter,
+ const MappingFile *Mapping = nullptr)
+ : HeaderFilter(HeaderFilter), Mapping(Mapping) {}
std::unique_ptr<clang::FrontendAction> create() override {
- return std::make_unique<Action>(HeaderFilter, EditedFiles);
+ return std::make_unique<Action>(HeaderFilter, EditedFiles, Mapping);
}
const llvm::StringMap<std::string> &editedFiles() const {
@@ -265,6 +308,7 @@ class ActionFactory : public tooling::FrontendActionFactory {
private:
llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;
+ const MappingFile *Mapping;
// Map from file name to final code with the include edits applied.
llvm::StringMap<std::string> EditedFiles;
};
@@ -390,7 +434,19 @@ int main(int argc, const char **argv) {
auto HeaderFilter = headerFilter();
if (!HeaderFilter)
return 1; // error already reported.
- ActionFactory Factory(HeaderFilter);
+
+ // Parse mapping files if specified.
+ std::optional<MappingFile> Mapping;
+ if (!MappingFiles.empty()) {
+ std::vector<std::string> Paths(MappingFiles.begin(), MappingFiles.end());
+ llvm::Expected<MappingFile> M = parseMappingFiles(Paths);
+ if (!M) {
+ llvm::errs() << toString(M.takeError()) << "\n";
+ return 1;
+ }
+ Mapping = std::move(*M);
+ }
+ ActionFactory Factory(HeaderFilter, Mapping ? &*Mapping : nullptr);
auto ErrorCode = Tool.run(&Factory);
if (Edit) {
for (const auto &NameAndContent : Factory.editedFiles()) {
diff --git a/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt b/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
index 416535649f622..7aeda34201820 100644
--- a/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
+++ b/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
@@ -10,6 +10,7 @@ add_unittest(ClangIncludeCleanerUnitTests ClangIncludeCleanerTests
FindHeadersTest.cpp
IncludeSpellerTest.cpp
LocateSymbolTest.cpp
+ MappingFileTest.cpp
RecordTest.cpp
TypesTest.cpp
WalkASTTest.cpp
diff --git a/clang-tools-extra/include-cleaner/unittests/MappingFileTest.cpp b/clang-tools-extra/include-cleaner/unittests/MappingFileTest.cpp
new file mode 100644
index 0000000000000..adfc16bcd3d25
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/unittests/MappingFileTest.cpp
@@ -0,0 +1,393 @@
+//===--- MappingFileTest.cpp ----------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang-include-cleaner/MappingFile.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <string>
+#include <system_error>
+#include <utility>
+#include <vector>
+
+namespace clang::include_cleaner {
+namespace {
+
+using ::llvm::Failed;
+using ::llvm::HasValue;
+using ::testing::AllOf;
+using ::testing::ElementsAre;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Pair;
+using ::testing::ResultOf;
+using ::testing::UnorderedElementsAre;
+
+// Write content to a temporary file, returning the path.
+std::string writeTempFile(llvm::StringRef Content) {
+ llvm::SmallString<256> Path;
+ std::error_code EC =
+ llvm::sys::fs::createTemporaryFile("mapping", "imp", Path);
+ EXPECT_FALSE(EC);
+ std::error_code WriteEC;
+ llvm::raw_fd_ostream OS(Path, WriteEC);
+ EXPECT_FALSE(WriteEC);
+ OS << Content;
+ OS.close();
+ return Path.str().str();
+}
+
+// StringMap entries lack the first_type/second_type typedefs that Pair
+// requires, so convert to a vector of std::pair for matching.
+std::vector<std::pair<std::string, std::string>>
+toPairs(const llvm::StringMap<std::string> &M) {
+ std::vector<std::pair<std::string, std::string>> Result;
+ for (const auto &E : M)
+ Result.emplace_back(E.getKey().str(), E.getValue());
+ return Result;
+}
+
+TEST(MappingFileTest, EmptyArray) {
+ std::string Path = writeTempFile("[]");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings, IsEmpty()),
+ Field("SymbolMappings", &MappingFile::SymbolMappings, IsEmpty()))));
+}
+
+TEST(MappingFileTest, IncludeMapping_AngleBrackets) {
+ std::string Path = writeTempFile(
+ R"([{"include": ["<private.h>", "private", "<public.h>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(
+ Pair("private.h", "<public.h>")))),
+ Field("SymbolMappings", &MappingFile::SymbolMappings, IsEmpty()))));
+}
+
+TEST(MappingFileTest, IncludeMapping_QuotedHeaders) {
+ std::string Path = writeTempFile(
+ R"([{"include": ["\"private.h\"", "private", "\"public.h\"", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair(
+ "private.h", "\"public.h\""))))));
+}
+
+TEST(MappingFileTest, SymbolMapping) {
+ std::string Path = writeTempFile(
+ R"([{"symbol": ["NULL", "private", "<stddef.h>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(AllOf(
+ Field("SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs,
+ UnorderedElementsAre(Pair("NULL", "<stddef.h>")))),
+ Field("IncludeMappings", &MappingFile::IncludeMappings, IsEmpty()))));
+}
+
+TEST(MappingFileTest, SymbolMapping_QualifiedName) {
+ std::string Path = writeTempFile(
+ R"([{"symbol": ["std::string", "private", "<string>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field("SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(
+ Pair("std::string", "<string>"))))));
+}
+
+TEST(MappingFileTest, MultipleEntries) {
+ std::string Path = writeTempFile(R"([
+ {"include": ["<a.h>", "private", "<b.h>", "public"]},
+ {"symbol": ["MyType", "private", "<mytype.h>", "public"]},
+ {"include": ["<c.h>", "private", "<d.h>", "public"]}
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>"),
+ Pair("c.h", "<d.h>")))),
+ Field("SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(
+ Pair("MyType", "<mytype.h>")))))));
+}
+
+TEST(MappingFileTest, UnquotedPrivatePublic) {
+ // Traditional IWYU format with unquoted private/public visibility values.
+ std::string Path = writeTempFile(
+ "[{\"include\": [\"<private.h>\", private, \"<public.h>\", public]}]");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(
+ Pair("private.h", "<public.h>"))))));
+}
+
+TEST(MappingFileTest, HashLineComment) {
+ std::string Path = writeTempFile(R"(
+ # This is a comment
+ [
+ # Another comment
+ {"include": ["<a.h>", "private", "<b.h>", "public"]}
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>"))))));
+}
+
+TEST(MappingFileTest, HashCommentAfterValue) {
+ std::string Path = writeTempFile(
+ "[{\"include\": [\"<a.h>\", \"private\", \"<b.h>\", \"public\"]}] # end");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>"))))));
+}
+
+TEST(MappingFileTest, RefEntry) {
+ std::string RefPath =
+ writeTempFile(R"([{"symbol": ["Foo", "private", "<foo.h>", "public"]}])");
+ std::string MainPath = writeTempFile(R"([{"ref": ")" + RefPath + R"("}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({MainPath}),
+ HasValue(Field(
+ "SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("Foo", "<foo.h>"))))));
+}
+
+TEST(MappingFileTest, MultiplePaths) {
+ std::string Path1 = writeTempFile(
+ R"([{"include": ["<a.h>", "private", "<b.h>", "public"]}])");
+ std::string Path2 =
+ writeTempFile(R"([{"symbol": ["X", "private", "<x.h>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path1, Path2}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>")))),
+ Field("SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("X", "<x.h>")))))));
+}
+
+TEST(MappingFileTest, RegexPattern_AngleBrackets) {
+ // "@<AE/.*>" is a regex pattern stored in IncludeRegexPatterns.
+ std::string Path = writeTempFile(
+ R"([{"include": ["@<AE/.*>", "private", "<Carbon/Carbon.h>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings, IsEmpty()),
+ Field("IncludeRegexPatterns", &MappingFile::IncludeRegexPatterns,
+ ElementsAre(Pair("AE/.*", "<Carbon/Carbon.h>"))))));
+}
+
+TEST(MappingFileTest, RegexPattern_QuotedHeader) {
+ // "@\"foo/.*\"" extracts regex from quoted form.
+ std::string Path = writeTempFile("[{\"include\": [\"@\\\"foo/.*\\\"\", "
+ "\"private\", \"<foo.h>\", \"public\"]}]");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field("IncludeRegexPatterns", &MappingFile::IncludeRegexPatterns,
+ ElementsAre(Pair("foo/.*", "<foo.h>")))));
+}
+
+TEST(MappingFileTest, RegexPattern_FrameworkStyle) {
+ // Pattern "AE/.*" should be stored as-is; framework path matching is
+ // handled at runtime in PragmaIncludes, not during parsing.
+ std::string Path = writeTempFile(
+ R"([{"include": ["@<AE/.*>", "private", "<Carbon/Carbon.h>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field("IncludeRegexPatterns", &MappingFile::IncludeRegexPatterns,
+ ElementsAre(Pair("AE/.*", "<Carbon/Carbon.h>")))));
+}
+
+TEST(MappingFileTest, RegexPattern_MultipleFrameworks) {
+ // Multiple regex patterns for different frameworks.
+ std::string Path = writeTempFile(R"([
+ {"include": ["@<AE/.*>", "private", "<Carbon/Carbon.h>", "public"]},
+ {"include": ["@<CarbonCore/.*>", "private", "<Carbon/Carbon.h>", "public"]},
+ {"include": ["@<HIToolbox/.*>", "private", "<Carbon/Carbon.h>", "public"]}
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field("IncludeRegexPatterns", &MappingFile::IncludeRegexPatterns,
+ ElementsAre(Pair("AE/.*", "<Carbon/Carbon.h>"),
+ Pair("CarbonCore/.*", "<Carbon/Carbon.h>"),
+ Pair("HIToolbox/.*", "<Carbon/Carbon.h>")))));
+}
+
+TEST(MappingFileTest, RegexAndExactInSameFile) {
+ std::string Path = writeTempFile(R"([
+ {"include": ["<exact.h>", "private", "<public.h>", "public"]},
+ {"include": ["@<regex/.*>", "private", "<public.h>", "public"]}
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs,
+ UnorderedElementsAre(Pair("exact.h", "<public.h>")))),
+ Field("IncludeRegexPatterns", &MappingFile::IncludeRegexPatterns,
+ ElementsAre(Pair("regex/.*", "<public.h>"))))));
+}
+
+TEST(MappingFileTest, TrailingComma) {
+ // YAML (and IWYU) allow a trailing comma after the last element.
+ std::string Path = writeTempFile(R"([
+ {"include": ["<a.h>", "private", "<b.h>", "public"]},
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>"))))));
+}
+
+TEST(MappingFileTest, WrongFieldCount_TooFew) {
+ // Entry with 3 scalars is silently skipped; other valid entries are kept.
+ std::string Path = writeTempFile(R"([
+ {"include": ["<a.h>", "private", "<b.h>"]},
+ {"include": ["<c.h>", "private", "<d.h>", "public"]}
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("c.h", "<d.h>"))))));
+}
+
+TEST(MappingFileTest, WrongFieldCount_TooMany) {
+ // Entry with 5 scalars is silently skipped; other valid entries are kept.
+ std::string Path = writeTempFile(R"([
+ {"include": ["<a.h>", "private", "<b.h>", "public", "extra"]},
+ {"include": ["<c.h>", "private", "<d.h>", "public"]}
+ ])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("c.h", "<d.h>"))))));
+}
+
+TEST(MappingFileTest, RefToNonexistentFile) {
+ std::string Path =
+ writeTempFile(R"([{"ref": "/nonexistent/path/to/ref.imp"}])");
+ EXPECT_THAT_EXPECTED(parseMappingFiles({Path}), Failed());
+}
+
+TEST(MappingFileTest, DuplicateKeys_FirstFilePriority) {
+ // When two files map the same key, the first-listed file takes priority.
+ std::string Path1 = writeTempFile(
+ R"([{"include": ["<a.h>", "private", "<first.h>", "public"]}])");
+ std::string Path2 = writeTempFile(
+ R"([{"include": ["<a.h>", "private", "<second.h>", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path1, Path2}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<first.h>"))))));
+}
+
+TEST(MappingFileTest, MultipleRefs) {
+ // A file may contain multiple "ref" entries; all referenced files are merged.
+ std::string RefPath1 = writeTempFile(
+ R"([{"include": ["<a.h>", "private", "<b.h>", "public"]}])");
+ std::string RefPath2 =
+ writeTempFile(R"([{"symbol": ["X", "private", "<x.h>", "public"]}])");
+ std::string MainPath = writeTempFile(
+ R"([{"ref": ")" + RefPath1 + R"("}, {"ref": ")" + RefPath2 + R"("}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({MainPath}),
+ HasValue(AllOf(
+ Field("IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>")))),
+ Field("SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("X", "<x.h>")))))));
+}
+
+TEST(MappingFileTest, RelativeRef) {
+ // Relative ref paths are resolved against the referring file's directory.
+ std::string RefPath =
+ writeTempFile(R"([{"symbol": ["Bar", "private", "<bar.h>", "public"]}])");
+ // Both temp files land in the same directory, so the bare filename is a
+ // valid relative ref from the main file.
+ std::string RefFilename = llvm::sys::path::filename(RefPath).str();
+ std::string MainPath =
+ writeTempFile(R"([{"ref": ")" + RefFilename + R"("}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({MainPath}),
+ HasValue(Field(
+ "SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("Bar", "<bar.h>"))))));
+}
+
+TEST(MappingFileTest, CircularRef) {
+ // Circular refs terminate via the Visited set; entries from all files are
+ // merged.
+ llvm::SmallString<256> PathA, PathB;
+ ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("mapping", "imp", PathA));
+ ASSERT_FALSE(llvm::sys::fs::createTemporaryFile("mapping", "imp", PathB));
+ {
+ std::error_code EC;
+ llvm::raw_fd_ostream OS(PathA, EC);
+ ASSERT_FALSE(EC);
+ OS << "[{\"ref\": \"" << PathB
+ << "\"}, {\"symbol\": [\"A\", \"private\", \"<a.h>\", \"public\"]}]";
+ }
+ {
+ std::error_code EC;
+ llvm::raw_fd_ostream OS(PathB, EC);
+ ASSERT_FALSE(EC);
+ OS << "[{\"ref\": \"" << PathA
+ << "\"}, {\"symbol\": [\"B\", \"private\", \"<b.h>\", \"public\"]}]";
+ }
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({PathA.str().str()}),
+ HasValue(
+ Field("SymbolMappings", &MappingFile::SymbolMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("A", "<a.h>"),
+ Pair("B", "<b.h>"))))));
+}
+
+TEST(MappingFileTest, NonexistentFile) {
+ EXPECT_THAT_EXPECTED(parseMappingFiles({"/nonexistent/path/to/file.imp"}),
+ Failed());
+}
+
+TEST(MappingFileTest, InvalidYAML) {
+ std::string Path = writeTempFile("not yaml: [}");
+ EXPECT_THAT_EXPECTED(parseMappingFiles({Path}), Failed());
+}
+
+TEST(MappingFileTest, ToWithoutDelimiters) {
+ // If the "to" header has no delimiters, angle brackets are added.
+ std::string Path =
+ writeTempFile(R"([{"include": ["<a.h>", "private", "b.h", "public"]}])");
+ ASSERT_THAT_EXPECTED(
+ parseMappingFiles({Path}),
+ HasValue(Field(
+ "IncludeMappings", &MappingFile::IncludeMappings,
+ ResultOf(toPairs, UnorderedElementsAre(Pair("a.h", "<b.h>"))))));
+}
+
+} // namespace
+} // namespace clang::include_cleaner
More information about the cfe-commits
mailing list