[clang-tools-extra] [clang-include-cleaner] Add --mapping-file support for IWYU .imp mapping files. (PR #195987)

Aaron Ballman via cfe-commits cfe-commits at lists.llvm.org
Fri Oct 2 07:10:17 PDT 2026


================
@@ -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;
+        }
----------------
AaronBallman wrote:

If the entry type is not one of the ones we know about, should it be diagnosed as a malformed input?

I suppose a bigger question is: should we throw a fuzzer at the parser to see if we can tease out parser bugs?

https://github.com/llvm/llvm-project/pull/195987


More information about the cfe-commits mailing list