[clang] [InstallAPI] Pick up input headers by directory traversal (PR #94508)

via cfe-commits cfe-commits at lists.llvm.org
Wed Jun 5 10:43:35 PDT 2024


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang

Author: Cyndy Ishida (cyndyishida)

<details>
<summary>Changes</summary>

Match TAPI behavior and allow input headers to be resolved via a passed directory, which is expected to be a library sitting in a build directory.

---

Patch is 34.57 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/94508.diff


19 Files Affected:

- (modified) clang/include/clang/Basic/DiagnosticInstallAPIKinds.td (+2) 
- (added) clang/include/clang/InstallAPI/DirectoryScanner.h (+81) 
- (modified) clang/include/clang/InstallAPI/HeaderFile.h (+8) 
- (added) clang/include/clang/InstallAPI/Library.h (+65) 
- (modified) clang/include/clang/InstallAPI/MachO.h (+1) 
- (modified) clang/lib/InstallAPI/CMakeLists.txt (+2) 
- (added) clang/lib/InstallAPI/DirectoryScanner.cpp (+300) 
- (added) clang/lib/InstallAPI/Library.cpp (+40) 
- (modified) clang/test/InstallAPI/asm.test (+1-1) 
- (modified) clang/test/InstallAPI/basic.test (+2-2) 
- (modified) clang/test/InstallAPI/binary-attributes.test (+4-2) 
- (modified) clang/test/InstallAPI/cpp.test (+2-2) 
- (modified) clang/test/InstallAPI/diagnostics-dsym.test (+2-2) 
- (added) clang/test/InstallAPI/directory-scanning-dylib.test (+57) 
- (added) clang/test/InstallAPI/directory-scanning-frameworks.test (+89) 
- (modified) clang/test/InstallAPI/functions.test (+1-1) 
- (modified) clang/test/InstallAPI/variables.test (+1-1) 
- (modified) clang/tools/clang-installapi/Options.cpp (+38-13) 
- (modified) clang/tools/clang-installapi/Options.h (+3) 


``````````diff
diff --git a/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td b/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
index cdf27247602f2..e10fa71011f30 100644
--- a/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
+++ b/clang/include/clang/Basic/DiagnosticInstallAPIKinds.td
@@ -26,6 +26,8 @@ def err_unsupported_environment : Error<"environment '%0' is not supported: '%1'
 def err_unsupported_os : Error<"os '%0' is not supported: '%1'">;
 def err_cannot_read_input_list : Error<"could not read %0 input list '%1': %2">;
 def err_invalid_label: Error<"label '%0' is reserved: use a different label name for -X<label>">;
+def err_directory_scanning: Error<"could not read directory '%0': %1">;
+def err_more_than_one_library: Error<"more than one framework/dynamic library found">;
 } // end of command line category.
 
 let CategoryName = "Verification" in {
diff --git a/clang/include/clang/InstallAPI/DirectoryScanner.h b/clang/include/clang/InstallAPI/DirectoryScanner.h
new file mode 100644
index 0000000000000..803328982ec87
--- /dev/null
+++ b/clang/include/clang/InstallAPI/DirectoryScanner.h
@@ -0,0 +1,81 @@
+//===- InstallAPI/DirectoryScanner.h ----------------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// The DirectoryScanner for collecting library files on the file system.
+///
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_INSTALLAPI_DIRECTORYSCANNER_H
+#define LLVM_CLANG_INSTALLAPI_DIRECTORYSCANNER_H
+
+#include "clang/Basic/FileManager.h"
+#include "clang/InstallAPI/Library.h"
+
+namespace clang::installapi {
+
+enum ScanMode {
+  /// Scanning Framework directory.
+  ScanFrameworks,
+  /// Scanning Dylib directory.
+  ScanDylibs,
+};
+
+class DirectoryScanner {
+public:
+  DirectoryScanner(FileManager &FM, ScanMode Mode = ScanMode::ScanFrameworks)
+      : FM(FM), Mode(Mode) {}
+
+  /// Scan for all input files throughout directory.
+  ///
+  /// \param Directory Path of input directory.
+  llvm::Error scan(StringRef Directory);
+
+  /// Take over ownership of stored libraries.
+  std::vector<Library> takeLibraries() { return std::move(Libraries); };
+
+  /// Get all the header files in libraries.
+  ///
+  /// \param Libraries Reference of collection of libraries.
+  static HeaderSeq getHeaders(ArrayRef<Library> Libraries);
+
+private:
+  /// Collect files for dylibs in usr/(local)/lib within directory.
+  llvm::Error scanForUnwrappedLibraries(StringRef Directory);
+
+  /// Collect files for any frameworks within directory.
+  llvm::Error scanForFrameworks(StringRef Directory);
+
+  /// Get a library from the libraries collection.
+  Library &getOrCreateLibrary(StringRef Path, std::vector<Library> &Libs) const;
+
+  /// Collect multiple frameworks from directory.
+  llvm::Error scanMultipleFrameworks(StringRef Directory,
+                                     std::vector<Library> &Libs) const;
+  /// Collect files from nested frameworks.
+  llvm::Error scanSubFrameworksDirectory(StringRef Directory,
+                                         std::vector<Library> &Libs) const;
+
+  /// Collect files from framework path.
+  llvm::Error scanFrameworkDirectory(StringRef Path, Library &Framework) const;
+
+  /// Collect header files from path.
+  llvm::Error scanHeaders(StringRef Path, Library &Lib, HeaderType Type,
+                          StringRef BasePath,
+                          StringRef ParentPath = StringRef()) const;
+
+  /// Collect files from Version directories inside Framework directories.
+  llvm::Error scanFrameworkVersionsDirectory(StringRef Path,
+                                             Library &Lib) const;
+  FileManager &FM;
+  ScanMode Mode;
+  StringRef RootPath;
+  std::vector<Library> Libraries;
+};
+
+} // namespace clang::installapi
+
+#endif // LLVM_CLANG_INSTALLAPI_DIRECTORYSCANNER_H
diff --git a/clang/include/clang/InstallAPI/HeaderFile.h b/clang/include/clang/InstallAPI/HeaderFile.h
index c67503d4ad49e..12a87c01ad1c4 100644
--- a/clang/include/clang/InstallAPI/HeaderFile.h
+++ b/clang/include/clang/InstallAPI/HeaderFile.h
@@ -97,6 +97,14 @@ class HeaderFile {
                                           Other.Excluded, Other.Extra,
                                           Other.Umbrella);
   }
