Skip to content

Commit 6d9b77c

Browse files
Steelskincompnerd
andauthored
Add argument to parse headers via a source's flags
`idt` requires input to have an entry in the compilation database in order to be properly parsed. This is not always the case when parsing headers, depending on the project configuration. This change adds `--main-file=<path>`. When set, `idt` looks up `<path>` in the compilation database and adjusts the command line to parse the positional argument with the same flags as `<path>`, assuming that the positional argument is a header. Fixits are gated behind the `is_in_main_tu()` helper when `--main-file` is set to prevent spilling of fixits outside of the positional input. The helper is a no-op otherwise, preserving existing behavior. Co-authored-by: Saleem Abdulrasool <compnerd@compnerd.org>
1 parent b3bf35d commit 6d9b77c

6 files changed

Lines changed: 117 additions & 2 deletions

File tree

Sources/idt/idt.cc

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
#include "llvm/ADT/SmallPtrSet.h"
1313

1414
#include <algorithm>
15+
#include <array>
16+
#include <cstdint>
1517
#include <cstdlib>
16-
#include <iostream>
18+
#include <memory>
1719
#include <optional>
20+
#include <set>
1821
#include <string>
1922
#include <tuple>
2023
#include <unordered_map>
24+
#include <utility>
2125
#include <vector>
2226

