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
