Author: Chuanqi Xu
Date: 2026-09-07T09:03:43Z
New Revision: 345dd01072956b7ddc36dbdfe39df2d991193e07

URL: 
https://github.com/llvm/llvm-project/commit/345dd01072956b7ddc36dbdfe39df2d991193e07
DIFF: 
https://github.com/llvm/llvm-project/commit/345dd01072956b7ddc36dbdfe39df2d991193e07.diff

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

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.

Added: 
    clang/test/ClangScanDeps/p1689-module-map.cppm

Modified: 
    clang/docs/ReleaseNotes.md
    clang/docs/StandardCPlusPlusModules.md
    clang/include/clang/DependencyScanning/ModuleDepCollector.h
    clang/include/clang/Lex/PreprocessorOptions.h
    clang/lib/DependencyScanning/ModuleDepCollector.cpp
    clang/lib/Lex/PPDirectives.cpp
    clang/lib/Tooling/DependencyScanningTool.cpp
    clang/test/ClangScanDeps/P1689.cppm

Removed: 
    


################################################################################
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 
diff erent 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 
diff erent 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 
diff erent 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"
+  }
+]


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to