+
+  bool operator<(const HeaderFile &Other) const {
+    if (isExtra() && Other.isExtra())
+      return std::tie(Type, Umbrella) < std::tie(Other.Type, Other.Umbrella);
+
+    return std::tie(Type, Umbrella, Extra, FullPath) <
+           std::tie(Other.Type, Other.Umbrella, Other.Extra, Other.FullPath);
+  }
 };
 
 /// Glob that represents a pattern of header files to retreive.
diff --git a/clang/include/clang/InstallAPI/Library.h b/clang/include/clang/InstallAPI/Library.h
new file mode 100644
index 0000000000000..8373d424dd364
--- /dev/null
+++ b/clang/include/clang/InstallAPI/Library.h
@@ -0,0 +1,65 @@
+//===- InstallAPI/Library.h -------------------------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// Defines the content of a library, such as public and private
+/// header files, and whether it is a framework.
+///
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_INSTALLAPI_LIBRARY_H
+#define LLVM_CLANG_INSTALLAPI_LIBRARY_H
+
+#include "clang/InstallAPI/HeaderFile.h"
+#include "clang/InstallAPI/MachO.h"
+
+namespace clang::installapi {
+
+class Library {
+public:
+  Library(StringRef Directory) : BaseDirectory(Directory) {}
+
+  /// Capture the name of the framework by the install name.
+  ///
+  /// \param InstallName The install name of the library encoded in a dynamic
+  /// library.
+  static StringRef getFrameworkNameFromInstallName(StringRef InstallName);
+
+  /// Get name of library by the discovered file path.
+  StringRef getName() const;
+
+  /// Get discovered path of library.
+  StringRef getPath() const { return BaseDirectory; }
+
+  /// Add a header file that belongs to the library.
+  ///
+  /// \param FullPath Path to header file.
+  /// \param Type Access level of header.
+  /// \param IncludePath The way the header should be included.
+  void addHeaderFile(StringRef FullPath, HeaderType Type,
+                     StringRef IncludePath = StringRef()) {
+    Headers.emplace_back(FullPath, Type, IncludePath);
+  }
+
+  /// Determine if library is empty.
+  bool empty() {
+    return SubFrameworks.empty() && Headers.empty() &&
+           FrameworkVersions.empty();
+  }
+
+private:
+  std::string BaseDirectory;
+  HeaderSeq Headers;
+  std::vector<Library> SubFrameworks;
+  std::vector<Library> FrameworkVersions;
+  bool IsUnwrappedDylib{false};
+
+  friend class DirectoryScanner;
+};
+
+} // namespace clang::installapi
+
+#endif // LLVM_CLANG_INSTALLAPI_LIBRARY_H
diff --git a/clang/include/clang/InstallAPI/MachO.h b/clang/include/clang/InstallAPI/MachO.h
index 1ea544412f4cd..6036a7e5397cb 100644
--- a/clang/include/clang/InstallAPI/MachO.h
+++ b/clang/include/clang/InstallAPI/MachO.h
@@ -31,6 +31,7 @@ using RecordLinkage = llvm::MachO::RecordLinkage;
 using Record = llvm::MachO::Record;
 using EncodeKind = llvm::MachO::EncodeKind;
 using GlobalRecord = llvm::MachO::GlobalRecord;
