From d14965b24e4a11b70f97829838b3fa5b2f096c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller?= Date: Tue, 6 Jun 2017 17:23:48 +0200 Subject: [PATCH] Add basic compilation database parsing --- include/cppast/libclang_parser.hpp | 68 ++++++++++++--- src/libclang/libclang_parser.cpp | 135 +++++++++++++++++++++++++++++ test/CMakeLists.txt | 39 +++++---- test/libclang_parser.cpp | 70 +++++++++++++++ 4 files changed, 283 insertions(+), 29 deletions(-) create mode 100644 test/libclang_parser.cpp diff --git a/include/cppast/libclang_parser.hpp b/include/cppast/libclang_parser.hpp index 87f590d..a5f24ef 100644 --- a/include/cppast/libclang_parser.hpp +++ b/include/cppast/libclang_parser.hpp @@ -25,6 +25,50 @@ namespace cppast }; } // namespace detail + /// The exception thrown when a fatal parse error occurs. + class libclang_error final : public std::runtime_error + { + public: + /// \effects Creates it with a message. + libclang_error(std::string msg) : std::runtime_error(std::move(msg)) + { + } + }; + + /// A compilation database. + /// + /// This represents a `compile_commands.json` file, + /// which stores all the commands needed to compile a set of files. + /// It can be generated by CMake using the `CMAKE_EXPORT_COMPILE_COMMANDS` option. + class libclang_compilation_database + { + public: + /// \effects Creates it giving the directory where the `compile_commands.json` file is located. + /// \throws `libclang_error` if the database could not be loaded or found. + libclang_compilation_database(const std::string& build_directory); + + libclang_compilation_database(libclang_compilation_database&& other) + : database_(other.database_) + { + other.database_ = nullptr; + } + + ~libclang_compilation_database(); + + libclang_compilation_database& operator=(libclang_compilation_database&& other) + { + libclang_compilation_database tmp(std::move(other)); + std::swap(tmp.database_, database_); + return *this; + } + + private: + using database = void*; + database database_; + + friend libclang_compile_config; + }; + /// Compilation config for the [cppast::libclang_parser](). class libclang_compile_config final : public compile_config { @@ -36,6 +80,20 @@ namespace cppast /// It will also define `__cppast__` with the value `"libclang"` as well as `__cppast_major__` and `__cppast_minor__`. libclang_compile_config(); + /// Creates the configuration stored in the database. + /// + /// \effects It will use the options found in the database for the specified file. + /// This does not necessarily need to match the file that is going to be parsed, + /// but it should. + /// It will also add the default configuration options. + /// \notes Header files are not included in the compilation database, + /// you need to pass in the file name of the corresponding source file, + /// if you want to parse one. + /// \notes It will only consider options you could also set by the other functions. + /// \notes The file key will include the specified directory in the JSON, if it is not a full path. + libclang_compile_config(const libclang_compilation_database& database, + const std::string& file); + /// \effects Sets the path to the location of the `clang++` binary and the version of that binary. /// \notes It will be used for preprocessing. void set_clang_binary(std::string binary, int major, int minor, int patch) @@ -64,16 +122,6 @@ namespace cppast friend detail::libclang_compile_config_access; }; - /// The exception thrown when a fatal parse error occurs. - class libclang_error final : public std::runtime_error - { - public: - /// \effects Creates it with a message. - libclang_error(std::string msg) : std::runtime_error(std::move(msg)) - { - } - }; - /// A parser that uses libclang. class libclang_parser final : public parser { diff --git a/src/libclang/libclang_parser.cpp b/src/libclang/libclang_parser.cpp index 4718e97..44bcb85 100644 --- a/src/libclang/libclang_parser.cpp +++ b/src/libclang/libclang_parser.cpp @@ -7,6 +7,8 @@ #include #include +#include + #include "libclang_visitor.hpp" #include "raii_wrapper.hpp" #include "parse_error.hpp" @@ -33,6 +35,22 @@ const std::vector& detail::libclang_compile_config_access::flags( return config.get_flags(); } +libclang_compilation_database::libclang_compilation_database(const std::string& build_directory) +{ + static_assert(std::is_same::value, "forgot to update type"); + + auto error = CXCompilationDatabase_NoError; + database_ = clang_CompilationDatabase_fromDirectory(build_directory.c_str(), &error); + if (error != CXCompilationDatabase_NoError) + throw libclang_error("unable to load compilation database"); +} + +libclang_compilation_database::~libclang_compilation_database() +{ + if (database_) + clang_CompilationDatabase_dispose(database_); +} + namespace { int parse_number(const char*& str) @@ -65,6 +83,123 @@ libclang_compile_config::libclang_compile_config() : compile_config({}) define_macro("__cppast_version_minor__", CPPAST_VERSION_MINOR); } +namespace +{ + struct cxcompile_commands_deleter + { + void operator()(CXCompileCommands cmds) + { + clang_CompileCommands_dispose(cmds); + } + }; + + using cxcompile_commands = detail::raii_wrapper; +} + +namespace +{ + bool is_flag(const detail::cxstring& str) + { + return str.length() > 1u && str[0] == '-'; + } + + const char* find_flag_arg_sep(const std::string& last_flag) + { + if (last_flag[1] == 'D') + // no separator, equal is part of the arg + return nullptr; + return std::strchr(last_flag.c_str(), '='); + } + + template + void parse_flags(CXCompileCommand cmd, Func callback) + { + auto no_args = clang_CompileCommand_getNumArgs(cmd); + std::string last_flag; + for (auto i = 1u /* 0 is compiler executable */; i != no_args; ++i) + { + detail::cxstring str(clang_CompileCommand_getArg(cmd, i)); + if (is_flag(str)) + { + if (!last_flag.empty()) + { + // process last flag + std::string args; + if (auto ptr = find_flag_arg_sep(last_flag)) + { + auto pos = std::size_t(ptr - last_flag.c_str()); + ++ptr; + while (*ptr) + args += *ptr++; + last_flag.erase(pos); + } + else if (last_flag.size() > 2u) + { + // assume two character flag + args = last_flag.substr(2u); + last_flag.erase(2u); + } + + callback(std::move(last_flag), std::move(args)); + } + + last_flag = str.std_str(); + } + else if (!last_flag.empty()) + { + // we have flags + args + callback(std::move(last_flag), str.std_str()); + last_flag.clear(); + } + // else skip argument + } + } +} + +libclang_compile_config::libclang_compile_config(const libclang_compilation_database& database, + const std::string& file) +: libclang_compile_config() +{ + auto cxcommands = + clang_CompilationDatabase_getCompileCommands(database.database_, file.c_str()); + if (cxcommands == nullptr) + throw libclang_error(detail::format("no compile commands specified for file '", file, "'")); + cxcompile_commands commands(cxcommands); + + auto size = clang_CompileCommands_getSize(commands.get()); + for (auto i = 0u; i != size; ++i) + { + auto cmd = clang_CompileCommands_getCommand(commands.get(), i); + auto dir = detail::cxstring(clang_CompileCommand_getDirectory(cmd)); + parse_flags(cmd, [&](std::string flag, std::string args) { + if (flag == "-I") + { + if (args.front() == '/' || args.front() == '\\') + { + add_flag(std::move(flag) + std::move(args)); + } + else + { + // path relative to the directory + if (dir[dir.length() - 1] != '/' && dir[dir.length() - 1] != '\\') + add_flag(std::move(flag) + dir.std_str() + '/' + std::move(args)); + else + add_flag(std::move(flag) + dir.std_str() + std::move(args)); + } + } + else if (flag == "-D" || flag == "-U") + // preprocessor options + this->add_flag(std::move(flag) + std::move(args)); + else if (flag == "-std") + // standard + this->add_flag(std::move(flag) + "=" + std::move(args)); + else if (flag == "-f" && (args == "ms-compatibility" || args == "ms-extensions")) + // other options + this->add_flag(std::move(flag) + std::move(args)); + }); + } +} + void libclang_compile_config::do_set_flags(cpp_standard standard, compile_flags flags) { switch (standard) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a613ec6..1ac93b9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -9,25 +9,26 @@ if(NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/catch.hpp) endif() set(tests - code_generator.cpp - cpp_alias_template.cpp - cpp_class.cpp - cpp_class_template.cpp - cpp_enum.cpp - cpp_friend.cpp - cpp_function.cpp - cpp_function_template.cpp - cpp_language_linkage.cpp - cpp_member_function.cpp - cpp_member_variable.cpp - cpp_namespace.cpp - cpp_preprocessor.cpp - cpp_static_assert.cpp - cpp_template_parameter.cpp - cpp_type_alias.cpp - cpp_variable.cpp - visitor.cpp - integration.cpp) + code_generator.cpp + cpp_alias_template.cpp + cpp_class.cpp + cpp_class_template.cpp + cpp_enum.cpp + cpp_friend.cpp + cpp_function.cpp + cpp_function_template.cpp + cpp_language_linkage.cpp + cpp_member_function.cpp + cpp_member_variable.cpp + cpp_namespace.cpp + cpp_preprocessor.cpp + cpp_static_assert.cpp + cpp_template_parameter.cpp + cpp_type_alias.cpp + cpp_variable.cpp + integration.cpp + libclang_parser.cpp + visitor.cpp) add_executable(cppast_test test.cpp test_parser.hpp ${tests}) target_include_directories(cppast_test PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/test/libclang_parser.cpp b/test/libclang_parser.cpp new file mode 100644 index 0000000..718c99c --- /dev/null +++ b/test/libclang_parser.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2017 Jonathan Müller +// This file is subject to the license terms in the LICENSE file +// found in the top-level directory of this distribution. + +#include + +#include + +#include + +using namespace cppast; + +libclang_compilation_database get_database(const char* json) +{ + std::ofstream file("compile_commands.json"); + file << json; + file.close(); + + return libclang_compilation_database("."); +} + +void require_flags(const libclang_compile_config& config, const char* flags) +{ + std::string result; + auto config_flags = detail::libclang_compile_config_access::flags(config); + // skip first 4, those are the default options + for (auto iter = config_flags.begin() + 4; iter != config_flags.end(); ++iter) + result += *iter + ' '; + result.pop_back(); + REQUIRE(result == flags); +} + +TEST_CASE("libclang_compile_config") +{ + // only test database parser + auto json = R"([ +{ + "directory": "/foo", + "command": "/usr/bin/clang++ -Irelative -I/absolute -DA=FOO -DB(X)=X -c -o a.o a.cpp", + "file": "a.cpp" +}, +{ + "directory": "/bar/", + "command": "/usr/bin/clang++ -Irelative -DA=FOO -c -o b.o b.cpp", + "command": "/usr/bin/clang++ -I/absolute -DB(X)=X -c -o b.o b.cpp", + "file": "/b.cpp", +}, +{ + "directory": "/bar/", + "command": "/usr/bin/clang++ -I/absolute -DB(X)=X -c -o b.o b.cpp", + "file": "/b.cpp", +}, +{ + "directory": "", + "command": "/usr/bin/clang++ -std=c++14 -fms-extensions -fms-compatibility -c -o c.o c.cpp", + "file": "/c.cpp", +} +])"; + + auto database = get_database(json); + + libclang_compile_config a(database, "/foo/a.cpp"); + require_flags(a, "-I/foo/relative -I/absolute -DA=FOO -DB(X)=X"); + + libclang_compile_config b(database, "/b.cpp"); + require_flags(b, "-I/bar/relative -DA=FOO -I/absolute -DB(X)=X"); + + libclang_compile_config c(database, "/c.cpp"); + require_flags(c, "-std=c++14 -fms-extensions -fms-compatibility"); +}