Skip to content

[modulemap] Add -fno-implicit-module-map-file= to exclude from implicit discovery#208191

Open
torarnv wants to merge 1 commit into
llvm:mainfrom
torarnv:add-flag-to-ignore-specific-modulemap
Open

[modulemap] Add -fno-implicit-module-map-file= to exclude from implicit discovery#208191
torarnv wants to merge 1 commit into
llvm:mainfrom
torarnv:add-flag-to-ignore-specific-modulemap

Conversation

@torarnv

@torarnv torarnv commented Jul 8, 2026

Copy link
Copy Markdown
Member

Add a driver/cc1 flag that excludes a specific module map file from implicit module map discovery (-fimplicit-module-maps), without disabling implicit discovery globally. The excluded map can still be loaded explicitly via -fmodule-map-file=.

The exclusion is applied in HeaderSearch::lookupModuleMapFile(), the single entry point for implicit discovery, so all callers (directory scanning and framework inference) honor it. Matching is done by the underlying FileEntry rather than by string, making it robust to path spelling, symlinks, and '.'/'..' components.

This lets a project ship a standard-named module.modulemap for consumers that opt in to modules (e.g. Swift's ClangImporter, which relies on implicit discovery) while preventing unrelated C++ consumers from implicitly modularizing the same headers. The party that must opt out (the C++ consumer) is the one that can inject the flag, and the shipped module map stays standard syntax.

@llvmorg-github-actions llvmorg-github-actions Bot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules labels Jul 8, 2026
@torarnv torarnv changed the title [modulemap] Add -fno-implicit-module-map-file= to exclude maps from i… [modulemap] Add -fno-implicit-module-map-file= to exclude from implicit discovery Jul 8, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-clang-modules

@llvm/pr-subscribers-clang

Author: Tor Arne Vestbø (torarnv)

Changes

…mplicit discovery

Add a driver/cc1 flag that excludes a specific module map file from implicit module map discovery (-fimplicit-module-maps), without disabling implicit discovery globally. The excluded map can still be loaded explicitly via -fmodule-map-file=.

The exclusion is applied in HeaderSearch::lookupModuleMapFile(), the single entry point for implicit discovery, so all callers (directory scanning and framework inference) honor it. Matching is done by the underlying FileEntry rather than by string, making it robust to path spelling, symlinks, and '.'/'..' components.

This lets a project ship a standard-named module.modulemap for consumers that opt in to modules (e.g. Swift's ClangImporter, which relies on implicit discovery) while preventing unrelated C++ consumers from implicitly modularizing the same headers. The party that must opt out (the C++ consumer) is the one that can inject the flag, and the shipped module map stays standard syntax.


Full diff: https://github.com/llvm/llvm-project/pull/208191.diff

6 Files Affected:

  • (modified) clang/docs/ReleaseNotes.md (+7)
  • (modified) clang/include/clang/Lex/HeaderSearch.h (+11)
  • (modified) clang/include/clang/Lex/HeaderSearchOptions.h (+6)
  • (modified) clang/include/clang/Options/Options.td (+7)
  • (modified) clang/lib/Lex/HeaderSearch.cpp (+33-2)
  • (added) clang/test/Modules/no-implicit-module-map-file.c (+67)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index b7142ecb072ff..8e69c9b600c15 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -416,6 +416,13 @@ latest release, please see the [Clang Web Site](https://clang.llvm.org) or the
   reproducable builds. These macros can be redefined from the command line if
   necessary. `/d1nodatetime-` can be used to turn this feature off if
   necessary to override the common build settings.
+- New option `-fno-implicit-module-map-file=<file>` added to exclude a specific
+  module map from *implicit* module map discovery (`-fimplicit-module-maps`),
+  without disabling implicit discovery globally. The excluded map can still be
+  loaded explicitly via `-fmodule-map-file=`. This lets a project ship a
+  standard-named `module.modulemap` for consumers that opt in (e.g. Swift's
+  ClangImporter) while preventing unrelated C++ consumers from implicitly
+  modularizing the same headers.
 
 ### Deprecated Compiler Flags
 
diff --git a/clang/include/clang/Lex/HeaderSearch.h b/clang/include/clang/Lex/HeaderSearch.h
index 962787cc48a17..5ad8facb60b73 100644
--- a/clang/include/clang/Lex/HeaderSearch.h
+++ b/clang/include/clang/Lex/HeaderSearch.h
@@ -21,6 +21,7 @@
 #include "clang/Lex/ModuleMap.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/MapVector.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringMap.h"
@@ -30,6 +31,7 @@
 #include <cassert>
 #include <cstddef>
 #include <memory>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
@@ -277,6 +279,11 @@ class HeaderSearch {
   /// a system header.
   std::vector<std::pair<std::string, bool>> SystemHeaderPrefixes;
 
+  /// Lazily-computed set of module map files, resolved to their underlying
+  /// \c FileEntry, that should be excluded from implicit module map discovery.
+  /// Derived from \c HeaderSearchOptions::ExcludedImplicitModuleMapFiles.
+  std::optional<llvm::DenseSet<const FileEntry *>> ExcludedImplicitModuleMaps;
+
   /// The context hash used in SpecificModuleCachePath (unless suppressed).
   std::string ContextHash;
 
@@ -731,6 +738,10 @@ class HeaderSearch {
   OptionalFileEntryRef lookupModuleMapFile(DirectoryEntryRef Dir,
                                            bool IsFramework);
 
+  /// Determine whether the given module map file has been excluded from
+  /// implicit module map discovery via \c -fno-implicit-module-map-file=.
+  bool isModuleMapFileExcludedFromImplicitDiscovery(FileEntryRef File);
+
   /// Determine whether there is a module map that may map the header
   /// with the given file name to a (sub)module.
   /// Always returns false if modules are disabled.
diff --git a/clang/include/clang/Lex/HeaderSearchOptions.h b/clang/include/clang/Lex/HeaderSearchOptions.h
index fd46ea223bae1..2c7bf65451879 100644
--- a/clang/include/clang/Lex/HeaderSearchOptions.h
+++ b/clang/include/clang/Lex/HeaderSearchOptions.h
@@ -187,6 +187,12 @@ class HeaderSearchOptions {
   /// The set of user-provided virtual filesystem overlay files.
   std::vector<std::string> VFSOverlayFiles;
 
+  /// Module map files that should be excluded from implicit module map
+  /// discovery (i.e. the ones that would otherwise be found by name while
+  /// walking header search directories). They can still be loaded explicitly
+  /// via \c -fmodule-map-file=.
+  std::vector<std::string> ExcludedImplicitModuleMapFiles;
+
   /// Include the compiler builtin includes.
   LLVM_PREFERRED_TYPE(bool)
   unsigned UseBuiltinIncludes : 1;
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 35b0390bd2fc0..3d8e76c6c18f7 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -3802,6 +3802,13 @@ def fmodule_map_file : Joined<["-"], "fmodule-map-file=">,
   MetaVarName<"<file>">,
   HelpText<"Load this module map file">,
   MarshallingInfoStringVector<FrontendOpts<"ModuleMapFiles">>;
+def fno_implicit_module_map_file : Joined<["-"], "fno-implicit-module-map-file=">,
+  Group<f_Group>,
+  Visibility<[ClangOption, CC1Option, CLOption]>,
+  MetaVarName<"<file>">,
+  HelpText<"Exclude this module map file from implicit module map discovery. "
+           "It can still be loaded explicitly via -fmodule-map-file=.">,
+  MarshallingInfoStringVector<HeaderSearchOpts<"ExcludedImplicitModuleMapFiles">>;
 def fmodule_file : Joined<["-"], "fmodule-file=">,
   Group<i_Group>,
   Visibility<[ClangOption, CC1Option, CLOption]>,
diff --git a/clang/lib/Lex/HeaderSearch.cpp b/clang/lib/Lex/HeaderSearch.cpp
index ecd80db10f2bd..8abec291de1ae 100644
--- a/clang/lib/Lex/HeaderSearch.cpp
+++ b/clang/lib/Lex/HeaderSearch.cpp
@@ -2171,10 +2171,39 @@ HeaderSearch::parseModuleMapFileImpl(FileEntryRef File, bool IsSystem,
   return MMR_NewlyProcessed;
 }
 
+bool HeaderSearch::isModuleMapFileExcludedFromImplicitDiscovery(
+    FileEntryRef File) {
+  if (HSOpts.ExcludedImplicitModuleMapFiles.empty())
+    return false;
+
+  // Resolve the excluded paths to their underlying files once, so that
+  // matching is robust to path spelling (relative vs absolute, symlinks, ...)
+  // rather than a brittle string comparison.
+  if (!ExcludedImplicitModuleMaps) {
+    ExcludedImplicitModuleMaps.emplace();
+    for (StringRef Path : HSOpts.ExcludedImplicitModuleMapFiles)
+      if (auto Excluded = FileMgr.getOptionalFileRef(Path))
+        ExcludedImplicitModuleMaps->insert(&Excluded->getFileEntry());
+  }
+
+  return ExcludedImplicitModuleMaps->contains(&File.getFileEntry());
+}
+
 OptionalFileEntryRef
 HeaderSearch::lookupModuleMapFile(DirectoryEntryRef Dir, bool IsFramework) {
   if (!HSOpts.ImplicitModuleMaps)
     return std::nullopt;
+
+  // Return the given implicitly-discovered module map file unless it has been
+  // excluded via -fno-implicit-module-map-file=, in which case discovery
+  // continues as if the file were not present.
+  auto ReturnUnlessExcluded =
+      [&](FileEntryRef F) -> OptionalFileEntryRef {
+    if (isModuleMapFileExcludedFromImplicitDiscovery(F))
+      return std::nullopt;
+    return F;
+  };
+
   // For frameworks, the preferred spelling is Modules/module.modulemap, but
   // module.map at the framework root is also accepted.
   SmallString<128> ModuleMapFileName(Dir.getName());
@@ -2182,12 +2211,14 @@ HeaderSearch::lookupModuleMapFile(DirectoryEntryRef Dir, bool IsFramework) {
     llvm::sys::path::append(ModuleMapFileName, "Modules");
   llvm::sys::path::append(ModuleMapFileName, "module.modulemap");
   if (auto F = FileMgr.getOptionalFileRef(ModuleMapFileName))
-    return *F;
+    return ReturnUnlessExcluded(*F);
 
   // Continue to allow module.map, but warn it's deprecated.
   ModuleMapFileName = Dir.getName();
   llvm::sys::path::append(ModuleMapFileName, "module.map");
   if (auto F = FileMgr.getOptionalFileRef(ModuleMapFileName)) {
+    if (isModuleMapFileExcludedFromImplicitDiscovery(*F))
+      return std::nullopt;
     Diags.Report(diag::warn_deprecated_module_dot_map)
         << ModuleMapFileName << 0 << IsFramework;
     return *F;
@@ -2200,7 +2231,7 @@ HeaderSearch::lookupModuleMapFile(DirectoryEntryRef Dir, bool IsFramework) {
     llvm::sys::path::append(ModuleMapFileName, "Modules",
                             "module.private.modulemap");
     if (auto F = FileMgr.getOptionalFileRef(ModuleMapFileName))
-      return *F;
+      return ReturnUnlessExcluded(*F);
   }
   return std::nullopt;
 }
diff --git a/clang/test/Modules/no-implicit-module-map-file.c b/clang/test/Modules/no-implicit-module-map-file.c
new file mode 100644
index 0000000000000..5e91f51a3fc19
--- /dev/null
+++ b/clang/test/Modules/no-implicit-module-map-file.c
@@ -0,0 +1,67 @@
+// Test -fno-implicit-module-map-file=: exclude a specific module map from
+// *implicit* module-map discovery, without disabling implicit discovery
+// globally, and without affecting explicit -fmodule-map-file= loading or other
+// implicitly-discovered maps.
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+
+// Baseline: the module map is discovered implicitly and the #include is
+// translated into an import of module 'Foo'.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -I %t/include -Rmodule-include-translation -verify=translated %t/use-foo.c
+
+// Excluded: the same map is skipped for implicit discovery, so the #include
+// stays textual (no translation) and compiles as an ordinary header.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -I %t/include -Rmodule-include-translation \
+// RUN:   -fno-implicit-module-map-file=%t/include/module.modulemap \
+// RUN:   -verify=textual %t/use-foo.c
+
+// Path robustness: a different spelling of the same file (extra "/./" and a
+// "/../") still matches, because matching is by underlying file, not string.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -I %t/include -Rmodule-include-translation \
+// RUN:   -fno-implicit-module-map-file=%t/include/./sub/../module.modulemap \
+// RUN:   -verify=textual %t/use-foo.c
+
+// Explicit still works: even though the map is on the exclusion list, loading
+// it explicitly via -fmodule-map-file= makes 'Foo' modular again, so the
+// include is translated.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -I %t/include -Rmodule-include-translation \
+// RUN:   -fno-implicit-module-map-file=%t/include/module.modulemap \
+// RUN:   -fmodule-map-file=%t/include/module.modulemap \
+// RUN:   -verify=translated %t/use-foo.c
+
+// Other maps unaffected: excluding Foo's map does not stop a different,
+// non-excluded map from being discovered and used.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN:   -I %t/include -I %t/other -Rmodule-include-translation \
+// RUN:   -fno-implicit-module-map-file=%t/include/module.modulemap \
+// RUN:   -verify=other %t/use-bar.c
+
+//--- include/module.modulemap
+module Foo { header "foo.h" export * }
+
+//--- include/foo.h
+void foo(void);
+
+//--- include/sub/.keep
+
+//--- other/module.modulemap
+module Bar { header "bar.h" export * }
+
+//--- other/bar.h
+void bar(void);
+
+//--- use-foo.c
+// translated-remark@+2 {{treating #include as an import of module 'Foo'}}
+// textual-no-diagnostics
+#include "foo.h"
+void test(void) { foo(); }
+
+//--- use-bar.c
+// other-remark@+1 {{treating #include as an import of module 'Bar'}}
+#include "bar.h"
+void test(void) { bar(); }

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

…mplicit discovery

Add a driver/cc1 flag that excludes a specific module map file from
*implicit* module map discovery (-fimplicit-module-maps), without
disabling implicit discovery globally. The excluded map can still be
loaded explicitly via -fmodule-map-file=.

The exclusion is applied in HeaderSearch::lookupModuleMapFile(), the
single entry point for implicit discovery, so all callers (directory
scanning and framework inference) honor it. Matching is done by the
underlying FileEntry rather than by string, making it robust to path
spelling, symlinks, and '.'/'..' components.

This lets a project ship a standard-named module.modulemap for
consumers that opt in to modules (e.g. Swift's ClangImporter, which
relies on implicit discovery) while preventing unrelated C++ consumers
from implicitly modularizing the same headers. The party that must opt
out (the C++ consumer) is the one that can inject the flag, and the
shipped module map stays standard syntax.
@torarnv torarnv force-pushed the add-flag-to-ignore-specific-modulemap branch from 8106ad4 to e81b0d4 Compare July 8, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant