[clang] [clang][modules] Make -fmodules-decluse work on the public/private pair of modules (PR #192585)

via cfe-commits cfe-commits at lists.llvm.org
Thu Apr 16 19:52:39 PDT 2026


https://github.com/matts1 created https://github.com/llvm/llvm-project/pull/192585

Previously, it would only check the main module.
Now, if the main module is a private module (foo_Private), it will also
check the public module.

>From 6b72aad0ffcdbbfe688491ab32f750a1e73ada44 Mon Sep 17 00:00:00 2001
From: Matt Stark <msta at google.com>
Date: Thu, 16 Apr 2026 16:28:27 +1000
Subject: [PATCH 1/2] [clang][modules] Write test for fmodules-decluse with
 private modules.

This just creates a test that shows the existing behaviour of clang, so
that it's clear what changes I'm making.
---
 .../Modules/declare-use-private-textual.cpp   | 69 +++++++++++++++++++
 1 file changed, 69 insertions(+)
 create mode 100644 clang/test/Modules/declare-use-private-textual.cpp

diff --git a/clang/test/Modules/declare-use-private-textual.cpp b/clang/test/Modules/declare-use-private-textual.cpp
new file mode 100644
index 0000000000000..92c7144559ee7
--- /dev/null
+++ b/clang/test/Modules/declare-use-private-textual.cpp
@@ -0,0 +1,69 @@
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+// RUN: %clang_cc1 -fimplicit-module-maps -fmodules-cache-path=%t -fmodules-strict-decluse -fmodule-name=lib_Private -I %t %t/lib.cpp -verify
+
+//--- module.modulemap
+// This simulates a modulemap that might reasonably be generated by a build system for something resembling the following.
+// This can be used to perform strict dependency checking to ensure that your build graph is correctly declared.
+// cc_library(
+//   name = "lib",
+//   # .cpp file in sources -> generate a compilation action with -fmodule-name=lib_Private
+//   # .h file in sources -> add "textual header" to private modulemap
+//   sources = ["lib.cpp"],
+//   # headers -> textual header in public module
+//   headers = ["lib.h"],
+//   # Implementation dep -> in use of private module
+//   implementation_deps = [":impl_dep"]
+//   # dep -> in use of private and public module
+//   deps = [":direct"],
+// )
+// cc_library(name = "direct", headers = ["direct.h"], deps = [":indirect"], ...)
+// cc_library(name = "indirect", headers = ["indirect.h"], ..)
+// cc_library(name = "impl_dep", headers = ["impl_dep.h"], ...)
+module lib_Private {
+  use impl_dep
+  use lib
+  use direct
+}
+
+module lib {
+  textual header "lib.h"
+  use direct
+}
+
+
+module impl_dep {
+  textual header "impl_dep.h"
+}
+
+module direct {
+  textual header "direct.h"
+  use indirect
+}
+
+module indirect {
+  textual header "indirect.h"
+}
+
+//--- impl_dep.h
+void impl_dep();
+//--- direct.h
+#include "impl_dep.h" // OK (it should only verify the main module).
+void direct();
+//--- indirect.h
+void indirect();
+//--- not_module.h
+void not_module();
+
+//--- lib.cpp
+#include "lib.h"
+#include "direct.h" // OK
+#include "indirect.h" // expected-error {{module lib_Private does not directly depend on a module exporting 'indirect.h', which is part of indirectly-used module indirect}}
+#include "impl_dep.h" // OK
+#include "not_module.h" // expected-error {{lib_Private does not depend on a module exporting 'not_module.h'}}
+
+//--- lib.h
+#include "direct.h" // OK
+#include "indirect.h"  // OK
+#include "impl_dep.h"  // OK
+#include "not_module.h" // OK
\ No newline at end of file

>From 59766087d89ba1e41569fec00b318557bf3b19c2 Mon Sep 17 00:00:00 2001
From: Matt Stark <msta at google.com>
Date: Thu, 16 Apr 2026 16:28:27 +1000
Subject: [PATCH 2/2] [clang][modules] Make -fmodules-decluse work on the
 public/private pair of modules

Previously, it would only check the main module.
Now, if the main module is a private module (foo_Private), it will also
check the public module.
---
 .../include/clang/Basic/DiagnosticLexKinds.td |  2 +
 clang/lib/Lex/HeaderSearch.cpp                | 14 +++++--
 clang/lib/Lex/ModuleMap.cpp                   | 42 ++++++++++++++++---
 .../Modules/declare-use-private-textual.cpp   |  6 +--
 4 files changed, 52 insertions(+), 12 deletions(-)

diff --git a/clang/include/clang/Basic/DiagnosticLexKinds.td b/clang/include/clang/Basic/DiagnosticLexKinds.td
index 2846db8320275..85fa290de6fd9 100644
--- a/clang/include/clang/Basic/DiagnosticLexKinds.td
+++ b/clang/include/clang/Basic/DiagnosticLexKinds.td
@@ -999,6 +999,8 @@ def err_undeclared_use_of_module : Error<
   "module %0 does not depend on a module exporting '%1'">;
 def err_undeclared_use_of_module_indirect : Error<
   "module %0 does not directly depend on a module exporting '%1', which is part of indirectly-used module %2">;
+def err_undeclared_use_of_module_private : Error<
+  "module %0 does not depend on a module exporting '%1', which is part of module %2, although its private module does">;
 def warn_non_modular_include_in_framework_module : Warning<
   "include of non-modular header inside framework module '%0': '%1'">,
   InGroup<NonModularIncludeInFrameworkModule>, DefaultIgnore;
diff --git a/clang/lib/Lex/HeaderSearch.cpp b/clang/lib/Lex/HeaderSearch.cpp
index 5cc2c04a68077..ecdfc701c2e22 100644
--- a/clang/lib/Lex/HeaderSearch.cpp
+++ b/clang/lib/Lex/HeaderSearch.cpp
@@ -58,6 +58,8 @@ ALWAYS_ENABLED_STATISTIC(NumFrameworkLookups, "Number of framework lookups.");
 ALWAYS_ENABLED_STATISTIC(NumSubFrameworkLookups,
                          "Number of subframework lookups.");
 
+static constexpr StringRef ModuleMapExtension = ".modulemap";
+
 const IdentifierInfo *
 HeaderFileInfo::getControllingMacro(ExternalPreprocessorSource *External) {
   if (LazyControllingMacro.isID()) {
@@ -2059,12 +2061,16 @@ static OptionalFileEntryRef getPrivateModuleMap(FileEntryRef File,
                                                 bool Diagnose = true) {
   StringRef Filename = llvm::sys::path::filename(File.getName());
   SmallString<128>  PrivateFilename(File.getDir().getName());
-  if (Filename == "module.map")
+  if (Filename.ends_with(ModuleMapExtension)) {
+    SmallString<128> PrivateName(Filename);
+    PrivateName.resize(PrivateName.size() - ModuleMapExtension.size());
+    PrivateName.append(".private.modulemap");
+    llvm::sys::path::append(PrivateFilename, PrivateName);
+  } else if (Filename == "module.map") {
     llvm::sys::path::append(PrivateFilename, "module_private.map");
-  else if (Filename == "module.modulemap")
-    llvm::sys::path::append(PrivateFilename, "module.private.modulemap");
-  else
+  } else {
     return std::nullopt;
+  }
   auto PMMFile = FileMgr.getOptionalFileRef(PrivateFilename);
   if (PMMFile) {
     if (Diagnose && Filename == "module.map")
diff --git a/clang/lib/Lex/ModuleMap.cpp b/clang/lib/Lex/ModuleMap.cpp
index 56ae51ada7148..e78c6693cbd10 100644
--- a/clang/lib/Lex/ModuleMap.cpp
+++ b/clang/lib/Lex/ModuleMap.cpp
@@ -46,6 +46,8 @@
 
 using namespace clang;
 
+static constexpr llvm::StringRef kPrivateModuleSuffix = "_Private";
+
 void ModuleMapCallbacks::anchor() {}
 
 void ModuleMap::resolveLinkAsDependencies(Module *Mod) {
@@ -498,10 +500,31 @@ void ModuleMap::diagnoseHeaderInclusion(Module *RequestingModule,
 
   // No errors for indirect modules. This may be a bit of a problem for modules
   // with no source files.
-  if (getTopLevelOrNull(RequestingModule) != getTopLevelOrNull(SourceModule))
-    return;
+  Module *TopLevelRequestingModule = getTopLevelOrNull(RequestingModule);
+  Module *TopLevelSourceModule = getTopLevelOrNull(SourceModule);
+  bool IsPublicForMainPrivateModule = false;
+  if (TopLevelRequestingModule != TopLevelSourceModule) {
+    // Suppose we have a pair of files foo.cpp / foo.h.
+    // Our build system may want to verify that foo.cpp only uses things
+    // declared in the implementation_deps of foo, while foo.h only uses things
+    // declared in interface_deps. This requires them to be two seperate
+    // modules, foo_Private and foo. This check is required to ensure that foo.h
+    // is still checked. Otherwise, foo.h would never be checked, since it will
+    // never be the top-level module.
+    if (TopLevelRequestingModule && TopLevelSourceModule &&
+        llvm::StringRef(TopLevelSourceModule->Name)
+            .ends_with(kPrivateModuleSuffix) &&
+        llvm::StringRef(TopLevelSourceModule->Name)
+                .drop_back(kPrivateModuleSuffix.size()) ==
+            TopLevelRequestingModule->Name) {
+      IsPublicForMainPrivateModule = true;
+    } else {
+      return;
+    }
+  }
 
   bool Excluded = false;
+  bool UsedByPrivateModule = false;
   Module *Private = nullptr;
   Module *NotUsed = nullptr;
 
@@ -524,6 +547,9 @@ void ModuleMap::diagnoseHeaderInclusion(Module *RequestingModule,
       if (RequestingModule && LangOpts.ModulesDeclUse &&
           !RequestingModule->directlyUses(Header.getModule())) {
         NotUsed = Header.getModule();
+        if (IsPublicForMainPrivateModule) {
+          UsedByPrivateModule = SourceModule->directlyUses(Header.getModule());
+        }
         continue;
       }
 
@@ -543,9 +569,15 @@ void ModuleMap::diagnoseHeaderInclusion(Module *RequestingModule,
 
   // We have found a module, but we don't use it.
   if (NotUsed) {
-    Diags.Report(FilenameLoc, diag::err_undeclared_use_of_module_indirect)
-        << RequestingModule->getTopLevelModule()->Name << Filename
-        << NotUsed->Name;
+    if (UsedByPrivateModule) {
+      Diags.Report(FilenameLoc, diag::err_undeclared_use_of_module_private)
+          << RequestingModule->getTopLevelModule()->Name << Filename
+          << NotUsed->Name << SourceModule->Name;
+    } else {
+      Diags.Report(FilenameLoc, diag::err_undeclared_use_of_module_indirect)
+          << RequestingModule->getTopLevelModule()->Name << Filename
+          << NotUsed->Name;
+    }
     return;
   }
 
diff --git a/clang/test/Modules/declare-use-private-textual.cpp b/clang/test/Modules/declare-use-private-textual.cpp
index 92c7144559ee7..c030e2afa6e6d 100644
--- a/clang/test/Modules/declare-use-private-textual.cpp
+++ b/clang/test/Modules/declare-use-private-textual.cpp
@@ -64,6 +64,6 @@ void not_module();
 
 //--- lib.h
 #include "direct.h" // OK
-#include "indirect.h"  // OK
-#include "impl_dep.h"  // OK
-#include "not_module.h" // OK
\ No newline at end of file
+#include "indirect.h"  // expected-error {{module lib does not directly depend on a module exporting 'indirect.h', which is part of indirectly-used module indirect}}
+#include "impl_dep.h"  // expected-error {{module lib does not depend on a module exporting 'impl_dep.h', which is part of module impl_dep, although its private module does}}
+#include "not_module.h" // expected-error {{module lib does not depend on a module exporting 'not_module.h'}}
\ No newline at end of file



More information about the cfe-commits mailing list