[clang] [C++20] [Modules] [ScanDeps] Scan results for module map file (PR #221652)

Chuanqi Xu via cfe-commits cfe-commits at lists.llvm.org
Mon Sep 7 01:22:12 PDT 2026


https://github.com/ChuanqiXu9 updated https://github.com/llvm/llvm-project/pull/221652

>From a58b1fbd6d40ff590b6585c44a3f9ceec17dd615 Mon Sep 17 00:00:00 2001
From: Chuanqi Xu <chuanqi.xcq at alibaba-inc.com>
Date: Mon, 7 Sep 2026 14:42:06 +0800
Subject: [PATCH] [C++20] [Modules] [ScanDeps] Scan results for module map file

Previously we described the trick to use module map to import
std module implicitly. But in practice, this may not work as
build tools can't get the dependencies.

In this patch, we updated clang-scan-deps to report the dependencies
between consumers and the corresponding module described in module
map file in P1689 format. The P1689 format in clang-scan-deps
is the defacto dependency description between compiler and build
tools. This helps end users to use the trick without waiting for
the support from various build tools.
---
 clang/docs/ReleaseNotes.md                    |   3 +
 clang/docs/StandardCPlusPlusModules.md        |  31 ++++
 .../DependencyScanning/ModuleDepCollector.h   |   1 +
 clang/include/clang/Lex/PreprocessorOptions.h |   5 +
 .../DependencyScanning/ModuleDepCollector.cpp |  24 +++-
 clang/lib/Lex/PPDirectives.cpp                |  83 ++++++-----
 clang/lib/Tooling/DependencyScanningTool.cpp  |   5 +
 clang/test/ClangScanDeps/P1689.cppm           |  30 +++-
 .../test/ClangScanDeps/p1689-module-map.cppm  | 132 ++++++++++++++++++
 9 files changed, 270 insertions(+), 44 deletions(-)
 create mode 100644 clang/test/ClangScanDeps/p1689-module-map.cppm

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index d9ac67d1a2824..a49971adef86f 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -171,6 +171,9 @@ features cannot lower the translation-unit ABI level;
 
 #### C++20 Feature Support
 
+- Now clang-scan-deps in P1689 format can find the dependencies described in
+  module map file. See the documents of standard C++ modules for details.
+
 #### C++17 Feature Support
 
 #### Resolutions to C++ Defect Reports
diff --git a/clang/docs/StandardCPlusPlusModules.md b/clang/docs/StandardCPlusPlusModules.md
index e56bdd23ad0d3..4905f5ad2d49e 100644
--- a/clang/docs/StandardCPlusPlusModules.md
+++ b/clang/docs/StandardCPlusPlusModules.md
@@ -2533,6 +2533,37 @@ Individual command line options can be specified after `--`.
 options. Note that the path to the compiler executable needs to be specified
 explicitly instead of using `clang++` directly.
 
+Module maps can also introduce module dependencies by translating includes to
+imports. For example:
+
+```c++
+// a.modulemap
+module a {
+  header "a.h"
+}
+
+// use.cpp
+#include "a.h"
+```
+
+```console
+$ clang-scan-deps -format=p1689 -- <path-to-compiler-executable>/clang++ \
+    -std=c++20 use.cpp -c -o use.o -fmodule-map-file=a.modulemap
+```
+
+The rule for `use.o` contains a requirement for `a`:
+
+```text
+{
+  "primary-output": "use.o",
+  "requires": [
+    {
+      "logical-name": "a"
+    }
+  ]
+}
+```
+
 Users may want the scanner to get the transitive dependency information for
 headers. Otherwise, the project has to be scanned twice, once for headers and
 once for modules. To address this, `clang-scan-deps` will recognize the
diff --git a/clang/include/clang/DependencyScanning/ModuleDepCollector.h b/clang/include/clang/DependencyScanning/ModuleDepCollector.h
index 4713cbd9387ec..6e896bd26f763 100644
--- a/clang/include/clang/DependencyScanning/ModuleDepCollector.h
+++ b/clang/include/clang/DependencyScanning/ModuleDepCollector.h
@@ -154,6 +154,7 @@ class ModuleDepCollector final : public DependencyCollector {
   PPCallbacks *CollectorPPPtr = nullptr;
 
   void handleImport(const Module *Imported);
+  void addRequiredStdCXXModule(StringRef ModuleName);
 
   /// Returns the ID or nothing if the dependency is spurious and is ignored.
   std::optional<ModuleID> handleTopLevelModule(serialization::ModuleFile *MF);
diff --git a/clang/include/clang/Lex/PreprocessorOptions.h b/clang/include/clang/Lex/PreprocessorOptions.h
index 10a8ee98f6782..ce5a5d5b2922f 100644
--- a/clang/include/clang/Lex/PreprocessorOptions.h
+++ b/clang/include/clang/Lex/PreprocessorOptions.h
@@ -176,6 +176,10 @@ class PreprocessorOptions {
   /// be skipped so that the client can get a strict subset of the contents.
   bool SingleModuleParseMode = false;
 
+  /// When enabled, we don't try to load the corresponding module required by
+  /// the module map. This is used generally by the scanner.
+  bool DependencyScanningModuleMapImports = false;
+
   /// When enabled, the preprocessor will construct editor placeholder tokens.
   bool LexEditorPlaceholders = true;
 
@@ -262,6 +266,7 @@ class PreprocessorOptions {
     DumpDeserializedPCHDecls = false;
     ImplicitPCHInclude.clear();
     SingleFileParseMode = false;
+    DependencyScanningModuleMapImports = false;
     LexEditorPlaceholders = true;
     RetainRemappedFileBuffers = true;
     PrecompiledPreambleBytes.first = 0;
diff --git a/clang/lib/DependencyScanning/ModuleDepCollector.cpp b/clang/lib/DependencyScanning/ModuleDepCollector.cpp
index d48ac6b45cf46..5b38e5105bc31 100644
--- a/clang/lib/DependencyScanning/ModuleDepCollector.cpp
+++ b/clang/lib/DependencyScanning/ModuleDepCollector.cpp
@@ -586,6 +586,10 @@ class ModuleDepCollector::ModuleDepCollectorPP final : public PPCallbacks {
       // here as `FileChanged` will never see it.
       MDC.addFileDep(FileName);
     }
+    if (ModuleImported && SuggestedModule &&
+        MDC.ScanInstance.getPreprocessorOpts()
+            .DependencyScanningModuleMapImports)
+      MDC.addRequiredStdCXXModule(SuggestedModule->getFullModuleName());
     MDC.handleImport(SuggestedModule);
   }
 
@@ -593,10 +597,7 @@ class ModuleDepCollector::ModuleDepCollectorPP final : public PPCallbacks {
                     const Module *Imported) override {
     auto &PP = MDC.ScanInstance.getPreprocessor();
     if (PP.getLangOpts().CPlusPlusModules && PP.isImportingCXXNamedModules()) {
-      P1689ModuleInfo RequiredModule;
-      RequiredModule.ModuleName = Path[0].getIdentifierInfo()->getName().str();
-      RequiredModule.Type = P1689ModuleInfo::ModuleType::NamedCXXModule;
-      MDC.RequiredStdCXXModules.push_back(std::move(RequiredModule));
+      MDC.addRequiredStdCXXModule(Path[0].getIdentifierInfo()->getName());
       return;
     }
 
@@ -604,6 +605,19 @@ class ModuleDepCollector::ModuleDepCollectorPP final : public PPCallbacks {
   }
 };
 
+void ModuleDepCollector::addRequiredStdCXXModule(StringRef ModuleName) {
+  if (llvm::any_of(RequiredStdCXXModules,
+                   [ModuleName](const P1689ModuleInfo &RequiredModule) {
+                     return RequiredModule.ModuleName == ModuleName;
+                   }))
+    return;
+
+  P1689ModuleInfo RequiredModule;
+  RequiredModule.ModuleName = ModuleName.str();
+  RequiredModule.Type = P1689ModuleInfo::ModuleType::NamedCXXModule;
+  RequiredStdCXXModules.push_back(std::move(RequiredModule));
+}
+
 void ModuleDepCollector::handleImport(const Module *Imported) {
   auto &MDC = *this;
 
@@ -643,7 +657,7 @@ void ModuleDepCollector::run(DependencyConsumer &Consumer) {
     // Put the module as required instead. Since the implementation
     // unit will import the primary module implicitly.
     if (PP.isInImplementationUnit())
-      MDC.RequiredStdCXXModules.push_back(ProvidedModule);
+      MDC.addRequiredStdCXXModule(ProvidedModule.ModuleName);
     else
       MDC.ProvidedStdCXXModule = ProvidedModule;
   }
diff --git a/clang/lib/Lex/PPDirectives.cpp b/clang/lib/Lex/PPDirectives.cpp
index ac14bf03ae547..f1e9aaa72ff04 100644
--- a/clang/lib/Lex/PPDirectives.cpp
+++ b/clang/lib/Lex/PPDirectives.cpp
@@ -2446,6 +2446,7 @@ Preprocessor::ImportAction Preprocessor::HandleHeaderIncludeOrImport(
   // determining valid cases).
 
   enum { Enter, Import, Skip, IncludeLimitReached } Action = Enter;
+  bool DependencyScanModuleImport = false;
 
   if (PPOpts.SingleFileParseMode)
     Action = IncludeLimitReached;
@@ -2510,41 +2511,49 @@ Preprocessor::ImportAction Preprocessor::HandleHeaderIncludeOrImport(
     if (!IsImportDecl)
       diagnoseAutoModuleImport(*this, StartLoc, IncludeTok, Path, CharEnd);
 
-    // Load the module to import its macros. We'll make the declarations
-    // visible when the parser gets here.
-    // FIXME: Pass ModuleToImport in here rather than converting it to a path
-    // and making the module loader convert it back again.
-    ModuleLoadResult Imported = TheModuleLoader.loadModule(
-        IncludeTok.getLocation(), Path, Module::Hidden,
-        /*IsInclusionDirective=*/true);
-    assert((Imported == nullptr || Imported == ModuleToImport) &&
-           "the imported module is different than the suggested one");
-
-    if (Imported) {
-      Action = Import;
-    } else if (Imported.isMissingExpected()) {
-      markClangModuleAsAffecting(
-          static_cast<Module *>(Imported)->getTopLevelModule());
-      // We failed to find a submodule that we assumed would exist (because it
-      // was in the directory of an umbrella header, for instance), but no
-      // actual module containing it exists (because the umbrella header is
-      // incomplete).  Treat this as a textual inclusion.
-      ModuleToImport = nullptr;
-      UsableClangHeaderModule = false;
-    } else if (Imported.isConfigMismatch()) {
-      // On a configuration mismatch, enter the header textually. We still know
-      // that it's part of the corresponding module.
+    if (PPOpts.DependencyScanningModuleMapImports &&
+        ModuleToImport->Kind == Module::ModuleMapModule) {
+      // Dependency scanning only needs the module name. Avoid requiring the
+      // module file, which may not have been built yet.
+      Action = Skip;
+      DependencyScanModuleImport = true;
     } else {
-      // We hit an error processing the import. Bail out.
-      if (hadModuleLoaderFatalFailure()) {
-        // With a fatal failure in the module loader, we abort parsing.
-        Token &Result = IncludeTok;
-        assert(CurLexer && "#include but no current lexer set!");
-        Result.startToken();
-        CurLexer->FormTokenWithChars(Result, CurLexer->BufferEnd, tok::eof);
-        CurLexer->cutOffLexing();
+      // Load the module to import its macros. We'll make the declarations
+      // visible when the parser gets here.
+      // FIXME: Pass ModuleToImport in here rather than converting it to a path
+      // and making the module loader convert it back again.
+      ModuleLoadResult Imported = TheModuleLoader.loadModule(
+          IncludeTok.getLocation(), Path, Module::Hidden,
+          /*IsInclusionDirective=*/true);
+      assert((Imported == nullptr || Imported == ModuleToImport) &&
+             "the imported module is different than the suggested one");
+
+      if (Imported) {
+        Action = Import;
+      } else if (Imported.isMissingExpected()) {
+        markClangModuleAsAffecting(
+            static_cast<Module *>(Imported)->getTopLevelModule());
+        // We failed to find a submodule that we assumed would exist (because it
+        // was in the directory of an umbrella header, for instance), but no
+        // actual module containing it exists (because the umbrella header is
+        // incomplete).  Treat this as a textual inclusion.
+        ModuleToImport = nullptr;
+        UsableClangHeaderModule = false;
+      } else if (Imported.isConfigMismatch()) {
+        // On a configuration mismatch, enter the header textually. We still
+        // know that it's part of the corresponding module.
+      } else {
+        // We hit an error processing the import. Bail out.
+        if (hadModuleLoaderFatalFailure()) {
+          // With a fatal failure in the module loader, we abort parsing.
+          Token &Result = IncludeTok;
+          assert(CurLexer && "#include but no current lexer set!");
+          Result.startToken();
+          CurLexer->FormTokenWithChars(Result, CurLexer->BufferEnd, tok::eof);
+          CurLexer->cutOffLexing();
+        }
+        return {ImportAction::None};
       }
-      return {ImportAction::None};
     }
   }
 
@@ -2605,10 +2614,10 @@ Preprocessor::ImportAction Preprocessor::HandleHeaderIncludeOrImport(
   if (Callbacks && !IsImportDecl) {
     // Notify the callback object that we've seen an inclusion directive.
     // FIXME: Use a different callback for a pp-import?
-    Callbacks->InclusionDirective(HashLoc, IncludeTok, LookupFilename, isAngled,
-                                  FilenameRange, File, SearchPath, RelativePath,
-                                  SuggestedModule.getModule(), Action == Import,
-                                  FileCharacter);
+    Callbacks->InclusionDirective(
+        HashLoc, IncludeTok, LookupFilename, isAngled, FilenameRange, File,
+        SearchPath, RelativePath, SuggestedModule.getModule(),
+        Action == Import || DependencyScanModuleImport, FileCharacter);
     if (Action == Skip && File)
       Callbacks->FileSkipped(*File, FilenameTok, FileCharacter);
   }
diff --git a/clang/lib/Tooling/DependencyScanningTool.cpp b/clang/lib/Tooling/DependencyScanningTool.cpp
index a937879706522..f7e94bcd44e44 100644
--- a/clang/lib/Tooling/DependencyScanningTool.cpp
+++ b/clang/lib/Tooling/DependencyScanningTool.cpp
@@ -253,6 +253,11 @@ std::optional<P1689Rule> DependencyScanningTool::getP1689ModuleDependencyFile(
 
   class P1689ActionController : public DependencyActionController {
   public:
+    void initializeScanInvocation(CompilerInvocation &ScanInvocation) override {
+      ScanInvocation.getPreprocessorOpts().DependencyScanningModuleMapImports =
+          true;
+    }
+
     // The lookupModuleOutput is for clang modules. P1689 format don't need it.
     std::string lookupModuleOutput(const ModuleDeps &,
                                    ModuleOutputKind Kind) override {
diff --git a/clang/test/ClangScanDeps/P1689.cppm b/clang/test/ClangScanDeps/P1689.cppm
index a1e2215b2c591..77d75ed5b2a82 100644
--- a/clang/test/ClangScanDeps/P1689.cppm
+++ b/clang/test/ClangScanDeps/P1689.cppm
@@ -46,11 +46,12 @@
 // Check that we can generate multiple make-style dependency information with compilation database.
 // RUN: cat %t/P1689.dep | FileCheck %t/Checks.cpp -DPREFIX=%/t --check-prefix=CHECK-MAKE
 //
-// Check that we can mix the use of -format=p1689 and -fmodules.
+// Check that we can mix the use of -format=p1689 and -fmodules and that an
+// include translated through a module map is reported as a requirement.
 // RUN: clang-scan-deps -format=p1689 \
 // RUN:   -- %clang++ -std=c++20 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache -c %t/impl_part.cppm -o %t/impl_part.o \
 // RUN:   | sed 's:\\\\\?:/:g' \
-// RUN:   | FileCheck %t/impl_part.cppm -DPREFIX=%/t
+// RUN:   | FileCheck %t/impl_part.cppm -DPREFIX=%/t --check-prefix=CHECK-MODULES
 //
 // Check the path in the make style dependencies are generated in relative path form
 // RUN: cd %t
@@ -183,6 +184,31 @@ void World() {
 // CHECK-NEXT:   "version": 1
 // CHECK-NEXT: }
 
+// CHECK-MODULES:      {
+// CHECK-MODULES-NEXT:   "revision": 0,
+// CHECK-MODULES-NEXT:   "rules": [
+// CHECK-MODULES-NEXT:     {
+// CHECK-MODULES-NEXT:       "primary-output": "[[PREFIX]]/impl_part.o",
+// CHECK-MODULES-NEXT:       "provides": [
+// CHECK-MODULES-NEXT:         {
+// CHECK-MODULES-NEXT:           "is-interface": false,
+// CHECK-MODULES-NEXT:           "logical-name": "M:impl_part",
+// CHECK-MODULES-NEXT:           "source-path": "[[PREFIX]]/impl_part.cppm"
+// CHECK-MODULES-NEXT:         }
+// CHECK-MODULES-NEXT:       ],
+// CHECK-MODULES-NEXT:       "requires": [
+// CHECK-MODULES-NEXT:         {
+// CHECK-MODULES-NEXT:           "logical-name": "Mock"
+// CHECK-MODULES-NEXT:         },
+// CHECK-MODULES-NEXT:         {
+// CHECK-MODULES-NEXT:           "logical-name": "M:interface_part"
+// CHECK-MODULES-NEXT:         }
+// CHECK-MODULES-NEXT:       ]
+// CHECK-MODULES-NEXT:     }
+// CHECK-MODULES-NEXT:   ],
+// CHECK-MODULES-NEXT:   "version": 1
+// CHECK-MODULES-NEXT: }
+
 // CHECK-MAKE: [[PREFIX]]/impl_part.o.ddi:
 // CHECK-MAKE:   [[PREFIX]]/impl_part.cppm
 // CHECK-MAKE:   [[PREFIX]]/header.mock
diff --git a/clang/test/ClangScanDeps/p1689-module-map.cppm b/clang/test/ClangScanDeps/p1689-module-map.cppm
new file mode 100644
index 0000000000000..7981c07264d73
--- /dev/null
+++ b/clang/test/ClangScanDeps/p1689-module-map.cppm
@@ -0,0 +1,132 @@
+// UNSUPPORTED: system-windows
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+
+// RUN: clang-scan-deps -format=p1689 -- \
+// RUN:   %clang++ -std=c++20 -c %t/include-only.cpp -o %t/include-only.o \
+// RUN:   -fmodule-map-file=%t/module.modulemap \
+// RUN:   | FileCheck %s -DPREFIX=%/t --check-prefix=INCLUDE-ONLY
+// RUN: clang-scan-deps --mode=preprocess-dependency-directives -format=p1689 -- \
+// RUN:   %clang++ -std=c++20 -c %t/include-only.cpp -o %t/include-only.o \
+// RUN:   -fmodule-map-file=%t/module.modulemap \
+// RUN:   | FileCheck %s -DPREFIX=%/t --check-prefix=INCLUDE-ONLY
+
+// RUN: clang-scan-deps -format=p1689 -- \
+// RUN:   %clang++ -std=c++20 -c %t/mixed.cpp -o %t/mixed.o \
+// RUN:   -fmodule-map-file=%t/module.modulemap \
+// RUN:   | FileCheck %s -DPREFIX=%/t --check-prefix=MIXED
+
+// RUN: sed "s|DIR|%/t|g" %t/compile_commands.json.in > %t/compile_commands.json
+// RUN: clang-scan-deps -format=p1689 \
+// RUN:   -compilation-database %t/compile_commands.json \
+// RUN:   | FileCheck %s -DPREFIX=%/t --check-prefix=INCLUDE-ONLY
+
+// Check that this does not enable single-module parse mode, which would skip
+// all branches of a conditional whose controlling macro is undefined.
+// RUN: clang-scan-deps -format=p1689 -- \
+// RUN:   %clang++ -std=c++20 -c %t/conditional.cpp -o %t/conditional.o \
+// RUN:   -MD -MT %t/conditional.o -MF %t/conditional.dep > /dev/null
+// RUN: cat %t/conditional.dep \
+// RUN:   | FileCheck %s -DPREFIX=%/t --check-prefix=CONDITIONAL
+
+// CONDITIONAL-NOT: false.h
+// CONDITIONAL:     [[PREFIX]]/true.h
+
+// INCLUDE-ONLY:      {
+// INCLUDE-ONLY-NEXT:   "revision": 0,
+// INCLUDE-ONLY-NEXT:   "rules": [
+// INCLUDE-ONLY-NEXT:     {
+// INCLUDE-ONLY-NEXT:       "primary-output": "[[PREFIX]]/include-only.o",
+// INCLUDE-ONLY-NEXT:       "requires": [
+// INCLUDE-ONLY-NEXT:         {
+// INCLUDE-ONLY-NEXT:           "logical-name": "alpha"
+// INCLUDE-ONLY-NEXT:         },
+// INCLUDE-ONLY-NEXT:         {
+// INCLUDE-ONLY-NEXT:           "logical-name": "beta.gamma"
+// INCLUDE-ONLY-NEXT:         }
+// INCLUDE-ONLY-NEXT:       ]
+// INCLUDE-ONLY-NEXT:     }
+// INCLUDE-ONLY-NEXT:   ],
+// INCLUDE-ONLY-NEXT:   "version": 1
+// INCLUDE-ONLY-NEXT: }
+
+// MIXED:      {
+// MIXED-NEXT:   "revision": 0,
+// MIXED-NEXT:   "rules": [
+// MIXED-NEXT:     {
+// MIXED-NEXT:       "primary-output": "[[PREFIX]]/mixed.o",
+// MIXED-NEXT:       "requires": [
+// MIXED-NEXT:         {
+// MIXED-NEXT:           "logical-name": "alpha"
+// MIXED-NEXT:         }
+// MIXED-NEXT:       ]
+// MIXED-NEXT:     }
+// MIXED-NEXT:   ],
+// MIXED-NEXT:   "version": 1
+// MIXED-NEXT: }
+
+//--- module.modulemap
+module alpha {
+  header "alpha-1.h"
+  header "alpha-2.h"
+}
+module beta {
+  module gamma {
+    header "beta.h"
+  }
+}
+module unused {
+  header "unused.h"
+}
+
+//--- alpha-1.h
+#pragma once
+#error mapped headers must not be textually included
+
+//--- alpha-2.h
+#pragma once
+#error mapped headers must not be textually included
+
+//--- beta.h
+#pragma once
+#error mapped headers must not be textually included
+
+//--- unused.h
+#pragma once
+
+//--- unmapped.h
+#pragma once
+
+//--- false.h
+#pragma once
+
+//--- true.h
+#pragma once
+
+//--- include-only.cpp
+#include "alpha-1.h"
+#include "alpha-2.h"
+#include "beta.h"
+#include "unmapped.h"
+
+//--- mixed.cpp
+#include "alpha-1.h"
+import alpha;
+
+//--- conditional.cpp
+#if UNDEFINED
+#include "false.h"
+#else
+#include "true.h"
+#endif
+
+//--- compile_commands.json.in
+[
+  {
+    "directory": "DIR",
+    "command": "clang++ -std=c++20 -c DIR/include-only.cpp -o DIR/include-only.o -fmodule-map-file=DIR/module.modulemap",
+    "file": "DIR/include-only.cpp",
+    "output": "DIR/include-only.o"
+  }
+]



More information about the cfe-commits mailing list