diff --git a/Sources/idt/idt.cc b/Sources/idt/idt.cc index d383a91..b68e0fe 100644 --- a/Sources/idt/idt.cc +++ b/Sources/idt/idt.cc @@ -12,12 +12,16 @@ #include "llvm/ADT/SmallPtrSet.h" #include +#include +#include #include -#include +#include #include +#include #include #include #include +#include #include namespace idt { @@ -63,11 +67,23 @@ ignored_symbols("ignore", llvm::cl::CommaSeparated, llvm::cl::cat(idt::category)); +llvm::cl::opt +main_file("main-file", + llvm::cl::desc("Path of a translation unit with an entry in the " + "compilation database whose flags will be used to " + "parse the positional argument"), + llvm::cl::value_desc("path"), llvm::cl::cat(idt::category)); + template bool contains(const std::set& set, const Key& key) { return set.find(key) != set.end(); } +template +bool contains(const std::array& arr, const T& value) { + return std::find(arr.begin(), arr.end(), value) != arr.end(); +} + const std::set &get_ignored_symbols() { static auto kIgnoredFunctions = [&]() -> std::set { return { ignored_symbols.begin(), ignored_symbols.end() }; @@ -271,6 +287,16 @@ class visitor : public clang::RecursiveASTVisitor { return false; } + // When `--main-file` is set, the positional argument is parsed as the + // translation unit's main file; restrict fixits to it so we don't + // accidentally rewrite decls in transitively-included headers. Outside + // of `--main-file` mode, this is a no-op. + template bool is_in_main_tu(const Decl_ *D) const { + if (main_file.empty()) + return true; + return source_manager_.isInMainFile(get_location(D)); + } + template inline bool is_in_system_header(const Decl_ *D) const { return source_manager_.isInSystemHeader(get_location(D)); @@ -347,6 +373,10 @@ class visitor : public clang::RecursiveASTVisitor { if (!is_in_header(FD)) return; + // Restrict to the main TU when `--main-file` is set. + if (!is_in_main_tu(FD)) + return; + // Ignore friend declarations. if (FD->getFriendObjectKind() != clang::Decl::FOK_None) return; @@ -431,6 +461,10 @@ class visitor : public clang::RecursiveASTVisitor { if (!is_in_header(VD)) return; + // Restrict to the main TU when `--main-file` is set. + if (!is_in_main_tu(VD)) + return; + // Skip local variables. We are only interested in static fields. if (VD->getParentFunctionOrMethod()) return; @@ -501,6 +535,10 @@ class visitor : public clang::RecursiveASTVisitor { if (is_in_system_header(RD)) return; + // Restrict to the main TU when `--main-file` is set. + if (!is_in_main_tu(RD)) + return; + // Skip exporting template classes. For fully-specialized template classes, // isTemplated() returns false so they will be annotated if needed. if (RD->isTemplated()) @@ -737,6 +775,59 @@ struct factory : clang::tooling::FrontendActionFactory { return std::make_unique(); } }; + +// CompilationDatabase wrapper that looks up flags under one path (the +// "donor") and reports them as belonging to a different path (the actual +// input). Used by `--main-file` for projects where headers do not appear in +// the compilation database. +class main_file_database : public clang::tooling::CompilationDatabase { +public: + main_file_database(const clang::tooling::CompilationDatabase &inner, + std::string donor) + : inner_(inner), donor_(std::move(donor)) {} + + std::vector + getCompileCommands(llvm::StringRef path) const override { + auto commands = inner_.getCompileCommands(donor_); + for (auto &command : commands) + rewrite(command, path); + return commands; + } + + // Stub these out for safety since we do not use them. + std::vector getAllFiles() const override { return {}; } + + std::vector + getAllCompileCommands() const override { + return {}; + } + +private: + static void rewrite(clang::tooling::CompileCommand &command, + llvm::StringRef path) { + static constexpr std::array kSourceExts{ + ".cpp", ".cc", ".cxx", ".c++", ".C", + }; + + // Replace the first source-file argument with `path`. If none is + // present, append `path` as the input. + bool swapped = false; + for (auto &arg : command.CommandLine) { + if (contains(kSourceExts, llvm::sys::path::extension(llvm::StringRef(arg)))) { + arg = path.str(); + swapped = true; + break; + } + } + if (!swapped) + command.CommandLine.push_back(path.str()); + + command.Filename = path.str(); + } + + const clang::tooling::CompilationDatabase &inner_; + std::string donor_; +}; } int main(int argc, char *argv[]) { @@ -746,7 +837,13 @@ int main(int argc, char *argv[]) { CommonOptionsParser::create(argc, const_cast(argv), idt::category, llvm::cl::OneOrMore); if (options) { - ClangTool tool{options->getCompilations(), options->getSourcePathList()}; + std::unique_ptr wrapped; + CompilationDatabase *cdb = &options->getCompilations(); + if (!main_file.empty()) { + wrapped = std::make_unique(*cdb, main_file); + cdb = wrapped.get(); + } + ClangTool tool{*cdb, options->getSourcePathList()}; return tool.run(new idt::factory{}); } else { llvm::logAllUnhandledErrors(std::move(options.takeError()), llvm::errs()); diff --git a/Tests/MainFile.cc b/Tests/MainFile.cc new file mode 100644 index 0000000..0c42c40 --- /dev/null +++ b/Tests/MainFile.cc @@ -0,0 +1,5 @@ +// RUN: rm -rf %t && mkdir %t +// RUN: echo "[{\"directory\":\"%p\",\"command\":\"clang++ -c %s\",\"file\":\"%s\"}]" | sed -e 's/\\/\\\\/g' > %t/compile_commands.json +// RUN: %idt -p %t --main-file %s -export-macro IDT_TEST_ABI %S/include/MainFile.h 2>&1 | %FileCheck %s + +// CHECK: MainFile.h:1:1: remark: unexported public interface 'main_file_target' diff --git a/Tests/MainFileMainTU.cc b/Tests/MainFileMainTU.cc new file mode 100644 index 0000000..54935cc --- /dev/null +++ b/Tests/MainFileMainTU.cc @@ -0,0 +1,9 @@ +// RUN: rm -rf %t && mkdir %t +// RUN: echo "[{\"directory\":\"%p\",\"command\":\"clang++ -c %s\",\"file\":\"%s\"}]" | sed -e 's/\\/\\\\/g' > %t/compile_commands.json +// RUN: %idt -p %t --main-file %s -export-macro IDT_TEST_ABI %S/include/MainFileMainTU.h 2>&1 | %FileCheck %s + +// The outer header's decl is in the main TU and gets a fixit. +// CHECK: MainFileMainTU.h:2:1: remark: unexported public interface 'outer_decl' +// The inner header is transitively included but is *not* the main file, +// so its decl must be skipped. +// CHECK-NOT: MainFileMainTUInner.h diff --git a/Tests/include/MainFile.h b/Tests/include/MainFile.h new file mode 100644 index 0000000..7d358e9 --- /dev/null +++ b/Tests/include/MainFile.h @@ -0,0 +1 @@ +int main_file_target(int); diff --git a/Tests/include/MainFileMainTU.h b/Tests/include/MainFileMainTU.h new file mode 100644 index 0000000..f7bc8db --- /dev/null +++ b/Tests/include/MainFileMainTU.h @@ -0,0 +1,2 @@ +#include "MainFileMainTUInner.h" +int outer_decl(int); diff --git a/Tests/include/MainFileMainTUInner.h b/Tests/include/MainFileMainTUInner.h new file mode 100644 index 0000000..69dd2e0 --- /dev/null +++ b/Tests/include/MainFileMainTUInner.h @@ -0,0 +1 @@ +int inner_decl(int);