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 <[email protected]>
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

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to