//===------------------ ProjectModules.cpp --------- ------------*- C++-*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "ProjectModules.h" #include "Compiler.h" #include "support/Logger.h" #include "clang/DependencyScanning/DependencyScanningService.h" #include "clang/Tooling/DependencyScanningTool.h" #include "clang/Tooling/Tooling.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/Path.h" #include "llvm/TargetParser/Host.h" namespace clang::clangd { namespace { llvm::SmallString<128> normalizePath(PathRef Path) { llvm::SmallString<128> Result(Path); llvm::sys::path::remove_dots(Result, /*remove_dot_dot=*/true); llvm::sys::path::native(Result, llvm::sys::path::Style::posix); return Result; } std::string normalizePath(PathRef Path, PathRef WorkingDir) { if (Path.empty()) return {}; llvm::SmallString<128> Result; if (llvm::sys::path::is_absolute(Path) || WorkingDir.empty()) Result = Path; else { Result = WorkingDir; llvm::sys::path::append(Result, Path); } return normalizePath(Result).str().str(); } /// The information related to modules parsed from compile commands. /// Including the source file, the module file it produces (if it is a /// producer), and the module and the corresponding module files it /// requires (if it is a consumer) struct ParsedCompileCommandInfo { std::string SourceFile; std::optional OutputModuleFile; // Map from required module name to the module file path. llvm::StringMap RequiredModuleFiles; }; /// Get ParsedCompileCommandInfo by looking at the '--precompile', /// '-fmodule-file=' and '-fmodule-file=' commands in the compile command. std::optional parseCompileCommandInfo(tooling::CompileCommand Cmd, const ThreadsafeFS &TFS) { auto FS = TFS.view(std::nullopt); auto Tokenizer = llvm::Triple(llvm::sys::getProcessTriple()).isOSWindows() ? llvm::cl::TokenizeWindowsCommandLine : llvm::cl::TokenizeGNUCommandLine; tooling::addExpandedResponseFiles(Cmd.CommandLine, Cmd.Directory, Tokenizer, *FS); ParsedCompileCommandInfo Result; Result.SourceFile = normalizePath(Cmd.Filename, Cmd.Directory); bool SawPrecompile = false; for (size_t I = 1; I < Cmd.CommandLine.size(); ++I) { llvm::StringRef Arg = Cmd.CommandLine[I]; if (Arg == "--precompile") { SawPrecompile = true; continue; } if (Arg.consume_front("-fmodule-output=")) { Result.OutputModuleFile = normalizePath(Arg, Cmd.Directory); continue; } if (Arg == "-fmodule-output" && I + 1 < Cmd.CommandLine.size()) { Result.OutputModuleFile = normalizePath(Cmd.CommandLine[++I], Cmd.Directory); continue; } if (SawPrecompile && Arg == "-o" && I + 1 < Cmd.CommandLine.size()) { Result.OutputModuleFile = normalizePath(Cmd.CommandLine[++I], Cmd.Directory); continue; } if (SawPrecompile && Arg.starts_with("-o") && Arg.size() > 2) { Result.OutputModuleFile = normalizePath(Arg.drop_front(2), Cmd.Directory); continue; } if (!Arg.consume_front("-fmodule-file=")) continue; auto Sep = Arg.find('='); if (Sep == llvm::StringRef::npos || Sep == 0 || Sep + 1 == Arg.size()) continue; Result.RequiredModuleFiles[Arg.take_front(Sep)] = normalizePath(Arg.drop_front(Sep + 1), Cmd.Directory); } return Result; } std::optional getCompileCommandForFile(const clang::tooling::CompilationDatabase &CDB, PathRef FilePath, const ProjectModules::CommandMangler &Mangler) { auto Candidates = CDB.getCompileCommands(FilePath); if (Candidates.empty()) return std::nullopt; // Choose the first candidates as the compile commands as the file. // Following the same logic with // DirectoryBasedGlobalCompilationDatabase::getCompileCommand. tooling::CompileCommand Cmd = std::move(Candidates.front()); if (Mangler) Mangler(Cmd, FilePath); return Cmd; } /// A scanner to query the dependency information for C++20 Modules. /// /// The scanner can scan a single file with `scan(PathRef)` member function /// or scan the whole project with `globalScan(vector)` member /// function. See the comments of `globalScan` to see the details. /// /// The ModuleDependencyScanner can get the directly required module names for a /// specific source file. Also the ModuleDependencyScanner can get the source /// file declaring the primary module interface for a specific module name. /// /// IMPORTANT NOTE: we assume that every module unit is only declared once in a /// source file in the project. But the assumption is not strictly true even /// besides the invalid projects. The language specification requires that every /// module unit should be unique in a valid program. But a project can contain /// multiple programs. Then it is valid that we can have multiple source files /// declaring the same module in a project as long as these source files don't /// interfere with each other. class ModuleDependencyScanner { public: ModuleDependencyScanner( std::shared_ptr CDB, const ThreadsafeFS &TFS) : CDB(CDB), Service([&TFS] { dependencies::DependencyScanningServiceOptions Opts; Opts.MakeVFS = [&] { return TFS.view(std::nullopt); }; Opts.Mode = dependencies::ScanningMode::CanonicalPreprocessing; Opts.EmitWarnings = false; Opts.ReportAbsolutePaths = false; return Opts; }()) {} /// The scanned modules dependency information for a specific source file. struct ModuleDependencyInfo { /// The name of the module if the file is a module unit. std::optional ModuleName; /// A list of names for the modules that the file directly depends. std::vector RequiredModules; }; /// Scanning the single file specified by \param FilePath. std::optional scan(PathRef FilePath, const ProjectModules::CommandMangler &Mangler); /// Scanning every source file in the current project to get the /// to map. /// TODO: We should find an efficient method to get the /// to map. We can make it either by providing /// a global module dependency scanner to monitor every file. Or we /// can simply require the build systems (or even the end users) /// to provide the map. void globalScan(const ProjectModules::CommandMangler &Mangler); /// Get the source file from the module name. Note that the language /// guarantees all the module names are unique in a valid program. /// This function should only be called after globalScan. /// /// TODO: We should handle the case that there are multiple source files /// declaring the same module. PathRef getSourceForModuleName(llvm::StringRef ModuleName) const; /// Return the direct required modules. Indirect required modules are not /// included. std::vector getRequiredModules(PathRef File, const ProjectModules::CommandMangler &Mangler); private: std::shared_ptr CDB; // Whether the scanner has scanned the project globally. bool GlobalScanned = false; clang::dependencies::DependencyScanningService Service; // TODO: Add a scanning cache. // Map module name to source file path. llvm::StringMap ModuleNameToSource; }; std::optional ModuleDependencyScanner::scan(PathRef FilePath, const ProjectModules::CommandMangler &Mangler) { auto Cmd = getCompileCommandForFile(*CDB, FilePath, Mangler); if (!Cmd) return std::nullopt; using namespace clang::tooling; DependencyScanningTool ScanningTool(Service); std::string S; llvm::raw_string_ostream OS(S); DiagnosticOptions DiagOpts; DiagOpts.ShowCarets = false; TextDiagnosticPrinter DiagConsumer(OS, DiagOpts); std::optional ScanningResult = ScanningTool.getP1689ModuleDependencyFile(*Cmd, Cmd->Directory, DiagConsumer); if (!ScanningResult) { elog("Scanning modules dependencies for {0} failed: {1}", FilePath, S); std::string Cmdline; for (auto &Arg : Cmd->CommandLine) Cmdline += Arg + " "; elog("The command line the scanning tool use is: {0}", Cmdline); return std::nullopt; } ModuleDependencyInfo Result; if (ScanningResult->Provides) { Result.ModuleName = ScanningResult->Provides->ModuleName; auto [Iter, Inserted] = ModuleNameToSource.try_emplace( ScanningResult->Provides->ModuleName, FilePath); if (!Inserted && !pathEqual(normalizePath(Iter->second), normalizePath(FilePath))) { elog("Detected multiple source files ({0}, {1}) declaring the same " "module: '{2}'. " "Now clangd may find the wrong source in such case.", Iter->second, FilePath, ScanningResult->Provides->ModuleName); } } for (auto &Required : ScanningResult->Requires) Result.RequiredModules.push_back(Required.ModuleName); return Result; } void ModuleDependencyScanner::globalScan( const ProjectModules::CommandMangler &Mangler) { if (GlobalScanned) return; for (auto &File : CDB->getAllFiles()) scan(File, Mangler); GlobalScanned = true; } PathRef ModuleDependencyScanner::getSourceForModuleName( llvm::StringRef ModuleName) const { assert( GlobalScanned && "We should only call getSourceForModuleName after calling globalScan()"); if (auto It = ModuleNameToSource.find(ModuleName); It != ModuleNameToSource.end()) return It->second; return {}; } std::vector ModuleDependencyScanner::getRequiredModules( PathRef File, const ProjectModules::CommandMangler &Mangler) { auto ScanningResult = scan(File, Mangler); if (!ScanningResult) return {}; return ScanningResult->RequiredModules; } } // namespace /// TODO: The existing `ScanningAllProjectModules` is not efficient. See the /// comments in ModuleDependencyScanner for detail. /// /// In the future, we wish the build system can provide a well design /// compilation database for modules then we can query that new compilation /// database directly. Or we need to have a global long-live scanner to detect /// the state of each file. class ScanningAllProjectModules : public ProjectModules { public: ScanningAllProjectModules( std::shared_ptr CDB, const ThreadsafeFS &TFS) : Scanner(CDB, TFS) {} ~ScanningAllProjectModules() override = default; std::vector getRequiredModules(PathRef File) override { return Scanner.getRequiredModules(File, Mangler); } void setCommandMangler(CommandMangler Mangler) override { this->Mangler = std::move(Mangler); } /// RequiredSourceFile is not used intentionally. See the comments of /// ModuleDependencyScanner for detail. std::string getSourceForModuleName(llvm::StringRef ModuleName, PathRef RequiredSourceFile) override { Scanner.globalScan(Mangler); return Scanner.getSourceForModuleName(ModuleName).str(); } std::string getModuleNameForSource(PathRef File) override { auto ScanningResult = Scanner.scan(File, Mangler); if (!ScanningResult || !ScanningResult->ModuleName) return {}; return *ScanningResult->ModuleName; } // Determining Unique/Multiple needs a global scan; return Unknown for cost // reasons. We will have other ProjectModules implementations can determine // this more efficiently. ModuleNameState getModuleNameState(llvm::StringRef /*ModuleName*/) override { return ModuleNameState::Unknown; } private: ModuleDependencyScanner Scanner; CommandMangler Mangler; }; /// Reads project module information directly from compile commands. /// /// The key observation is that compile commands may already encode the mapping /// between a TU, the module names it imports, and the BMI paths it uses: /// - producers may spell the BMI path with `--precompile -o ` or /// `-fmodule-output=` /// - consumers may spell the mapping from module name to BMI path with /// `-fmodule-file==` /// /// When that information is present, we can answer /// `getSourceForModuleName(ModuleName, RequiredSourceFile)` by first looking up /// the BMI path the consumer TU uses for `ModuleName`, and then mapping that /// BMI path back to the module unit source that produced it. This avoids the /// older scanning-only approach of guessing the module unit from the module /// name alone. /// /// One subtle point is that producer commands alone do not reliably tell us the /// module name associated with a BMI path. In practice this backend learns that /// association from consumer `-fmodule-file=` entries, and then uses the BMI /// path to recover the producer source file. That is why indexing is built from /// both producer and consumer commands. /// /// Note that compilation database can be stale, so results from this backend /// should be treated as preferred hints rather than unquestionable truth. /// The compound layer below validates or falls back when needed. class CompileCommandsProjectModules : public ProjectModules { public: CompileCommandsProjectModules( std::shared_ptr CDB, const ThreadsafeFS &TFS) : CDB(std::move(CDB)), TFS(TFS) {} std::vector getRequiredModules(PathRef File) override { auto Parsed = parseFileCommand(File); if (!Parsed) return {}; std::vector Result; Result.reserve(Parsed->RequiredModuleFiles.size()); for (const auto &Required : Parsed->RequiredModuleFiles) Result.push_back(Required.getKey().str()); return Result; } std::string getModuleNameForSource(PathRef File) override { indexProducerCommands(); auto It = SourceToModuleName.find( maybeCaseFoldPath(normalizePath(File, /*WorkingDir=*/{}))); if (It == SourceToModuleName.end() || It->second.Ambiguous) return {}; return It->second.Name; } ModuleNameState getModuleNameState(llvm::StringRef ModuleName) override { indexProducerCommands(); auto It = ModuleNameToDistinctSources.find(ModuleName); if (It == ModuleNameToDistinctSources.end()) return ModuleNameState::Unknown; return It->second.size() > 1 ? ModuleNameState::Multiple : ModuleNameState::Unique; } std::string getSourceForModuleName(llvm::StringRef ModuleName, PathRef RequiredSourceFile) override { auto Parsed = parseFileCommand(RequiredSourceFile); if (!Parsed) return {}; auto It = Parsed->RequiredModuleFiles.find(ModuleName); if (It == Parsed->RequiredModuleFiles.end()) return {}; indexProducerCommands(); auto SourceIt = PCMToSource.find(maybeCaseFoldPath(It->second)); if (SourceIt == PCMToSource.end()) return {}; return SourceIt->second; } void setCommandMangler(CommandMangler Mangler) override { this->Mangler = std::move(Mangler); ProducerCommandsIndexed = false; PCMToSource.clear(); ModuleNameToDistinctSources.clear(); SourceToModuleName.clear(); } private: /// Parses the compile command for \p File into the module information /// encoded in the command line. std::optional parseFileCommand(PathRef File) const { auto Cmd = getCompileCommandForFile(*CDB, File, Mangler); if (!Cmd) return std::nullopt; return parseCompileCommandInfo(std::move(*Cmd), TFS); } /// Builds indexes from producer and consumer compile commands. /// /// Compile commands are parsed once up front. The first pass records which /// source file produces each BMI path. The second pass walks consumer /// commands, uses `-fmodule-file=` information to associate module names with /// those BMI paths, and then records which producer source files are /// referenced for each module name. void indexProducerCommands() { if (ProducerCommandsIndexed) return; std::vector ParsedCommands; auto AllFiles = CDB->getAllFiles(); ParsedCommands.reserve(AllFiles.size()); for (const auto &File : AllFiles) { auto Parsed = parseFileCommand(File); if (!Parsed) continue; if (Parsed->OutputModuleFile) PCMToSource[maybeCaseFoldPath(*Parsed->OutputModuleFile)] = Parsed->SourceFile; ParsedCommands.push_back(std::move(*Parsed)); } for (const auto &Parsed : ParsedCommands) { for (const auto &Required : Parsed.RequiredModuleFiles) { auto SourceIt = PCMToSource.find(maybeCaseFoldPath(Required.getValue())); if (SourceIt == PCMToSource.end()) continue; ModuleNameToDistinctSources[Required.getKey()].insert( maybeCaseFoldPath(SourceIt->second)); auto &Recovered = SourceToModuleName[maybeCaseFoldPath(SourceIt->second)]; if (Recovered.Name.empty()) Recovered.Name = Required.getKey().str(); else if (Recovered.Name != Required.getKey()) { if (!Recovered.Ambiguous) { elog("Detected conflicting module names ('{0}' and '{1}') for " "the same module file {2} produced by source {3}", Recovered.Name, Required.getKey(), Required.getValue(), SourceIt->second); } Recovered.Ambiguous = true; } } } ProducerCommandsIndexed = true; } std::shared_ptr CDB; const ThreadsafeFS &TFS; CommandMangler Mangler; bool ProducerCommandsIndexed = false; llvm::StringMap PCMToSource; using DistinctSourceSet = llvm::StringSet<>; llvm::StringMap ModuleNameToDistinctSources; struct RecoveredModuleName { std::string Name; bool Ambiguous = false; }; llvm::StringMap SourceToModuleName; }; /// Combines the compile-commands backend with the scanning backend. /// /// For getSourceForModuleName, it prefers compile-command-derived results when /// available to avoid scanning the whole project, but validates them against /// scanning results to avoid returning stale information. For other queries, /// it returns scanning results directly as scanning information is update to /// date. class CompoundProjectModules : public ProjectModules { public: CompoundProjectModules( std::shared_ptr CDB, const ThreadsafeFS &TFS) : CompileCommands( std::make_unique(CDB, TFS)), Scanning( std::make_unique(std::move(CDB), TFS)) {} std::vector getRequiredModules(PathRef File) override { // Return scanning results directly as it is fast enough and up to date. return Scanning->getRequiredModules(File); } std::string getModuleNameForSource(PathRef File) override { // Return scanning results directly as it is fast enough and up to date. return Scanning->getModuleNameForSource(File); } std::string getSourceForModuleName(llvm::StringRef ModuleName, PathRef RequiredSourceFile) override { auto FromCompileCommands = CompileCommands->getSourceForModuleName(ModuleName, RequiredSourceFile); // Check if the source still declares the module. // This is to validate compile-command-derived results may be stale and // scan a single file is fast enough. We just don't want to scan the project // entirely. if (!FromCompileCommands.empty() && Scanning->getModuleNameForSource(FromCompileCommands) == ModuleName) return FromCompileCommands; return Scanning->getSourceForModuleName(ModuleName, RequiredSourceFile); } ModuleNameState getModuleNameState(llvm::StringRef ModuleName) override { auto FromCompileCommands = CompileCommands->getModuleNameState(ModuleName); if (FromCompileCommands != ModuleNameState::Unknown) return FromCompileCommands; return Scanning->getModuleNameState(ModuleName); } void setCommandMangler(CommandMangler Mangler) override { this->Mangler = std::move(Mangler); auto ForwardMangler = [this](tooling::CompileCommand &Command, PathRef CommandPath) { if (this->Mangler) this->Mangler(Command, CommandPath); }; CompileCommands->setCommandMangler(ForwardMangler); Scanning->setCommandMangler(std::move(ForwardMangler)); } private: std::unique_ptr CompileCommands; std::unique_ptr Scanning; CommandMangler Mangler; }; /// Creates the project-modules facade used by clangd. /// /// The implementation is intentionally layered: /// /// CompoundProjectModules /// / \ /// v v /// CompileCommands ScanningAllProjectModules /// ProjectModules | /// | v /// | ModuleDependencyScanner /// | /// +-- preferred specifically for recovering the source file for a module /// | name in the context of a consumer TU, because compile commands /// | encode `module name -> BMI -> producer source` /// | /// +-- scanning remains fallback/validation for stale or missing data /// /// - `CompileCommandsProjectModules` reads module relationships that the build /// system already made explicit in compile commands. In particular, it uses /// producer-side BMI output paths together with consumer-side /// `-fmodule-file==` entries to recover the module unit source a /// TU actually depends on. This is the preferred source because it can /// distinguish different module producers for the same module name when /// different translation units reference different BMIs. /// - `ScanningAllProjectModules` derives module information by scanning source /// files. It is more expensive, but it can still answer queries that are not /// present in compile commands and validate compile-command-derived results. /// - `CompoundProjectModules` arbitrates between the two backends on a /// per-query basis. Compile commands are especially valuable for /// `getSourceForModuleName()` because they preserve the consumer TU's actual /// `module name -> BMI` choice. Other queries may still fall back to, or be /// validated by, scanning because compile-command information may be /// incomplete or stale. /// /// This split keeps the logic simple: compile commands provide precision when /// available, while scanning preserves compatibility with projects that have /// incomplete module information in their compilation database. std::unique_ptr getProjectModules( std::shared_ptr CDB, const ThreadsafeFS &TFS) { return std::make_unique(std::move(CDB), TFS); } } // namespace clang::clangd