https://github.com/sstepashka updated https://github.com/llvm/llvm-project/pull/195987
>From 5a9ff9f72e68d66645895df864ebebc6e7121dcf 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 | 401 ++++++++++++++++++ 13 files changed, 961 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..c0004b1f01c8a --- /dev/null +++ b/clang-tools-extra/include-cleaner/unittests/MappingFileTest.cpp @@ -0,0 +1,401 @@ +//===--- 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; +} + +// Convert backslashes to forward slashes for safe embedding in YAML strings. +// On Windows, raw paths would produce unrecognized escape sequences (e.g. \U). +std::string toYAMLPath(llvm::StringRef Path) { + return llvm::sys::path::convert_to_slash(Path); +} + +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": ")" + toYAMLPath(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": ")" + toYAMLPath(RefPath1) + R"("}, {"ref": ")" + + toYAMLPath(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\": \"" << toYAMLPath(PathB) + << "\"}, {\"symbol\": [\"A\", \"private\", \"<a.h>\", \"public\"]}]"; + } + { + std::error_code EC; + llvm::raw_fd_ostream OS(PathB, EC); + ASSERT_FALSE(EC); + OS << "[{\"ref\": \"" << toYAMLPath(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