+using InterfaceFile = llvm::MachO::InterfaceFile;
 using ObjCContainerRecord = llvm::MachO::ObjCContainerRecord;
 using ObjCInterfaceRecord = llvm::MachO::ObjCInterfaceRecord;
 using ObjCCategoryRecord = llvm::MachO::ObjCCategoryRecord;
diff --git a/clang/lib/InstallAPI/CMakeLists.txt b/clang/lib/InstallAPI/CMakeLists.txt
index b36493942300b..b63173bc1be3e 100644
--- a/clang/lib/InstallAPI/CMakeLists.txt
+++ b/clang/lib/InstallAPI/CMakeLists.txt
@@ -8,10 +8,12 @@ set(LLVM_LINK_COMPONENTS
 
 add_clang_library(clangInstallAPI
   DiagnosticBuilderWrappers.cpp
+  DirectoryScanner.cpp
   DylibVerifier.cpp
   FileList.cpp
   Frontend.cpp
   HeaderFile.cpp
+  Library.cpp
   Visitor.cpp
 
   LINK_LIBS
diff --git a/clang/lib/InstallAPI/DirectoryScanner.cpp b/clang/lib/InstallAPI/DirectoryScanner.cpp
new file mode 100644
index 0000000000000..ae4ba52de6ba9
--- /dev/null
+++ b/clang/lib/InstallAPI/DirectoryScanner.cpp
@@ -0,0 +1,300 @@
+//===- DirectoryScanner.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/InstallAPI/DirectoryScanner.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSwitch.h"
+#include "llvm/TextAPI/DylibReader.h"
+
+using namespace llvm;
+using namespace llvm::MachO;
+
+namespace clang::installapi {
+
+HeaderSeq DirectoryScanner::getHeaders(ArrayRef<Library> Libraries) {
+  HeaderSeq Headers;
+  for (const Library &Lib : Libraries)
+    llvm::append_range(Headers, Lib.Headers);
+  return Headers;
+}
+
+llvm::Error DirectoryScanner::scan(StringRef Directory) {
+  if (Mode == ScanMode::ScanFrameworks)
+    return scanForFrameworks(Directory);
+
+  return scanForUnwrappedLibraries(Directory);
+}
+
+llvm::Error DirectoryScanner::scanForUnwrappedLibraries(StringRef Directory) {
+  // Check some known sub-directory locations.
+  auto GetDirectory = [&](const char *Sub) -> OptionalDirectoryEntryRef {
+    SmallString<PATH_MAX> Path(Directory);
+    sys::path::append(Path, Sub);
+    return FM.getOptionalDirectoryRef(Path);
+  };
+
+  auto DirPublic = GetDirectory("usr/include");
+  auto DirPrivate = GetDirectory("usr/local/include");
+  if (!DirPublic && !DirPrivate) {
+    std::error_code ec = std::make_error_code(std::errc::not_a_directory);
+    return createStringError(ec,
+                             "cannot find any public (usr/include) or private "
+                             "(usr/local/include) header directory");
+  }
+
+  Library &Lib = getOrCreateLibrary(Directory, Libraries);
+  Lib.IsUnwrappedDylib = true;
+
+  if (DirPublic)
+    if (Error Err = scanHeaders(DirPublic->getName(), Lib, HeaderType::Public,
+                                Directory))
+      return Err;
+
+  if (DirPrivate)
+    if (Error Err = scanHeaders(DirPrivate->getName(), Lib, HeaderType::Private,
+                                Directory))
+      return Err;
+
+  return Error::success();
+}
+
+static bool isFramework(StringRef Path) {
+  while (Path.back() == '/')
+    Path = Path.slice(0, Path.size() - 1);
+
+  return llvm::StringSwitch<bool>(llvm::sys::path::extension(Path))
+      .Case(".framework", true)
+      .Default(false);
+}
+
+Library &
+DirectoryScanner::getOrCreateLibrary(StringRef Path,
+                                     std::vector<Library> &Libs) const {
+  if (Path.consume_front(RootPath) && Path.empty())
+    Path = "/";
+
+  auto LibIt =
+      find_if(Libs, [Path](const Library &L) { return L.getPath() == Path; });
+  if (LibIt != Libs.end())
+    return *LibIt;
+
+  Libs.emplace_back(Path);
+  return Libs.back();
+}
+
+Error DirectoryScanner::scanHeaders(StringRef Path, Library &Lib,
+                                    HeaderType Type, StringRef BasePath,
+                                    StringRef ParentPath) const {
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+  PathSeq SubDirectories;
+  for (vfs::directory_iterator i = FS.dir_begin(Path, ec), ie; i != ie;
+       i.increment(ec)) {
+    StringRef HeaderPath = i->path();
+    if (ec)
+      return createStringError(ec, "unable to read: " + HeaderPath);
+
+    if (sys::fs::is_symlink_file(HeaderPath))
+      continue;
+
+    // Ignore tmp files from unifdef.
+    const StringRef Filename = sys::path::filename(HeaderPath);
+    if (Filename.starts_with("."))
+      continue;
+
+    // If it is a directory, remember the subdirectory.
+    if (FM.getOptionalDirectoryRef(HeaderPath))
+      SubDirectories.push_back(HeaderPath.str());
+
+    if (!isHeaderFile(HeaderPath))
+      continue;
+
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (FS.status(HeaderPath) == std::errc::no_such_file_or_directory)
+      continue;
+
+    auto IncludeName = createIncludeHeaderName(HeaderPath);
+    Lib.addHeaderFile(HeaderPath, Type,
+                      IncludeName.has_value() ? IncludeName.value() : "");
+  }
+
+  // Go through the subdirectories.
+  // Sort the sub-directory first since different file systems might have
+  // different traverse order.
+  llvm::sort(SubDirectories);
+  if (ParentPath.empty())
+    ParentPath = Path;
+  for (const StringRef Dir : SubDirectories)
+    return scanHeaders(Dir, Lib, Type, BasePath, ParentPath);
+
+  return Error::success();
+}
+
+llvm::Error
+DirectoryScanner::scanMultipleFrameworks(StringRef Directory,
+                                         std::vector<Library> &Libs) const {
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+  for (vfs::directory_iterator i = FS.dir_begin(Directory, ec), ie; i != ie;
+       i.increment(ec)) {
+    StringRef Curr = i->path();
+
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (ec == std::errc::no_such_file_or_directory) {
+      ec.clear();
+      continue;
+    }
+    if (ec)
+      return createStringError(ec, Curr);
+
+    if (sys::fs::is_symlink_file(Curr))
+      continue;
+
+    if (isFramework(Curr)) {
+      if (FM.getOptionalDirectoryRef(Curr))
+        continue;
+      Library &Framework = getOrCreateLibrary(Curr, Libs);
+      if (Error Err = scanFrameworkDirectory(Curr, Framework))
+        return Err;
+    }
+  }
+
+  return Error::success();
+}
+
+llvm::Error
+DirectoryScanner::scanSubFrameworksDirectory(StringRef Directory,
+                                             std::vector<Library> &Libs) const {
+  if (FM.getOptionalDirectoryRef(Directory))
+    return scanMultipleFrameworks(Directory, Libs);
+
+  std::error_code ec = std::make_error_code(std::errc::not_a_directory);
+  return createStringError(ec, Directory);
+}
+
+/// FIXME: How to handle versions? For now scan them separately as independent
+/// frameworks.
+llvm::Error
+DirectoryScanner::scanFrameworkVersionsDirectory(StringRef Path,
+                                                 Library &Lib) const {
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+  for (vfs::directory_iterator i = FS.dir_begin(Path, ec), ie; i != ie;
+       i.increment(ec)) {
+    const StringRef Curr = i->path();
+
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (ec == std::errc::no_such_file_or_directory) {
+      ec.clear();
+      continue;
+    }
+    if (ec)
+      return createStringError(ec, Curr);
+
+    if (sys::fs::is_symlink_file(Curr))
+      continue;
+
+    // Each version should be a framework directory.
+    if (!FM.getOptionalDirectoryRef(Curr))
+      continue;
+
+    Library &VersionedFramework =
+        getOrCreateLibrary(Curr, Lib.FrameworkVersions);
+    if (Error Err = scanFrameworkDirectory(Curr, VersionedFramework))
+      return Err;
+  }
+
+  return Error::success();
+}
+
+llvm::Error DirectoryScanner::scanFrameworkDirectory(StringRef Path,
+                                                     Library &Framework) const {
+  // If the framework is inside Kernel or IOKit, scan headers in the different
+  // directories separately.
+  Framework.IsUnwrappedDylib =
+      Path.contains("Kernel.framework") || Path.contains("IOKit.framework");
+
+  // Unfortunately we cannot identify symlinks in the VFS. We assume that if
+  // there is a Versions directory, then we have symlinks and directly proceed
+  // to the Versions folder.
+  std::error_code ec;
+  auto &FS = FM.getVirtualFileSystem();
+
+  for (vfs::directory_iterator i = FS.dir_begin(Path, ec), ie; i != ie;
+       i.increment(ec)) {
+    StringRef Curr = i->path();
+    // Skip files that do not exist. This usually happens for broken symlinks.
+    if (ec == std::errc::no_such_file_or_directory) {
+      ec.clear();
+      continue;
+    }
+
+    if (ec)
+      return createStringError(ec, Curr);
+
+    if (sys::fs::is_symlink_file(Curr))
+      continue;
+
+    StringRef FileName = sys::path::filename(Curr);
+    // Scan all "public" headers.
+    if (FileName.contains("Headers")) {
+      if (Error Err = scanHeaders(Curr, Framework, HeaderType::Public, Curr))
+        return Err;
+      continue;
+    }
+    // Scan all "private" headers.
+    if (FileName.contains("PrivateHeaders")) {
+      if (Error Err = scanHeaders(Curr, Framework, HeaderType::Private, Curr))
+        return Err;
+      continue;
+    }
+    // Scan sub frameworks.
+    if (FileName.contains("Frameworks")) {
+      if (Error Err = scanSubFrameworksDirectory(Curr, Framework.SubFrameworks))
+        return Err;
+      continue;
+    }
+    // Check for versioned frameworks.
+    if (FileName.contains("Versions")) {
+      if (Error Err = scanFrameworkVersionsDirectory(Curr, Framework))
+        return Err;
+      continue;
+    }
+  }
+
+  return Error::success();
+}
+
+llvm::Error DirectoryScanner::scanForFrameworks(StringRef Directory) {
+  RootPath = "";
+
+  // Expect a certain directory structure and naming convention to find
+  // frameworks.
+  static const char *SubDirectories[] = {"System/Library/Frameworks/",
+                                         "System/Library/PrivateFrameworks/"};
+
+  // Check if the directory is already a framework.
+  if (isFramework(Directory)) {
+    Library &Framework = getOrCreateLibrary(Directory, Libraries);
+    if (Error Err = scanFrameworkDirectory(Directory, Framework))
+      return Err;
+    return Error::success();
+  }
+
+  // Check known sub-directory locations.
+  for (const auto *SubDir : SubDirectories) {
+    SmallString<PATH_MAX> Path(Directory);
+    sys::path::append(Path, SubDir);
+
+    if (Error Err = scanMultipleFrameworks(Path, Libraries))
+      return Err;
+  }
+
+  return Error::success();
+}
+} // namespace clang::installapi
diff --git a/clang/lib/InstallAPI/Library.cpp b/clang/lib/InstallAPI/Library.cpp
new file mode 100644
index 0000000000000..bdfa3535273e1
--- /dev/null
+++ b/clang/lib/InstallAPI/Library.cpp
@@ -0,0 +1,40 @@
+//===- Library.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/InstallAPI/Library.h"
+
+using namespace llvm;
+namespace clang::installapi {
+
+const Regex Rule("(.+)/(.+)\\.framework/");
+StringRef Library::getFrameworkNameFromInstallName(StringRef InstallName) {
+  assert(InstallName.contains(".framework") && "expected a framework");
+  SmallVector<StringRef, 3> Match;
+  Rule.match(InstallName, &Match);
+  if (Match.empty())
+    return "";
+  return Match.back();
+}
+
+StringRef Library::getName() const {
+  assert(!IsUnwrappedDylib && "expected a framework");
+  StringRef Path = BaseDirectory;
+
+  // Return the framework name extracted from path.
+  while (!Path.empty()) {
+    if (Path.ends_with(".framework"))
+      return sys::path::filename(Path);
+    Path = sys::path::parent_path(Path);
+  }
+
+  // Otherwise, return the name of the BaseDirectory.
+  Path = BaseDirectory;
+  return sys::path::filename(Path.rtrim("/"));
+}
+
+} // namespace clang::installapi
diff --git a/clang/test/InstallAPI/asm.test b/clang/test/InstallAPI/asm.test
index b6af7f643d72f..9df644a823909 100644
--- a/clang/test/InstallAPI/asm.test
+++ b/clang/test/InstallAPI/asm.test
@@ -3,7 +3,7 @@
 // RUN: sed -e "s|DSTROOT|%/t|g" %t/inputs.json.in > %t/inputs.json
 
 // RUN: clang-installapi -target arm64-apple-macos13.1 \
-// RUN: -I%t/usr/include \
+// RUN: -...
[truncated]

``````````

</details>


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


More information about the cfe-commits mailing list