421 lines · cpp
1//===--- extra/module-map-checker/CoverageChecker.cpp -------------------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8//9// This file implements a class that validates a module map by checking that10// all headers in the corresponding directories are accounted for.11//12// This class uses a previously loaded module map object.13// Starting at the module map file directory, or just the include14// paths, if specified, it will collect the names of all the files it15// considers headers (no extension, .h, or .inc--if you need more, modify the16// ModularizeUtilities::isHeader function).17// It then compares the headers against those referenced18// in the module map, either explicitly named, or implicitly named via an19// umbrella directory or umbrella file, as parsed by the ModuleMap object.20// If headers are found which are not referenced or covered by an umbrella21// directory or file, warning messages will be produced, and the doChecks22// function will return an error code of 1. Other errors result in an error23// code of 2. If no problems are found, an error code of 0 is returned.24//25// Note that in the case of umbrella headers, this tool invokes the compiler26// to preprocess the file, and uses a callback to collect the header files27// included by the umbrella header or any of its nested includes. If any28// front end options are needed for these compiler invocations, these are29// to be passed in via the CommandLine parameter.30//31// Warning message have the form:32//33// warning: module.modulemap does not account for file: Level3A.h34//35// Note that for the case of the module map referencing a file that does36// not exist, the module map parser in Clang will (at the time of this37// writing) display an error message.38//39// Potential problems with this program:40//41// 1. Might need a better header matching mechanism, or extensions to the42// canonical file format used.43//44// 2. It might need to support additional header file extensions.45//46// Future directions:47//48// 1. Add an option to fix the problems found, writing a new module map.49// Include an extra option to add unaccounted-for headers as excluded.50//51//===----------------------------------------------------------------------===//52 53#include "CoverageChecker.h"54#include "ModularizeUtilities.h"55#include "clang/AST/ASTConsumer.h"56#include "clang/AST/ASTContext.h"57#include "clang/AST/RecursiveASTVisitor.h"58#include "clang/Basic/SourceManager.h"59#include "clang/Frontend/CompilerInstance.h"60#include "clang/Frontend/FrontendAction.h"61#include "clang/Frontend/FrontendActions.h"62#include "clang/Lex/PPCallbacks.h"63#include "clang/Lex/Preprocessor.h"64#include "clang/Options/Options.h"65#include "clang/Tooling/CompilationDatabase.h"66#include "clang/Tooling/Tooling.h"67#include "llvm/Option/Option.h"68#include "llvm/Support/CommandLine.h"69#include "llvm/Support/FileSystem.h"70#include "llvm/Support/Path.h"71#include "llvm/Support/raw_ostream.h"72 73using namespace Modularize;74using namespace clang;75using namespace clang::driver;76using namespace clang::options;77using namespace clang::tooling;78namespace cl = llvm::cl;79namespace sys = llvm::sys;80 81// Preprocessor callbacks.82// We basically just collect include files.83class CoverageCheckerCallbacks : public PPCallbacks {84public:85 CoverageCheckerCallbacks(CoverageChecker &Checker) : Checker(Checker) {}86 ~CoverageCheckerCallbacks() override {}87 88 // Include directive callback.89 void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,90 StringRef FileName, bool IsAngled,91 CharSourceRange FilenameRange,92 OptionalFileEntryRef File, StringRef SearchPath,93 StringRef RelativePath, const Module *SuggestedModule,94 bool ModuleImported,95 SrcMgr::CharacteristicKind FileType) override {96 Checker.collectUmbrellaHeaderHeader(File->getName());97 }98 99private:100 CoverageChecker &Checker;101};102 103// Frontend action stuff:104 105// Consumer is responsible for setting up the callbacks.106class CoverageCheckerConsumer : public ASTConsumer {107public:108 CoverageCheckerConsumer(CoverageChecker &Checker, Preprocessor &PP) {109 // PP takes ownership.110 PP.addPPCallbacks(std::make_unique<CoverageCheckerCallbacks>(Checker));111 }112};113 114class CoverageCheckerAction : public SyntaxOnlyAction {115public:116 CoverageCheckerAction(CoverageChecker &Checker) : Checker(Checker) {}117 118protected:119 std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,120 StringRef InFile) override {121 return std::make_unique<CoverageCheckerConsumer>(Checker,122 CI.getPreprocessor());123 }124 125private:126 CoverageChecker &Checker;127};128 129class CoverageCheckerFrontendActionFactory : public FrontendActionFactory {130public:131 CoverageCheckerFrontendActionFactory(CoverageChecker &Checker)132 : Checker(Checker) {}133 134 std::unique_ptr<FrontendAction> create() override {135 return std::make_unique<CoverageCheckerAction>(Checker);136 }137 138private:139 CoverageChecker &Checker;140};141 142// CoverageChecker class implementation.143 144// Constructor.145CoverageChecker::CoverageChecker(StringRef ModuleMapPath,146 std::vector<std::string> &IncludePaths,147 ArrayRef<std::string> CommandLine,148 clang::ModuleMap *ModuleMap)149 : ModuleMapPath(ModuleMapPath), IncludePaths(IncludePaths),150 CommandLine(CommandLine),151 ModMap(ModuleMap) {}152 153// Create instance of CoverageChecker, to simplify setting up154// subordinate objects.155std::unique_ptr<CoverageChecker> CoverageChecker::createCoverageChecker(156 StringRef ModuleMapPath, std::vector<std::string> &IncludePaths,157 ArrayRef<std::string> CommandLine, clang::ModuleMap *ModuleMap) {158 159 return std::make_unique<CoverageChecker>(ModuleMapPath, IncludePaths,160 CommandLine, ModuleMap);161}162 163// Do checks.164// Starting from the directory of the module.modulemap file,165// Find all header files, optionally looking only at files166// covered by the include path options, and compare against167// the headers referenced by the module.modulemap file.168// Display warnings for unaccounted-for header files.169// Returns error_code of 0 if there were no errors or warnings, 1 if there170// were warnings, 2 if any other problem, such as if a bad171// module map path argument was specified.172std::error_code CoverageChecker::doChecks() {173 std::error_code returnValue;174 175 // Collect the headers referenced in the modules.176 collectModuleHeaders();177 178 // Collect the file system headers.179 if (!collectFileSystemHeaders())180 return std::error_code(2, std::generic_category());181 182 // Do the checks. These save the problematic file names.183 findUnaccountedForHeaders();184 185 // Check for warnings.186 if (!UnaccountedForHeaders.empty())187 returnValue = std::error_code(1, std::generic_category());188 189 return returnValue;190}191 192// The following functions are called by doChecks.193 194// Collect module headers.195// Walks the modules and collects referenced headers into196// ModuleMapHeadersSet.197void CoverageChecker::collectModuleHeaders() {198 for (ModuleMap::module_iterator I = ModMap->module_begin(),199 E = ModMap->module_end();200 I != E; ++I) {201 collectModuleHeaders(*I->second);202 }203}204 205// Collect referenced headers from one module.206// Collects the headers referenced in the given module into207// ModuleMapHeadersSet.208// FIXME: Doesn't collect files from umbrella header.209bool CoverageChecker::collectModuleHeaders(const Module &Mod) {210 211 if (std::optional<Module::Header> UmbrellaHeader =212 Mod.getUmbrellaHeaderAsWritten()) {213 // Collect umbrella header.214 ModuleMapHeadersSet.insert(215 ModularizeUtilities::getCanonicalPath(UmbrellaHeader->Entry.getName()));216 // Preprocess umbrella header and collect the headers it references.217 if (!collectUmbrellaHeaderHeaders(UmbrellaHeader->Entry.getName()))218 return false;219 } else if (std::optional<Module::DirectoryName> UmbrellaDir =220 Mod.getUmbrellaDirAsWritten()) {221 // Collect headers in umbrella directory.222 if (!collectUmbrellaHeaders(UmbrellaDir->Entry.getName()))223 return false;224 }225 226 for (const auto &Header : Mod.getAllHeaders())227 ModuleMapHeadersSet.insert(228 ModularizeUtilities::getCanonicalPath(Header.Entry.getName()));229 230 for (auto *Submodule : Mod.submodules())231 collectModuleHeaders(*Submodule);232 233 return true;234}235 236// Collect headers from an umbrella directory.237bool CoverageChecker::collectUmbrellaHeaders(StringRef UmbrellaDirName) {238 // Initialize directory name.239 SmallString<256> Directory(ModuleMapDirectory);240 if (UmbrellaDirName.size())241 sys::path::append(Directory, UmbrellaDirName);242 if (Directory.size() == 0)243 Directory = ".";244 // Walk the directory.245 std::error_code EC;246 for (sys::fs::directory_iterator I(Directory.str(), EC), E; I != E;247 I.increment(EC)) {248 if (EC)249 return false;250 std::string File(I->path());251 llvm::ErrorOr<sys::fs::basic_file_status> Status = I->status();252 if (!Status)253 return false;254 sys::fs::file_type Type = Status->type();255 // If the file is a directory, ignore the name and recurse.256 if (Type == sys::fs::file_type::directory_file) {257 if (!collectUmbrellaHeaders(File))258 return false;259 continue;260 }261 // If the file does not have a common header extension, ignore it.262 if (!ModularizeUtilities::isHeader(File))263 continue;264 // Save header name.265 ModuleMapHeadersSet.insert(ModularizeUtilities::getCanonicalPath(File));266 }267 return true;268}269 270// Collect headers referenced from an umbrella file.271bool272CoverageChecker::collectUmbrellaHeaderHeaders(StringRef UmbrellaHeaderName) {273 274 SmallString<256> PathBuf(ModuleMapDirectory);275 276 // If directory is empty, it's the current directory.277 if (ModuleMapDirectory.length() == 0)278 sys::fs::current_path(PathBuf);279 280 // Create the compilation database.281 FixedCompilationDatabase Compilations(Twine(PathBuf), CommandLine);282 283 std::vector<std::string> HeaderPath;284 HeaderPath.push_back(std::string(UmbrellaHeaderName));285 286 // Create the tool and run the compilation.287 ClangTool Tool(Compilations, HeaderPath);288 CoverageCheckerFrontendActionFactory ActionFactory(*this);289 int HadErrors = Tool.run(&ActionFactory);290 291 // If we had errors, exit early.292 return !HadErrors;293}294 295// Called from CoverageCheckerCallbacks to track a header included296// from an umbrella header.297void CoverageChecker::collectUmbrellaHeaderHeader(StringRef HeaderName) {298 299 SmallString<256> PathBuf(ModuleMapDirectory);300 // If directory is empty, it's the current directory.301 if (ModuleMapDirectory.length() == 0)302 sys::fs::current_path(PathBuf);303 // HeaderName will have an absolute path, so if it's the module map304 // directory, we remove it, also skipping trailing separator.305 if (HeaderName.starts_with(PathBuf))306 HeaderName = HeaderName.substr(PathBuf.size() + 1);307 // Save header name.308 ModuleMapHeadersSet.insert(ModularizeUtilities::getCanonicalPath(HeaderName));309}310 311// Collect file system header files.312// This function scans the file system for header files,313// starting at the directory of the module.modulemap file,314// optionally filtering out all but the files covered by315// the include path options.316// Returns true if no errors.317bool CoverageChecker::collectFileSystemHeaders() {318 319 // Get directory containing the module.modulemap file.320 // Might be relative to current directory, absolute, or empty.321 ModuleMapDirectory = ModularizeUtilities::getDirectoryFromPath(ModuleMapPath);322 323 // If no include paths specified, we do the whole tree starting324 // at the module.modulemap directory.325 if (IncludePaths.size() == 0) {326 if (!collectFileSystemHeaders(StringRef("")))327 return false;328 }329 else {330 // Otherwise we only look at the sub-trees specified by the331 // include paths.332 for (const std::string &IncludePath : IncludePaths) {333 if (!collectFileSystemHeaders(IncludePath))334 return false;335 }336 }337 338 // Sort it, because different file systems might order the file differently.339 llvm::sort(FileSystemHeaders);340 341 return true;342}343 344// Collect file system header files from the given path.345// This function scans the file system for header files,346// starting at the given directory, which is assumed to be347// relative to the directory of the module.modulemap file.348// \returns True if no errors.349bool CoverageChecker::collectFileSystemHeaders(StringRef IncludePath) {350 351 // Initialize directory name.352 SmallString<256> Directory(ModuleMapDirectory);353 if (IncludePath.size())354 sys::path::append(Directory, IncludePath);355 if (Directory.size() == 0)356 Directory = ".";357 if (IncludePath.starts_with("/") || IncludePath.starts_with("\\") ||358 ((IncludePath.size() >= 2) && (IncludePath[1] == ':'))) {359 llvm::errs() << "error: Include path \"" << IncludePath360 << "\" is not relative to the module map file.\n";361 return false;362 }363 364 // Recursively walk the directory tree.365 std::error_code EC;366 int Count = 0;367 for (sys::fs::recursive_directory_iterator I(Directory.str(), EC), E; I != E;368 I.increment(EC)) {369 if (EC)370 return false;371 //std::string file(I->path());372 StringRef file(I->path());373 llvm::ErrorOr<sys::fs::basic_file_status> Status = I->status();374 if (!Status)375 return false;376 sys::fs::file_type type = Status->type();377 // If the file is a directory, ignore the name (but still recurses).378 if (type == sys::fs::file_type::directory_file)379 continue;380 // Assume directories or files starting with '.' are private and not to381 // be considered.382 if (file.contains("\\.") || file.contains("/."))383 continue;384 // If the file does not have a common header extension, ignore it.385 if (!ModularizeUtilities::isHeader(file))386 continue;387 // Save header name.388 FileSystemHeaders.push_back(ModularizeUtilities::getCanonicalPath(file));389 Count++;390 }391 if (Count == 0) {392 llvm::errs() << "warning: No headers found in include path: \""393 << IncludePath << "\"\n";394 }395 return true;396}397 398// Find headers unaccounted-for in module map.399// This function compares the list of collected header files400// against those referenced in the module map. Display401// warnings for unaccounted-for header files.402// Save unaccounted-for file list for possible.403// fixing action.404// FIXME: There probably needs to be some canonalization405// of file names so that header path can be correctly406// matched. Also, a map could be used for the headers407// referenced in the module, but408void CoverageChecker::findUnaccountedForHeaders() {409 // Walk over file system headers.410 for (std::vector<std::string>::const_iterator I = FileSystemHeaders.begin(),411 E = FileSystemHeaders.end();412 I != E; ++I) {413 // Look for header in module map.414 if (ModuleMapHeadersSet.insert(*I).second) {415 UnaccountedForHeaders.push_back(*I);416 llvm::errs() << "warning: " << ModuleMapPath417 << " does not account for file: " << *I << "\n";418 }419 }420}421