2327
namespace idt {
@@ -63,11 +67,23 @@ ignored_symbols("ignore",
6367
llvm::cl::CommaSeparated,
6468
llvm::cl::cat(idt::category));
6569

70+
llvm::cl::opt<std::string>
71+
main_file("main-file",
72+
llvm::cl::desc("Path of a translation unit with an entry in the "
73+
"compilation database whose flags will be used to "
74+
"parse the positional argument"),
75+
llvm::cl::value_desc("path"), llvm::cl::cat(idt::category));
76+
6677
template <typename Key, typename Compare, typename Allocator>
6778
bool contains(const std::set<Key, Compare, Allocator>& set, const Key& key) {
6879
return set.find(key) != set.end();
6980
}
7081

82+
template <class T, std::size_t N>
83+
bool contains(const std::array<T, N>& arr, const T& value) {
84+
return std::find(arr.begin(), arr.end(), value) != arr.end();
85+
}
86+
7187
const std::set<std::string> &get_ignored_symbols() {
7288
static auto kIgnoredFunctions = [&]() -> std::set<std::string> {
7389
return { ignored_symbols.begin(), ignored_symbols.end() };
@@ -271,6 +287,16 @@ class visitor : public clang::RecursiveASTVisitor<visitor> {
271287
return false;
272288
}
273289

290+
// When `--main-file` is set, the positional argument is parsed as the
291+
// translation unit's main file; restrict fixits to it so we don't
292+
// accidentally rewrite decls in transitively-included headers. Outside
293+
// of `--main-file` mode, this is a no-op.
294+
template <typename Decl_> bool is_in_main_tu(const Decl_ *D) const {
295+
if (main_file.empty())
296+
return true;
297+
return source_manager_.isInMainFile(get_location(D));
298+
}
299+
274300
template <typename Decl_>
275301
inline bool is_in_system_header(const Decl_ *D) const {
276302
return source_manager_.isInSystemHeader(get_location(D));
@@ -347,6 +373,10 @@ class visitor : public clang::RecursiveASTVisitor<visitor> {
347373
if (!is_in_header(FD))
348374
return;
349375

376+
// Restrict to the main TU when `--main-file` is set.
377+
if (!is_in_main_tu(FD))
378+
return;
379+
350380
// Ignore friend declarations.
351381
if (FD->getFriendObjectKind() != clang::Decl::FOK_None)
352382
return;
@@ -431,6 +461,10 @@ class visitor : public clang::RecursiveASTVisitor<visitor> {
431461
if (!is_in_header(VD))
432462
return;
433463

464+
// Restrict to the main TU when `--main-file` is set.
465+
if (!is_in_main_tu(VD))
466+
return;
467+
434468
// Skip local variables. We are only interested in static fields.
435469
if (VD->getParentFunctionOrMethod())
436470
return;
@@ -501,6 +535,10 @@ class visitor : public clang::RecursiveASTVisitor<visitor> {
501535
if (is_in_system_header(RD))
502536
return;
503537

538+
// Restrict to the main TU when `--main-file` is set.
539+
if (!is_in_main_tu(RD))
540+
return;
541+
504542
// Skip exporting template classes. For fully-specialized template classes,
505543
// isTemplated() returns false so they will be annotated if needed.
506544
if (RD->isTemplated())
@@ -737,6 +775,59 @@ struct factory : clang::tooling::FrontendActionFactory {
737775
return std::make_unique<idt::action>();
738776
}
739777
};
778+
779+
// CompilationDatabase wrapper that looks up flags under one path (the
780+
// "donor") and reports them as belonging to a different path (the actual
781+
// input). Used by `--main-file` for projects where headers do not appear in
782+
// the compilation database.
783+
class main_file_database : public clang::tooling::CompilationDatabase {
784+
public:
785+
main_file_database(const clang::tooling::CompilationDatabase &inner,
786+
std::string donor)
787+
: inner_(inner), donor_(std::move(donor)) {}
788+
789+
std::vector<clang::tooling::CompileCommand>
790+
getCompileCommands(llvm::StringRef path) const override {
791+
auto commands = inner_.getCompileCommands(donor_);
792+
for (auto &command : commands)
793+
rewrite(command, path);
794+
return commands;
795+
}
796+
797+
// Stub these out for safety since we do not use them.
798+
std::vector<std::string> getAllFiles() const override { return {}; }
799+
800+
std::vector<clang::tooling::CompileCommand>
801+
getAllCompileCommands() const override {
802+
return {};
803+
}
804+
805+
private:
806+
static void rewrite(clang::tooling::CompileCommand &command,
807+
llvm::StringRef path) {
808+
static constexpr std::array<llvm::StringRef, 5> kSourceExts{
809+
".cpp", ".cc", ".cxx", ".c++", ".C",
810+
};
811+
812+
// Replace the first source-file argument with `path`. If none is
813+
// present, append `path` as the input.
814+
bool swapped = false;
815+
for (auto &arg : command.CommandLine) {
816+
if (contains(kSourceExts, llvm::sys::path::extension(llvm::StringRef(arg)))) {
817+
arg = path.str();
818+
swapped = true;
819+
break;
820+
}
821+
}
822+
if (!swapped)
823+
command.CommandLine.push_back(path.str());
824+
825+
command.Filename = path.str();
826+
}
827+
828+
const clang::tooling::CompilationDatabase &inner_;
829+
std::string donor_;
830+
};
740831
}
741832

742833
int main(int argc, char *argv[]) {
@@ -746,7 +837,13 @@ int main(int argc, char *argv[]) {
746837
CommonOptionsParser::create(argc, const_cast<const char **>(argv),
747838
idt::category, llvm::cl::OneOrMore);
748839
if (options) {
749-
ClangTool tool{options->getCompilations(), options->getSourcePathList()};
840+
std::unique_ptr<CompilationDatabase> wrapped;
841+
CompilationDatabase *cdb = &options->getCompilations();
842+
if (!main_file.empty()) {
843+
wrapped = std::make_unique<idt::main_file_database>(*cdb, main_file);
844+
cdb = wrapped.get();
845+
}
846+
ClangTool tool{*cdb, options->getSourcePathList()};
750847
return tool.run(new idt::factory{});
751848
} else {
752849
llvm::logAllUnhandledErrors(std::move(options.takeError()), llvm::errs());

Tests/MainFile.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// RUN: rm -rf %t && mkdir %t
2+
// RUN: echo "[{\"directory\":\"%p\",\"command\":\"clang++ -c %s\",\"file\":\"%s\"}]" | sed -e 's/\\/\\\\/g' > %t/compile_commands.json
3+
// RUN: %idt -p %t --main-file %s -export-macro IDT_TEST_ABI %S/include/MainFile.h 2>&1 | %FileCheck %s
4+
5+
// CHECK: MainFile.h:1:1: remark: unexported public interface 'main_file_target'

Tests/MainFileMainTU.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// RUN: rm -rf %t && mkdir %t
2+
// RUN: echo "[{\"directory\":\"%p\",\"command\":\"clang++ -c %s\",\"file\":\"%s\"}]" | sed -e 's/\\/\\\\/g' > %t/compile_commands.json
3+
// RUN: %idt -p %t --main-file %s -export-macro IDT_TEST_ABI %S/include/MainFileMainTU.h 2>&1 | %FileCheck %s
4+
5+
// The outer header's decl is in the main TU and gets a fixit.
6+
// CHECK: MainFileMainTU.h:2:1: remark: unexported public interface 'outer_decl'
7+
// The inner header is transitively included but is *not* the main file,
8+
// so its decl must be skipped.
9+
// CHECK-NOT: MainFileMainTUInner.h

Tests/include/MainFile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
int main_file_target(int);

Tests/include/MainFileMainTU.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#include "MainFileMainTUInner.h"
2+
int outer_decl(int);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
int inner_decl(int);

0 commit comments

Comments
 (0)